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" }