From db15513eb07536c4ad0c5bf08d25e01316e9f2e1 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Fri, 4 Sep 2026 16:09:56 +0200 Subject: [PATCH 1/4] fix(templates): five template bugs surfaced against v0.3.0-rc.1 + regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugs from the shellui-integration-notes.md v0.3.0-rc.1 section: - `accordion-type` sub-dep declared by AccordionTemplate.Dependencies but never registered in ComponentRegistry: `shellui add accordion` reported `Failed: accordion-type` and consumers couldn't compile. - TabsTemplate emitted `_effectiveValue = ";` because a `""` inside a `@"..."` verbatim string was written as one `"` — should have been `""""`. - SelectTemplate rendered the raw text `expand_more` because it used a `material-symbols-outlined` span; ShellUI never links Material Symbols and every other component uses inline SVG. Swapped to an inline chevron. Two more of the same classes found by the new sweep tests: - FileUploadTemplate had the same Material Symbols dependency for `cloud_upload` — swapped to an inline upload-cloud SVG. - DashboardLayout01Template had the `segments[0] == ""` verbatim-quote bug in BuildBreadcrumb (already fixed in DashboardLayout02) — now `segments[0] == """"`. - ContextMenuTemplate referenced `ContextMenuItem` from `YourProjectNamespace.Components.Models` but no `context-menu-models` template existed and the parent didn't declare it as a dep — same shape as the `data-table-models` bug. Added ContextMenuModelsTemplate, registered it, and declared it as ContextMenu's dependency. Regression tests (added, all green — 86/86 pass): - TemplateCompileTests.EveryRazorTemplate_CodeBlockParses walks every installable Razor template and parses its `@code` block, catching the whole verbatim-string bug class in one shot. - DependencyGraphTests.EveryDeclaredDependency_ResolvesInRegistry walks every component's Dependencies and asserts each name resolves via GetMetadata — the accordion-type and context-menu-models class of bug. - DependencyGraphTests.EveryInstallableComponent_HasContentMapping asserts every `IsAvailable = true` component routes through GetComponentContent (intentional empty sub-component stubs are exempted). - HiddenNetworkDependencyTests.NoTemplate_DependsOnMaterialSymbols asserts no template body references `material-symbols-outlined`. Co-Authored-By: Claude Opus 4.7 --- ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs | 66 +++++++++++++++++++ ShellUI.Tests/TemplateCompileTests.cs | 40 +++++++++++ src/ShellUI.Templates/ComponentRegistry.cs | 4 ++ .../Templates/ContextMenuModelsTemplate.cs | 30 +++++++++ .../Templates/ContextMenuTemplate.cs | 2 +- .../Templates/DashboardLayout01Template.cs | 2 +- .../Templates/FileUploadTemplate.cs | 8 ++- .../Templates/SelectTemplate.cs | 5 +- .../Templates/TabsTemplate.cs | 2 +- 9 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 src/ShellUI.Templates/Templates/ContextMenuModelsTemplate.cs diff --git a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs index e0833d3..d5c47ab 100644 --- a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs +++ b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs @@ -84,6 +84,72 @@ public void ChartVariants_TransitivelyPullInBlazorApexCharts(string componentNam } } +public class DependencyGraphTests +{ + // Every dependency name declared by any registered component must itself resolve + // via ComponentRegistry.GetMetadata. Catches the class of bug where a sub-component + // template exists on disk but is never wired into the registry — the CLI then + // reports "Failed: " and the consumer project won't compile. + [Fact] + public void EveryDeclaredDependency_ResolvesInRegistry() + { + var missing = new List(); + foreach (var (name, metadata) in ComponentRegistry.Components) + { + foreach (var dep in metadata.Dependencies) + { + if (ComponentRegistry.GetMetadata(dep) is null) + missing.Add($"{name} -> {dep}"); + } + } + Assert.True(missing.Count == 0, + "The following declared dependencies are not registered in ComponentRegistry:\n " + + string.Join("\n ", missing)); + } + + // Every installable component (IsAvailable = true) must route through the + // GetComponentContent switch. Sub-component stubs registered as IsAvailable = false + // with intentionally-empty Content are allowed — they exist only so the CLI can + // resolve them as dependency names when the parent renders their markup inline. + [Fact] + public void EveryInstallableComponent_HasContentMapping() + { + var missing = new List(); + foreach (var (name, metadata) in ComponentRegistry.Components) + { + if (!metadata.IsAvailable) continue; + var content = ComponentRegistry.GetComponentContent(name); + if (string.IsNullOrWhiteSpace(content)) + missing.Add(name); + } + Assert.True(missing.Count == 0, + "The following installable components have no content mapping:\n " + + string.Join("\n ", missing)); + } +} + +public class HiddenNetworkDependencyTests +{ + // ShellUI never ships a to Google Material Symbols and every other component + // uses inline SVG for its chrome. If a template renders `material-symbols-outlined`, + // the raw icon name ("expand_more", "check", ...) shows as literal text on + // consumers. Fail loudly here so the SVG-swap convention stays enforced. + [Fact] + public void NoTemplate_DependsOnMaterialSymbols() + { + var offenders = new List(); + foreach (var (name, _) in ComponentRegistry.Components) + { + var content = ComponentRegistry.GetComponentContent(name); + if (content is not null && content.Contains("material-symbols-outlined")) + offenders.Add(name); + } + Assert.True(offenders.Count == 0, + "The following templates reference Material Symbols (not shipped by ShellUI):\n " + + string.Join("\n ", offenders)); + } +} + public class DataTableTemplateContentTests { // The library-wide convention is `Components.Models` for model namespaces regardless diff --git a/ShellUI.Tests/TemplateCompileTests.cs b/ShellUI.Tests/TemplateCompileTests.cs index 80338f3..1587d23 100644 --- a/ShellUI.Tests/TemplateCompileTests.cs +++ b/ShellUI.Tests/TemplateCompileTests.cs @@ -38,6 +38,8 @@ public void CsharpTemplate_GeneratedContentParses(string componentName) [InlineData("dashboard-02")] [InlineData("button")] [InlineData("dialog")] + [InlineData("tabs")] + [InlineData("select")] public void RazorTemplate_CodeBlockParses(string componentName) { var content = ComponentRegistry.GetComponentContent(componentName); @@ -76,6 +78,44 @@ public void RazorTemplate_CodeBlockParses(string componentName) string.Join("\n", errors.Select(e => $" {e.Location.GetLineSpan().StartLinePosition}: {e.GetMessage()}"))); } + /// Exhaustive sweep: parse the @code block of every installable .razor template + /// in the registry. This is the safety net for the class of bugs where a template + /// ships with an unescaped quote inside its verbatim string (Tabs, PieChart, etc.). + /// Kept separate from the targeted Theory above so a regression's failure point + /// lists just the offending component(s) rather than aborting on the first one. + [Fact] + public void EveryRazorTemplate_CodeBlockParses() + { + var failures = new List(); + foreach (var (name, metadata) in ComponentRegistry.Components) + { + if (!metadata.IsAvailable) continue; + if (!metadata.FilePath.EndsWith(".razor", System.StringComparison.OrdinalIgnoreCase)) continue; + + var content = ComponentRegistry.GetComponentContent(name); + if (string.IsNullOrWhiteSpace(content)) continue; + if (!content.Contains("@code")) continue; + + var codeBlock = ExtractCodeBlock(content!); + if (string.IsNullOrWhiteSpace(codeBlock)) + { + failures.Add($"{name}: @code block could not be extracted (likely unterminated string literal)"); + continue; + } + + var wrapped = $"class __Probe {{ {codeBlock} }}"; + var errors = CSharpSyntaxTree.ParseText(wrapped).GetDiagnostics() + .Where(d => d.Severity == DiagnosticSeverity.Error) + .Select(d => $"{d.Location.GetLineSpan().StartLinePosition}: {d.GetMessage()}") + .ToList(); + if (errors.Count > 0) + failures.Add($"{name}: {errors.Count} parse error(s) — {errors[0]}"); + } + Assert.True(failures.Count == 0, + "The following templates have parse errors in their @code block:\n " + + string.Join("\n ", failures)); + } + /// Strips Razor markup directives so the remaining text can be best-effort /// parsed as C#. Not a real Razor parser — just enough to surface useful /// diagnostics when ExtractCodeBlock fails. diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index b156164..6fad73d 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -81,6 +81,7 @@ public static class ComponentRegistry { "slider", SliderTemplate.Metadata }, { "toggle", ToggleTemplate.Metadata }, { "accordion", AccordionTemplate.Metadata }, + { "accordion-type", AccordionTypeTemplate.Metadata }, { "accordion-item", AccordionItemTemplate.Metadata }, { "accordion-trigger", AccordionTriggerTemplate.Metadata }, { "accordion-content", AccordionContentTemplate.Metadata }, @@ -150,6 +151,7 @@ public static class ComponentRegistry { "context-menu-trigger", ContextMenuTriggerTemplate.Metadata }, { "context-menu-content", ContextMenuContentTemplate.Metadata }, { "context-menu-option", ContextMenuOptionTemplate.Metadata }, + { "context-menu-models", ContextMenuModelsTemplate.Metadata }, { "empty-state", EmptyStateTemplate.Metadata }, { "stepper", StepperTemplate.Metadata }, { "stepper-list", StepperListTemplate.Metadata }, @@ -258,6 +260,7 @@ public static class ComponentRegistry "slider" => SliderTemplate.Content, "toggle" => ToggleTemplate.Content, "accordion" => AccordionTemplate.Content, + "accordion-type" => AccordionTypeTemplate.Content, "accordion-item" => AccordionItemTemplate.Content, "accordion-trigger" => AccordionTriggerTemplate.Content, "accordion-content" => AccordionContentTemplate.Content, @@ -327,6 +330,7 @@ public static class ComponentRegistry "context-menu-trigger" => ContextMenuTriggerTemplate.Content, "context-menu-content" => ContextMenuContentTemplate.Content, "context-menu-option" => ContextMenuOptionTemplate.Content, + "context-menu-models" => ContextMenuModelsTemplate.Content, "empty-state" => EmptyStateTemplate.Content, "stepper" => StepperTemplate.Content, "stepper-list" => StepperListTemplate.Content, diff --git a/src/ShellUI.Templates/Templates/ContextMenuModelsTemplate.cs b/src/ShellUI.Templates/Templates/ContextMenuModelsTemplate.cs new file mode 100644 index 0000000..b6ddd61 --- /dev/null +++ b/src/ShellUI.Templates/Templates/ContextMenuModelsTemplate.cs @@ -0,0 +1,30 @@ +using ShellUI.Core.Models; + +namespace ShellUI.Templates.Templates; + +public static class ContextMenuModelsTemplate +{ + public static ComponentMetadata Metadata => new() + { + Name = "context-menu-models", + DisplayName = "ContextMenu Models", + Description = "Models for ContextMenu component", + Category = ComponentCategory.Navigation, + FilePath = "Models/ContextMenuModels.cs", + IsAvailable = false, + Dependencies = new List() + }; + + public static string Content => @"namespace YourProjectNamespace.Components.Models; + +public class ContextMenuItem +{ + public string Label { get; set; } = string.Empty; + public string? Icon { get; set; } + public string? Shortcut { get; set; } + public bool Disabled { get; set; } + public bool IsSeparator { get; set; } + public object? Data { get; set; } +} +"; +} diff --git a/src/ShellUI.Templates/Templates/ContextMenuTemplate.cs b/src/ShellUI.Templates/Templates/ContextMenuTemplate.cs index e9f3905..f6756d7 100644 --- a/src/ShellUI.Templates/Templates/ContextMenuTemplate.cs +++ b/src/ShellUI.Templates/Templates/ContextMenuTemplate.cs @@ -12,7 +12,7 @@ public static class ContextMenuTemplate Category = ComponentCategory.Navigation, FilePath = "ContextMenu.razor", Tags = new List { "menu", "context", "right-click", "dropdown" }, - Dependencies = new List() + Dependencies = new List { "context-menu-models" } }; public const string Content = """ diff --git a/src/ShellUI.Templates/Templates/DashboardLayout01Template.cs b/src/ShellUI.Templates/Templates/DashboardLayout01Template.cs index 3f12033..43631fb 100644 --- a/src/ShellUI.Templates/Templates/DashboardLayout01Template.cs +++ b/src/ShellUI.Templates/Templates/DashboardLayout01Template.cs @@ -83,7 +83,7 @@ private void BuildBreadcrumb() var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); _breadcrumbItems.Clear(); - if (segments.Length == 0 || (segments.Length == 1 && segments[0] == "")) + if (segments.Length == 0 || (segments.Length == 1 && segments[0] == """")) { _breadcrumbItems.Add(new BreadcrumbItemData { Label = ""Home"", Href = ""/"", IsLast = true }); return; diff --git a/src/ShellUI.Templates/Templates/FileUploadTemplate.cs b/src/ShellUI.Templates/Templates/FileUploadTemplate.cs index 814ae81..5d0c865 100644 --- a/src/ShellUI.Templates/Templates/FileUploadTemplate.cs +++ b/src/ShellUI.Templates/Templates/FileUploadTemplate.cs @@ -28,7 +28,13 @@ @using Microsoft.JSInterop @ondragover:preventDefault=""true"" @ondrop:preventDefault=""true"">
- cloud_upload + + + +

@Text

@if (Multiple && AllowedFileCount > 1) diff --git a/src/ShellUI.Templates/Templates/SelectTemplate.cs b/src/ShellUI.Templates/Templates/SelectTemplate.cs index 0935e3f..4841ae7 100644 --- a/src/ShellUI.Templates/Templates/SelectTemplate.cs +++ b/src/ShellUI.Templates/Templates/SelectTemplate.cs @@ -26,7 +26,10 @@ public static class SelectTemplate @attributes=""AdditionalAttributes""> @ChildContent - expand_more + + +
@code { diff --git a/src/ShellUI.Templates/Templates/TabsTemplate.cs b/src/ShellUI.Templates/Templates/TabsTemplate.cs index 87c3b45..5c3ba50 100644 --- a/src/ShellUI.Templates/Templates/TabsTemplate.cs +++ b/src/ShellUI.Templates/Templates/TabsTemplate.cs @@ -32,7 +32,7 @@ public static class TabsTemplate [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } - private string _effectiveValue = ""; + private string _effectiveValue = """"; protected override void OnInitialized() { From 3a4d29979103098f2f45e41fe7be93a1c4bd7579 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Fri, 4 Sep 2026 16:11:22 +0200 Subject: [PATCH 2/4] test: widen icon-font guard to catch Material Icons and Font Awesome too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Material Symbols bug (bug #3 in the integration notes) is one instance of a broader class: templates that lean on an externally-loaded icon font ShellUI never links. Widen NoTemplate_DependsOnExternalIconFont into a Theory that also fails on `material-icons` (the older Google family), the `-rounded`/`-sharp` Material Symbols variants, and Font Awesome's `fa-solid`/`fa-regular`/`fa-brands` — the same footgun with a different filename. Co-Authored-By: Claude Opus 4.7 --- ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs index d5c47ab..a99bde6 100644 --- a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs +++ b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs @@ -130,22 +130,32 @@ public void EveryInstallableComponent_HasContentMapping() public class HiddenNetworkDependencyTests { - // ShellUI never ships a to Google Material Symbols and every other component - // uses inline SVG for its chrome. If a template renders `material-symbols-outlined`, - // the raw icon name ("expand_more", "check", ...) shows as literal text on - // consumers. Fail loudly here so the SVG-swap convention stays enforced. - [Fact] - public void NoTemplate_DependsOnMaterialSymbols() + // ShellUI never links Google Material Symbols/Icons or Font Awesome and every + // component uses inline SVG for its chrome. When a template depends on an + // external icon font the raw icon name ("expand_more", "cloud_upload", ...) + // shows as literal text or the glyph slot renders empty on consumers. + // Fail loudly here so the SVG-swap convention stays enforced across the whole + // registry — the specific classes below cover both Material families and + // Font Awesome's solid/regular/brands. + [Theory] + [InlineData("material-symbols-outlined")] // Material Symbols (newer variable font) + [InlineData("material-symbols-rounded")] + [InlineData("material-symbols-sharp")] + [InlineData("material-icons")] // Material Icons (older static font) + [InlineData("fa-solid")] // Font Awesome v6+ + [InlineData("fa-regular")] + [InlineData("fa-brands")] + public void NoTemplate_DependsOnExternalIconFont(string cssClass) { var offenders = new List(); foreach (var (name, _) in ComponentRegistry.Components) { var content = ComponentRegistry.GetComponentContent(name); - if (content is not null && content.Contains("material-symbols-outlined")) + if (content is not null && content.Contains(cssClass)) offenders.Add(name); } Assert.True(offenders.Count == 0, - "The following templates reference Material Symbols (not shipped by ShellUI):\n " + + $"The following templates reference `{cssClass}` (external icon font, not shipped by ShellUI):\n " + string.Join("\n ", offenders)); } } From 4120b77b7e2deac48dd02690ad7bd45aed52412f Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 23 Sep 2026 13:06:41 +0200 Subject: [PATCH 3/4] fix: adjust SidebarInset component to ensure minimum width is enforced Updated the SidebarInset component to include a minimum width constraint, enhancing layout stability and preventing overflow issues in responsive designs. --- src/ShellUI.Components/Components/SidebarInset.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellUI.Components/Components/SidebarInset.razor b/src/ShellUI.Components/Components/SidebarInset.razor index 6a0f863..5a9b622 100644 --- a/src/ShellUI.Components/Components/SidebarInset.razor +++ b/src/ShellUI.Components/Components/SidebarInset.razor @@ -1,7 +1,7 @@ @namespace ShellUI.Components
From 7fcf4298106ae2f0eb673a262c122159459b3dc5 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 23 Sep 2026 13:06:53 +0200 Subject: [PATCH 4/4] fix: enforce minimum width in SidebarInsetTemplate for improved layout stability Updated the SidebarInsetTemplate to include a minimum width constraint, ensuring better responsiveness and preventing layout overflow issues. --- src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs b/src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs index 95835e0..480a64f 100644 --- a/src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs +++ b/src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs @@ -17,7 +17,7 @@ public static class SidebarInsetTemplate public static string Content => @"@namespace YourProjectNamespace.Components.UI