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 '", 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); + } +} 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, 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 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, 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" }