From 0a0a8d0777056b095328c3b40006a1e2058f133b Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Tue, 13 Oct 2026 10:19:11 +0200 Subject: [PATCH 1/5] fix: format the package TimePicker value as a time span Value is a TimeSpan?, and "HH:mm" is a DateTime format, so setting Value threw a FormatException on render. It now uses @"hh\:mm" like the CLI template. --- ShellUI.Tests/PackageBehaviorTests.cs | 10 ++++++++++ src/ShellUI.Components/Components/TimePicker.razor | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ShellUI.Tests/PackageBehaviorTests.cs b/ShellUI.Tests/PackageBehaviorTests.cs index f1931dc..f7427d6 100644 --- a/ShellUI.Tests/PackageBehaviorTests.cs +++ b/ShellUI.Tests/PackageBehaviorTests.cs @@ -62,4 +62,14 @@ public void Dropdown_ClosesOnEscapeButNotOnOtherKeys() dropdown.Find("div.relative").KeyDown(new KeyboardEventArgs { Key = "Escape" }); Assert.Equal(new[] { false }, changes); } + + [Fact] + public void TimePicker_ShowsASetValue() + { + using var ctx = new BunitContext(); + + var picker = ctx.Render(p => p.Add(c => c.Value, new TimeSpan(9, 5, 0))); + + Assert.Contains("09:05", picker.Markup); + } } diff --git a/src/ShellUI.Components/Components/TimePicker.razor b/src/ShellUI.Components/Components/TimePicker.razor index 116facd..f0615c8 100644 --- a/src/ShellUI.Components/Components/TimePicker.razor +++ b/src/ShellUI.Components/Components/TimePicker.razor @@ -6,7 +6,7 @@ disabled="@Disabled" class="@Shell.Cn("flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", Disabled ? "cursor-not-allowed opacity-50" : "")"> - @(Value?.ToString("HH:mm") ?? Placeholder) + @(Value?.ToString(@"hh\:mm") ?? Placeholder) From 672bdaeb20193298e7ae15b7dc64a62bbbe20dd4 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Tue, 13 Oct 2026 10:19:11 +0200 Subject: [PATCH 2/5] fix: set color-scheme for the light and dark themes Without it the browser drew its own controls (date pickers, scrollbars, select lists, autofill) light on a dark page. The init theme, theme apply output, the package theme and the demo now set color-scheme in :root and .dark. --- .../BlazorInteractiveServer/wwwroot/input.css | 2 + ShellUI.Tests/ColorSchemeTests.cs | 42 +++++++++++++++++++ src/ShellUI.CLI/Services/ThemeService.cs | 8 ++-- .../wwwroot/shellui-theme.css | 2 + src/ShellUI.Templates/CssTemplates.cs | 2 + 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 ShellUI.Tests/ColorSchemeTests.cs diff --git a/NET10/BlazorInteractiveServer/wwwroot/input.css b/NET10/BlazorInteractiveServer/wwwroot/input.css index 63870d7..135d5ae 100644 --- a/NET10/BlazorInteractiveServer/wwwroot/input.css +++ b/NET10/BlazorInteractiveServer/wwwroot/input.css @@ -3,6 +3,7 @@ @custom-variant dark (&:is(.dark *)); :root { + color-scheme: light; --background: oklch(0.9900 0 0); --foreground: oklch(0 0 0); --card: oklch(1 0 0); @@ -58,6 +59,7 @@ } .dark { + color-scheme: dark; --background: oklch(0 0 0); --foreground: oklch(1 0 0); --card: oklch(0.1400 0 0); diff --git a/ShellUI.Tests/ColorSchemeTests.cs b/ShellUI.Tests/ColorSchemeTests.cs new file mode 100644 index 0000000..5a72c61 --- /dev/null +++ b/ShellUI.Tests/ColorSchemeTests.cs @@ -0,0 +1,42 @@ +using System.IO; +using ShellUI.CLI.Services; +using ShellUI.Templates; +using Xunit; + +namespace ShellUI.Tests; + +public class ColorSchemeTests +{ + [Fact] + public void InitTheme_SetsColorSchemeForBothThemes() + { + Assert.Contains("color-scheme: light;", CssTemplates.InputCss); + Assert.Contains("color-scheme: dark;", CssTemplates.InputCss); + } + + [Fact] + public void AppliedTheme_SetsColorSchemeForBothThemes() + { + var css = ThemeService.BuildThemeCss(new TweakcnTheme + { + Name = "t", + ThemeVars = new(), + LightVars = new() { ["background"] = "#fff" }, + DarkVars = new() { ["background"] = "#000" }, + }); + + Assert.Contains(":root {\n color-scheme: light;", css.Replace("\r\n", "\n")); + Assert.Contains(".dark {\n color-scheme: dark;", css.Replace("\r\n", "\n")); + } + + [Fact] + public void PackageTheme_SetsColorSchemeForBothThemes() + { + var root = new DirectoryInfo(System.AppContext.BaseDirectory); + while (root != null && !File.Exists(Path.Combine(root.FullName, "ShellUI.slnx"))) root = root.Parent; + var css = File.ReadAllText(Path.Combine(root!.FullName, "src", "ShellUI.Components", "wwwroot", "shellui-theme.css")); + + Assert.Contains("color-scheme: light;", css); + Assert.Contains("color-scheme: dark;", css); + } +} diff --git a/src/ShellUI.CLI/Services/ThemeService.cs b/src/ShellUI.CLI/Services/ThemeService.cs index 18ad88f..0a5ce80 100644 --- a/src/ShellUI.CLI/Services/ThemeService.cs +++ b/src/ShellUI.CLI/Services/ThemeService.cs @@ -102,8 +102,8 @@ public static string BuildThemeCss(TweakcnTheme theme) sb.AppendLine(BeginMarker); sb.AppendLine($"/* Theme: {theme.Name} — fetched from tweakcn */"); - WriteBlock(sb, ":root", theme.LightVars); - WriteBlock(sb, ".dark", theme.DarkVars); + WriteBlock(sb, ":root", "light", theme.LightVars); + WriteBlock(sb, ".dark", "dark", theme.DarkVars); if (theme.ThemeVars.Count > 0) { @@ -118,11 +118,13 @@ public static string BuildThemeCss(TweakcnTheme theme) return sb.ToString(); } - private static void WriteBlock(StringBuilder sb, string selector, Dictionary vars) + // color-scheme makes the browser's own controls (date popups, scrollbars, select lists) follow the theme. + private static void WriteBlock(StringBuilder sb, string selector, string colorScheme, Dictionary vars) { if (vars.Count == 0) return; sb.AppendLine(); sb.AppendLine($"{selector} {{"); + sb.AppendLine($" color-scheme: {colorScheme};"); foreach (var (k, v) in vars.OrderBy(kv => kv.Key, StringComparer.Ordinal)) sb.AppendLine($" --{k}: {v};"); sb.AppendLine("}"); diff --git a/src/ShellUI.Components/wwwroot/shellui-theme.css b/src/ShellUI.Components/wwwroot/shellui-theme.css index 39a8a34..bb94e87 100644 --- a/src/ShellUI.Components/wwwroot/shellui-theme.css +++ b/src/ShellUI.Components/wwwroot/shellui-theme.css @@ -1,4 +1,5 @@ :root { + color-scheme: light; --background: oklch(0.9900 0 0); --foreground: oklch(0 0 0); --card: oklch(1 0 0); @@ -54,6 +55,7 @@ } .dark { + color-scheme: dark; --background: oklch(0 0 0); --foreground: oklch(1 0 0); --card: oklch(0.1400 0 0); diff --git a/src/ShellUI.Templates/CssTemplates.cs b/src/ShellUI.Templates/CssTemplates.cs index 67b7e59..97a1e4c 100644 --- a/src/ShellUI.Templates/CssTemplates.cs +++ b/src/ShellUI.Templates/CssTemplates.cs @@ -35,6 +35,7 @@ public static class CssTemplates @custom-variant dark (&:is(.dark *)); :root { + color-scheme: light; --background: oklch(0.9900 0 0); --foreground: oklch(0 0 0); --card: oklch(1 0 0); @@ -90,6 +91,7 @@ public static class CssTemplates } .dark { + color-scheme: dark; --background: oklch(0 0 0); --foreground: oklch(1 0 0); --card: oklch(0.1400 0 0); From adeac5de827606688d19d48ce614c0bbea0af3dd Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Tue, 13 Oct 2026 10:19:12 +0200 Subject: [PATCH 3/5] fix: keep local changes and install new dependencies on update update rewrote every file it was given, including blocks apps are meant to edit, and never installed dependencies a component gained since it was installed, so projects stopped building (BreadcrumbList, TabItem). shellui.json now records a hash of each file the CLI writes; update skips files that changed unless --force is passed, and installs missing dependencies the way add does. Entries without a hash keep the editable blocks when they differ from the template. --- ShellUI.Tests/UpdateTests.cs | 87 +++++++++++++++++++ docs/ARCHITECTURE.md | 2 +- docs/CLI_SYNTAX.md | 10 ++- docs/FAQ.md | 4 +- docs/QUICKSTART.md | 2 +- src/ShellUI.CLI/Program.cs | 11 ++- .../Services/ComponentInstaller.cs | 35 ++++++-- src/ShellUI.CLI/Services/ComponentManager.cs | 79 +++++++++++++++-- src/ShellUI.Core/Models/ShellUIConfig.cs | 5 ++ 9 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 ShellUI.Tests/UpdateTests.cs diff --git a/ShellUI.Tests/UpdateTests.cs b/ShellUI.Tests/UpdateTests.cs new file mode 100644 index 0000000..e898a45 --- /dev/null +++ b/ShellUI.Tests/UpdateTests.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.IO; +using ShellUI.CLI.Services; +using ShellUI.Core.Models; +using ShellUI.Templates; +using Xunit; + +namespace ShellUI.Tests; + +public class UpdateDependencyTests +{ + [Fact] + public void MissingDependencies_FindsDependenciesAddedSinceInstall() + { + // A 0.3 install of tabs and breadcrumb, before tab-models and the breadcrumb parts existed. + var installed = new List { "tabs", "tabs-list", "tabs-trigger", "tabs-content", "breadcrumb", "breadcrumb-item" }; + + var missing = ComponentManager.MissingDependencies(new[] { "tabs", "breadcrumb" }, installed); + + Assert.Contains("tab-models", missing); + Assert.Contains("breadcrumb-list", missing); + Assert.Contains("breadcrumb-ellipsis", missing); + Assert.DoesNotContain("tabs-list", missing); + Assert.Equal(missing.Count, new HashSet(missing).Count); + } + + [Fact] + public void MissingDependencies_IsEmptyWhenEverythingIsInstalled() + { + var tabs = ComponentRegistry.GetMetadata("tabs")!; + var installed = new List(tabs.Dependencies) { "tabs" }; + + Assert.Empty(ComponentManager.MissingDependencies(new[] { "tabs" }, installed)); + } +} + +public class UpdateLocalChangesTests +{ + private const string Template = "
@ChildContent
\n"; + + private static InstalledComponent Entry(string name, string? hash) => new() { Name = name, Version = "0.4.0", Hash = hash }; + + [Fact] + public void FileMatchingTheRecordedHash_HasNoLocalChanges() + { + var entry = Entry("button", ComponentInstaller.ContentHash(Template)); + + Assert.False(ComponentManager.HasLocalChanges(entry, ComponentRegistry.GetMetadata("button")!, Template, Template)); + } + + [Fact] + public void EditedFile_HasLocalChanges() + { + var entry = Entry("button", ComponentInstaller.ContentHash(Template)); + + Assert.True(ComponentManager.HasLocalChanges(entry, ComponentRegistry.GetMetadata("button")!, Template + "", Template)); + } + + [Fact] + public void LineEndingChangesAreNotLocalChanges() + { + var entry = Entry("button", ComponentInstaller.ContentHash(Template)); + + Assert.False(ComponentManager.HasLocalChanges(entry, ComponentRegistry.GetMetadata("button")!, Template.Replace("\n", "\r\n"), Template)); + } + + [Theory] + [InlineData("app-sidebar")] + [InlineData("dashboard-02")] + [InlineData("auth-01")] + public void OlderInstall_EditableBlockThatDiffersFromTheTemplate_HasLocalChanges(string name) + { + Assert.True(ComponentManager.HasLocalChanges(Entry(name, null), ComponentRegistry.GetMetadata(name)!, "edited", Template)); + } + + [Fact] + public void OlderInstall_EditableBlockMatchingTheTemplate_HasNoLocalChanges() + { + Assert.False(ComponentManager.HasLocalChanges(Entry("app-sidebar", null), ComponentRegistry.GetMetadata("app-sidebar")!, Template, Template)); + } + + [Fact] + public void OlderInstall_OrdinaryComponentIsUpdatedAsBefore() + { + Assert.False(ComponentManager.HasLocalChanges(Entry("button", null), ComponentRegistry.GetMetadata("button")!, "edited", Template)); + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 66b0800..5ea66e5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -80,7 +80,7 @@ The implemented command tree is: | `shellui add ` | Install one or more direct targets and recursively install their registry dependencies. | | `shellui list` | List direct targets, with installed/available filtering. | | `shellui remove ` | Remove selected installed source files and update `shellui.json`. | -| `shellui update [components...]` | Rewrite selected installed source files, or all installed components with `--all`. | +| `shellui update [components...]` | Rewrite selected installed source files, or all installed components with `--all`; skips files changed since install unless `--force`, and installs new dependencies. | | `shellui theme init ` | Initialize a project and apply a fetched theme in one operation. | | `shellui theme apply ` | Apply a theme to `wwwroot/input.css` or emit a standalone override. | | `shellui theme update` | Re-fetch the theme recorded in `shellui.theme.lock`. | diff --git a/docs/CLI_SYNTAX.md b/docs/CLI_SYNTAX.md index 67a2e79..f2e2c19 100644 --- a/docs/CLI_SYNTAX.md +++ b/docs/CLI_SYNTAX.md @@ -20,7 +20,7 @@ shellui init [--force] [--style