diff --git a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs index e0833d3..a99bde6 100644 --- a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs +++ b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs @@ -84,6 +84,82 @@ 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 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(cssClass)) + offenders.Add(name); + } + Assert.True(offenders.Count == 0, + $"The following templates reference `{cssClass}` (external icon font, 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.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
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/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
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() {