From be8dcefaa04a9b0601697acf7b53fe41eff5747f Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 11:21:45 +0200 Subject: [PATCH 1/6] chore(ci): add assertions for chart-styles CSS and linking in App.razor Enhanced the CI workflow to verify the presence of chart-styles CSS and ensure it is correctly linked in App.razor. This prevents issues with invisible text on chart hover and improves the robustness of the installation process. --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd34e98..6e9d74c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,11 @@ jobs: # And the DataTable models file must land at Components/UI/Models/, not be missing. test -f Components/UI/Models/DataTableModels.cs || (echo "shellui add data-table did not install data-table-models"; exit 1) + # chart-styles ships the CSS for the custom tooltip + ApexCharts chrome. + # Without it, hovering a chart shows invisible white-on-white text. + test -f wwwroot/css/charts.css || (echo "shellui add chart did not install chart-styles CSS"; exit 1) + grep -q ' Date: Thu, 18 Jun 2026 11:22:11 +0200 Subject: [PATCH 2/6] fix(templates): update ChartStylesTemplate file path and add chart-styles dependency Adjusted the file path in ChartStylesTemplate to correctly reference the CSS file location. Additionally, updated the ChartTemplate to include "chart-styles" in its dependencies, ensuring proper integration of chart styles within the ShellUI theme system. --- src/ShellUI.Templates/Templates/ChartStylesTemplate.cs | 4 +++- src/ShellUI.Templates/Templates/ChartTemplate.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ShellUI.Templates/Templates/ChartStylesTemplate.cs b/src/ShellUI.Templates/Templates/ChartStylesTemplate.cs index 5f054c7..abf387f 100644 --- a/src/ShellUI.Templates/Templates/ChartStylesTemplate.cs +++ b/src/ShellUI.Templates/Templates/ChartStylesTemplate.cs @@ -10,7 +10,9 @@ public class ChartStylesTemplate DisplayName = "Chart Styles", Description = "CSS styles for ApexCharts integration with ShellUI theme system", Category = ComponentCategory.DataDisplay, - FilePath = "wwwroot/css/charts.css", + // FilePath walks up out of Components/UI/ to project root then into wwwroot/css/, + // matching the trick shellui-js uses for its own asset path. + FilePath = "../../wwwroot/css/charts.css", IsAvailable = false, Dependencies = new List(), Variants = new List(), diff --git a/src/ShellUI.Templates/Templates/ChartTemplate.cs b/src/ShellUI.Templates/Templates/ChartTemplate.cs index 478e9e8..14b57b5 100644 --- a/src/ShellUI.Templates/Templates/ChartTemplate.cs +++ b/src/ShellUI.Templates/Templates/ChartTemplate.cs @@ -11,7 +11,7 @@ public class ChartTemplate Description = "Base chart component with ShellUI theming and ApexCharts integration", Category = ComponentCategory.DataDisplay, FilePath = "Chart.razor", - Dependencies = new List { "chart-variants" }, + Dependencies = new List { "chart-variants", "chart-styles" }, NuGetDependencies = new List { new() { PackageId = "Blazor-ApexCharts", Version = "6.0.2" } From ee5c33d1314309d0a2efd4da21bd3f247028ca60 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 11:22:24 +0200 Subject: [PATCH 3/6] feat(tests): add unit tests for ChartStyles and StylesheetInjection functionality Introduced comprehensive unit tests for the ChartStylesRegistry and StylesheetInjection classes. The tests validate the registration and dependencies of chart styles, ensure correct file path targeting, and verify the functionality of stylesheet injection methods, enhancing overall test coverage and reliability of the ShellUI component system. --- ShellUI.Tests/ChartStylesTests.cs | 118 ++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 ShellUI.Tests/ChartStylesTests.cs diff --git a/ShellUI.Tests/ChartStylesTests.cs b/ShellUI.Tests/ChartStylesTests.cs new file mode 100644 index 0000000..8acb5e6 --- /dev/null +++ b/ShellUI.Tests/ChartStylesTests.cs @@ -0,0 +1,118 @@ +using ShellUI.CLI.Services; +using ShellUI.Templates; +using Xunit; + +namespace ShellUI.Tests; + +public class ChartStylesRegistryTests +{ + [Fact] + public void ChartStyles_IsRegisteredAndHidden() + { + var metadata = ComponentRegistry.GetMetadata("chart-styles"); + Assert.NotNull(metadata); + Assert.False(metadata!.IsAvailable, "chart-styles is a CSS asset bundled with chart, not a standalone install target"); + } + + [Fact] + public void Chart_DependsOnChartStyles() + { + var metadata = ComponentRegistry.GetMetadata("chart"); + Assert.NotNull(metadata); + Assert.Contains("chart-styles", metadata!.Dependencies); + } + + [Fact] + public void ChartStyles_FilePathTargetsWwwroot() + { + var metadata = ComponentRegistry.GetMetadata("chart-styles"); + Assert.NotNull(metadata); + // The `../../wwwroot/` prefix walks out of Components/UI/ to project root, + // matching the shellui-js convention. Without it the file lands inside the + // component tree and is never served. + Assert.StartsWith("../../wwwroot/", metadata!.FilePath); + Assert.EndsWith(".css", metadata.FilePath); + } + + [Fact] + public void ChartStyles_ContentCoversCustomTooltipAndApexClasses() + { + var content = ComponentRegistry.GetComponentContent("chart-styles"); + Assert.NotNull(content); + // Custom tooltip emitted by ChartVariants' Custom HTML — these were invisible + // before this branch because the CSS template existed but was never installed. + Assert.Contains(".custom-tooltip", content); + Assert.Contains(".custom-tooltip-title", content); + Assert.Contains(".custom-tooltip-item", content); + Assert.Contains(".custom-tooltip-marker", content); + Assert.Contains(".custom-tooltip-label", content); + Assert.Contains(".custom-tooltip-value", content); + // ApexCharts built-in classes also styled so charts without the custom HTML still look right. + Assert.Contains(".apexcharts-tooltip", content); + Assert.Contains(".apexcharts-legend", content); + Assert.Contains(".apexcharts-xaxis-label", content); + // Theme-aware: values come from the CSS variables the init script ships. + Assert.Contains("var(--popover", content); + Assert.Contains("var(--foreground", content); + Assert.Contains("var(--border", content); + } +} + +public class StylesheetInjectionTests +{ + [Fact] + public void ResolveHostStylesheetHref_StripsWwwrootPrefix() + { + Assert.Equal("css/charts.css", ComponentInstaller.ResolveHostStylesheetHref("../../wwwroot/css/charts.css")); + } + + [Fact] + public void ResolveHostStylesheetHref_IgnoresNonWwwrootFilePaths() + { + Assert.Null(ComponentInstaller.ResolveHostStylesheetHref("Button.razor")); + Assert.Null(ComponentInstaller.ResolveHostStylesheetHref("Variants/ButtonVariants.cs")); + } + + [Fact] + public void ResolveHostStylesheetHref_IgnoresNonCssAssets() + { + // shellui.js also uses ../../wwwroot/ but is a script, not a stylesheet. + Assert.Null(ComponentInstaller.ResolveHostStylesheetHref("../../wwwroot/shellui.js")); + } + + [Fact] + public void InjectStylesheetLink_AddsLinkBeforeHeadClose() + { + const string app = ""; + + var result = InitService.InjectStylesheetLink(app, "css/charts.css"); + + Assert.Contains("", result); + var linkIdx = result.IndexOf(""); + Assert.True(linkIdx > 0 && linkIdx < headCloseIdx, "link tag must sit inside "); + } + + [Fact] + public void InjectStylesheetLink_IsIdempotent() + { + const string app = ""; + + var once = InitService.InjectStylesheetLink(app, "css/charts.css"); + var twice = InitService.InjectStylesheetLink(once, "css/charts.css"); + + Assert.Equal(once, twice); + } + + [Fact] + public void InjectStylesheetLink_SupportsMultipleDifferentHrefs() + { + const string app = ""; + + var result = InitService.InjectStylesheetLink(app, "css/charts.css"); + result = InitService.InjectStylesheetLink(result, "css/another.css"); + + Assert.Contains("css/charts.css", result); + Assert.Contains("css/another.css", result); + } +} From 4de9e49fd08272e41fdd744b701500240448441f Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 11:22:41 +0200 Subject: [PATCH 4/6] feat(cli): inject stylesheets into host for installed components Enhanced the ComponentInstaller to automatically wire installed stylesheets from wwwroot into the host, eliminating the need for manual tag additions. Introduced a new method, ResolveHostStylesheetHref, to determine the correct href for stylesheets, streamlining the integration of CSS assets into the ShellUI component system. --- .../Services/ComponentInstaller.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/ShellUI.CLI/Services/ComponentInstaller.cs b/src/ShellUI.CLI/Services/ComponentInstaller.cs index 3a799a9..b82ee64 100644 --- a/src/ShellUI.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.CLI/Services/ComponentInstaller.cs @@ -77,6 +77,20 @@ await AnsiConsole.Status() // (so `dotnet add package` doesn't restore between every component). await InstallNuGetDependenciesAsync(projectInfo, pendingNuGetDeps); + // Wire any installed wwwroot/ stylesheets into the host so the user doesn't + // have to add tags by hand. Detected via FilePath, which uses the + // `../../wwwroot/` traversal trick that asset templates already follow. + foreach (var name in installedSet) + { + var metadata = ComponentRegistry.GetMetadata(name); + if (metadata == null) continue; + var href = ResolveHostStylesheetHref(metadata.FilePath); + if (href != null) + { + await InitService.InjectStylesheetIntoHostAsync(href); + } + } + // Update config var updatedJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { @@ -327,6 +341,18 @@ private static async Task InstallNuGetDependenciesAsync(ProjectInfo projectInfo, } } + // Returns the href that should appear in the host's tag, or null if the + // component's FilePath isn't a CSS asset under wwwroot/. Strips the `../../wwwroot/` + // prefix that asset templates use to escape Components/UI/. + internal static string? ResolveHostStylesheetHref(string filePath) + { + if (!filePath.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) return null; + var normalized = filePath.Replace('\\', '/'); + const string prefix = "../../wwwroot/"; + if (!normalized.StartsWith(prefix, StringComparison.Ordinal)) return null; + return normalized.Substring(prefix.Length); + } + private enum InstallResult { Success, From 84b064edbbff151eda925ae1f6b7a399db409e82 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 11:25:19 +0200 Subject: [PATCH 5/6] feat(templates): add chart-styles component to ComponentRegistry Updated the ComponentRegistry to include the "chart-styles" component, enhancing the template system by ensuring it is registered and its content is accessible. This addition supports improved styling options for charts within the ShellUI framework. --- src/ShellUI.Templates/ComponentRegistry.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index 56238e0..a1eb868 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -160,6 +160,7 @@ public static class ComponentRegistry { "carousel-dots", CarouselDotsTemplate.Metadata }, { "file-upload", FileUploadTemplate.Metadata }, { "chart-variants", ChartVariantsTemplate.Metadata }, + { "chart-styles", ChartStylesTemplate.Metadata }, { "chart", ChartTemplate.Metadata }, { "bar-chart", BarChartTemplate.Metadata }, { "line-chart", LineChartTemplate.Metadata }, @@ -331,6 +332,7 @@ public static class ComponentRegistry "carousel-dots" => CarouselDotsTemplate.Content, "file-upload" => FileUploadTemplate.Content, "chart-variants" => ChartVariantsTemplate.Content, + "chart-styles" => ChartStylesTemplate.Content, "chart" => ChartTemplate.Content, "bar-chart" => BarChartTemplate.Content, "line-chart" => LineChartTemplate.Content, From 933bc0682eafdf0a5ca2c1d889cd92589b322982 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 11:25:36 +0200 Subject: [PATCH 6/6] feat(cli): implement stylesheet injection for installed components Added methods to inject stylesheet links into the host files (App.razor and index.html) for newly installed components, streamlining the integration of CSS assets. This enhancement automates the process of linking stylesheets, improving user experience by eliminating the need for manual edits after installation. --- src/ShellUI.CLI/Services/InitService.cs | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/ShellUI.CLI/Services/InitService.cs b/src/ShellUI.CLI/Services/InitService.cs index 8cd1906..f6c6d90 100644 --- a/src/ShellUI.CLI/Services/InitService.cs +++ b/src/ShellUI.CLI/Services/InitService.cs @@ -540,6 +540,50 @@ internal static string RewriteWasmIndexHtml(string content) return content; } + // Idempotent injection of a stylesheet before . Used by post-install + // hooks when a component ships a CSS asset (e.g. chart-styles → css/charts.css). + internal static string InjectStylesheetLink(string content, string href) + { + var linkTag = $""; + if (content.Contains($"href=\"{href}\"")) + { + return content; + } + return Regex.Replace(content, @"", $" {linkTag}\n", RegexOptions.IgnoreCase); + } + + // Used by ComponentInstaller after `shellui add` so newly-installed CSS assets + // are wired into the host without requiring the user to edit App.razor by hand. + internal static async Task InjectStylesheetIntoHostAsync(string href) + { + var cwd = Directory.GetCurrentDirectory(); + + var appRazor = Path.Combine(cwd, "Components", "App.razor"); + if (File.Exists(appRazor)) + { + var original = await File.ReadAllTextAsync(appRazor); + var patched = InjectStylesheetLink(original, href); + if (patched != original) + { + await File.WriteAllTextAsync(appRazor, patched); + AnsiConsole.MarkupLine($"[green]Linked:[/] Components/App.razor → {href}"); + } + return; + } + + var indexHtml = Path.Combine(cwd, "wwwroot", "index.html"); + if (File.Exists(indexHtml)) + { + var original = await File.ReadAllTextAsync(indexHtml); + var patched = InjectStylesheetLink(original, href); + if (patched != original) + { + await File.WriteAllTextAsync(indexHtml, patched); + AnsiConsole.MarkupLine($"[green]Linked:[/] wwwroot/index.html → {href}"); + } + } + } + private static void RemoveBootstrapFiles() { try