diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 670628b..09cce67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | + 8.0.x 10.0.x - name: Restore dependencies @@ -221,6 +222,20 @@ jobs: grep -Fq 'state=open' "$BUNDLE" || (echo "ERROR: bundle missing data-[state=open] arbitrary-value modifier"; exit 1) grep -Fq 'px-2\.5' "$BUNDLE" || (echo "ERROR: bundle missing Badge padding — .cs variant scan regression?"; exit 1) + # The package also ships net8.0 and net9.0 builds. + for fw in net8.0 net9.0; do + cd "$TMPDIR/app" + dotnet new blazor -o "NuGetSmoke-$fw" -f "$fw" --no-restore + cd "NuGetSmoke-$fw" + cp ../NuGetSmoke/NuGet.config . + dotnet add package ShellUI.Components --prerelease + echo '@using ShellUI.Components' >> Components/_Imports.razor + echo '@page "/shellui"' > Components/Pages/ShellUISmoke.razor + echo ' ok' >> Components/Pages/ShellUISmoke.razor + dotnet build -c Debug + test -f wwwroot/shellui-classes.txt + done + - name: Upload build artifacts uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3e9d1a..b332542 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.x' + dotnet-version: | + 8.0.x + 10.0.x - name: Check package version matches tag run: | diff --git a/README.md b/README.md index 9ec8fce..8c03f85 100644 --- a/README.md +++ b/README.md @@ -24,18 +24,18 @@ | Channel | Version | Notes | |---|---|---| -| Latest stable (recommended) | `0.3.4` | Tailwind CSS `4.3.2` | +| Latest stable (recommended) | `0.3.5` | Tailwind CSS `4.3.2` | | Prerelease | `0.4.0-alpha.3` | This branch; install with `--version 0.4.0-alpha.3` | -The CLI needs the .NET 10 SDK and works in Blazor projects on .NET 8, 9 and 10. The `ShellUI.Components` NuGet package targets .NET 10 only; on .NET 8 or 9, use the CLI. +The CLI runs on .NET 8 or later and works in Blazor projects on .NET 8, 9 and 10. The `ShellUI.Components` NuGet package targets .NET 8, 9 and 10. (`0.4.0-alpha.3` and the 0.3 line need .NET 10 for both.) -`main` is the 0.4 line, so this README and `docs/` describe `0.4.0-alpha.3`, including features that are not in a stable release yet, such as `shellui init --dashboard` and the auth blocks. For the stable version, read the [v0.3.4 docs](https://github.com/shellui-dev/shellui/tree/v0.3.4). Prereleases need `--version` to install. +`main` is the 0.4 line, so this README and `docs/` describe `0.4.0-alpha.3`, including features that are not in a stable release yet, such as `shellui init --dashboard` and the auth blocks. For the stable version, read the [v0.3.5 docs](https://github.com/shellui-dev/shellui/tree/v0.3.5). Prereleases need `--version` to install. ShellUI is pre-1.0, so APIs and generated output can still change between minor versions. Validate it in your target Blazor and hosting environments before relying on it. ## Capabilities -- The CLI commands are `init`, `add`, `list`, `remove`, and `update`, plus `theme init`, `theme apply`, and `theme update`. +- The CLI commands are `init`, `add`, `list`, `remove`, `update` and `doctor`, plus `theme init`, `theme apply`, and `theme update`. - The component registry has **243 entries**: **93 direct install targets** and **150 hidden dependency entries**. `list` shows direct targets; `add` resolves hidden dependencies. - `0.4.0-alpha.1` adds `kbd`, `aspect-ratio`, `button-group`, `toggle-group`, `input-group`, `number-input`, `stat-card`, `timeline`, `tree-view`, `qr-code`, `image-viewer`, `chat`, `chat-message`, `chat-input`, and the `auth-01`, `auth-02` and `auth-03` blocks. - `0.3.0` added `typed-select`, `command-palette`, `data-picker`, `multi-select`, `tag-input`, `donut-chart`, `radar-chart`, and `radial-chart`. @@ -45,8 +45,8 @@ ShellUI is pre-1.0, so APIs and generated output can still change between minor ## Requirements -- The .NET 10 SDK -- A Blazor project on .NET 8, 9 or 10 with the CLI, or on .NET 10 with the NuGet package +- The .NET 8 SDK or later +- A Blazor project on .NET 8, 9 or 10, with the CLI or the NuGet package - Tailwind CSS `4.3.2` via either: - the standalone CLI, which does not require Node.js; or - npm, which requires Node.js and npm diff --git a/ShellUI.Tests/AddAllTests.cs b/ShellUI.Tests/AddAllTests.cs new file mode 100644 index 0000000..55e5a50 --- /dev/null +++ b/ShellUI.Tests/AddAllTests.cs @@ -0,0 +1,33 @@ +using ShellUI.CLI.Services; +using ShellUI.Templates; + +namespace ShellUI.Tests; + +public class AddAllTests +{ + [Fact] + public void LibraryComponents_IsEveryListedTargetExceptLayoutBlocks() + { + var all = ComponentInstaller.LibraryComponents().ToList(); + var listed = ComponentRegistry.Components.Values.Where(m => m.IsAvailable).ToList(); + + Assert.Equal(listed.Count(m => !m.IsLayoutBlock), all.Count); + Assert.Contains("button", all); + Assert.Contains("chart", all); + Assert.DoesNotContain("dashboard-02", all); + Assert.DoesNotContain("auth-01", all); + Assert.DoesNotContain("sidebar-account", all); + Assert.DoesNotContain("tab-models", all); + } + + [Theory] + [InlineData("bar-chart", "chart", true)] + [InlineData("multi-series-chart", "chart", true)] + [InlineData("button", "chart", false)] + [InlineData("dialog", "button", false)] + public void DependsOnAny_FollowsDependenciesTransitively(string name, string excluded, bool expected) + { + var names = new HashSet(StringComparer.OrdinalIgnoreCase) { excluded }; + Assert.Equal(expected, ComponentInstaller.DependsOnAny(name, names)); + } +} diff --git a/ShellUI.Tests/DoctorTests.cs b/ShellUI.Tests/DoctorTests.cs new file mode 100644 index 0000000..b24202f --- /dev/null +++ b/ShellUI.Tests/DoctorTests.cs @@ -0,0 +1,115 @@ +using System.Text.Json; +using ShellUI.CLI.Services; +using ShellUI.Core.Models; + +namespace ShellUI.Tests; + +public class DoctorTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), $"doctor-{Guid.NewGuid():N}"); + + public DoctorTests() + { + Directory.CreateDirectory(Path.Combine(_root, "Components", "UI", "Variants")); + Directory.CreateDirectory(Path.Combine(_root, "wwwroot")); + Directory.CreateDirectory(Path.Combine(_root, "Build")); + File.WriteAllText(Path.Combine(_root, "MyApp.csproj"), ""); + File.WriteAllText(Path.Combine(_root, "Build", "ShellUI.targets"), ""); + File.WriteAllText(Path.Combine(_root, "Components", "UI", "Button.razor"), ""); + File.WriteAllText(Path.Combine(_root, "Components", "UI", "Variants", "ButtonVariants.cs"), ""); + File.WriteAllText(Path.Combine(_root, "Components", "_Imports.razor"), "@using MyApp.Components.UI\n@using MyApp.Components.UI.Variants\n"); + File.WriteAllText(Path.Combine(_root, "Components", "App.razor"), + "\n" + + ""); + File.WriteAllText(Path.Combine(_root, "wwwroot", "input.css"), "@import \"tailwindcss\";"); + File.WriteAllText(Path.Combine(_root, "wwwroot", "app.css"), ""); + File.SetLastWriteTimeUtc(Path.Combine(_root, "wwwroot", "app.css"), DateTime.UtcNow.AddMinutes(1)); + WriteConfig("button", "button-variants"); + } + + public void Dispose() + { + try { Directory.Delete(_root, recursive: true); } catch (IOException) { } + } + + private void WriteConfig(params string[] installed) + { + var config = new ShellUIConfig { InstalledComponents = installed.Select(n => new InstalledComponent { Name = n, Version = "0.4.0" }).ToList() }; + File.WriteAllText(Path.Combine(_root, "shellui.json"), JsonSerializer.Serialize(config)); + } + + private List Errors() => DoctorService.Diagnose(_root).Where(f => f.Severity == DoctorSeverity.Error).ToList(); + + [Fact] + public void HealthyProject_HasNoErrors() + { + Assert.Empty(Errors()); + } + + [Fact] + public void MissingShelluiJson_IsReported() + { + File.Delete(Path.Combine(_root, "shellui.json")); + + Assert.Contains(Errors(), f => f.Message.Contains("shellui.json")); + } + + [Fact] + public void NoRenderMode_IsReported() + { + var app = Path.Combine(_root, "Components", "App.razor"); + File.WriteAllText(app, File.ReadAllText(app).Replace(" @rendermode=\"InteractiveServer\"", "")); + + Assert.Contains(Errors(), f => f.Message.Contains("render mode")); + } + + [Fact] + public void HostPageWithoutShelluiJs_IsReported() + { + var app = Path.Combine(_root, "Components", "App.razor"); + File.WriteAllText(app, File.ReadAllText(app).Replace("", "")); + + Assert.Contains(Errors(), f => f.Message.Contains("shellui.js")); + } + + [Fact] + public void MissingDependency_IsReported() + { + WriteConfig("button"); + + Assert.Contains(Errors(), f => f.Message.Contains("button-variants")); + } + + [Fact] + public void DeletedComponentFile_IsReported() + { + File.Delete(Path.Combine(_root, "Components", "UI", "Button.razor")); + + Assert.Contains(Errors(), f => f.Message.Contains("file is gone") && f.Message.Contains("button")); + } + + [Fact] + public void MissingUsing_IsReported() + { + File.WriteAllText(Path.Combine(_root, "Components", "_Imports.razor"), "@using MyApp.Components.UI\n"); + + Assert.Contains(Errors(), f => f.Message.Contains("MyApp.Components.UI.Variants")); + } + + [Fact] + public void PackageAndCliTogether_IsAWarning() + { + File.WriteAllText(Path.Combine(_root, "MyApp.csproj"), + ""); + + Assert.Contains(DoctorService.Diagnose(_root), f => f.Severity == DoctorSeverity.Warning && f.Message.Contains("ShellUI.Components package")); + } + + [Fact] + public void StaleAppCss_IsAWarning() + { + File.SetLastWriteTimeUtc(Path.Combine(_root, "wwwroot", "app.css"), DateTime.UtcNow.AddHours(-1)); + + Assert.Contains(DoctorService.Diagnose(_root), f => f.Severity == DoctorSeverity.Warning && f.Message.Contains("older than")); + } +} diff --git a/ShellUI.Tests/PackageBehaviorTests.cs b/ShellUI.Tests/PackageBehaviorTests.cs index 39dfce5..ab4b685 100644 --- a/ShellUI.Tests/PackageBehaviorTests.cs +++ b/ShellUI.Tests/PackageBehaviorTests.cs @@ -1,6 +1,7 @@ using Bunit; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; +using Microsoft.Extensions.DependencyInjection; using ShellUI.Components; using ShellUI.Components.Models; @@ -218,4 +219,47 @@ public void TimePicker_ShowsASetValue() Assert.Contains("09:05", picker.Markup); } + + [Fact] + public async Task Sonner_ClearFadesEveryToastThenRemovesIt() + { + using var ctx = new BunitContext(); + ctx.Services.AddShellUISonner(); + var service = ctx.Services.GetRequiredService(); + var sonner = ctx.Render(); + + await sonner.InvokeAsync(() => service.Show("Saved", duration: TimeSpan.Zero)); + await sonner.InvokeAsync(() => service.Error("Failed", duration: TimeSpan.Zero)); + await sonner.InvokeAsync(service.Clear); + + Assert.All(service.Toasts, t => Assert.True(t.Dismissing)); + Assert.Equal(2, sonner.FindAll(".opacity-0").Count); + sonner.WaitForAssertion(() => Assert.Empty(service.Toasts)); + Assert.DoesNotContain("Saved", sonner.Markup); + } + + [Fact] + public void ContextMenuOption_Destructive_ReplacesTheAccentColours() + { + using var ctx = new BunitContext(); + var option = ctx.Render(p => p.Add(c => c.Destructive, true).Add(c => c.ChildContent, "Delete")); + + var classes = option.Find("button").ClassList; + Assert.Contains("text-destructive", classes); + Assert.Contains("hover:bg-destructive/10", classes); + Assert.DoesNotContain("hover:bg-accent", classes); + } + + [Fact] + public void ContextMenu_DestructiveItem_IsRed() + { + using var ctx = new BunitContext(); + var menu = ctx.Render(p => p + .Add(c => c.IsOpen, true) + .Add(c => c.Items, new List { new() { Label = "Edit" }, new() { Label = "Delete", Destructive = true } })); + + var buttons = menu.FindAll("button"); + Assert.DoesNotContain("text-destructive", buttons[0].ClassList); + Assert.Contains("text-destructive", buttons[1].ClassList); + } } diff --git a/ShellUI.Tests/SafelistDriftTests.cs b/ShellUI.Tests/SafelistDriftTests.cs index 3f3ae50..f55f69c 100644 --- a/ShellUI.Tests/SafelistDriftTests.cs +++ b/ShellUI.Tests/SafelistDriftTests.cs @@ -39,6 +39,28 @@ public void Safelist_MatchesGeneratedFromCurrentSources() BuildDiffMessage(addedSinceCommit, removedSinceCommit)); } + [Fact] + public void Generator_HarvestsClassesInRazorExpressions() + { + var file = Path.Combine(Path.GetTempPath(), $"safelist-{Guid.NewGuid():N}.razor"); + File.WriteAllLines(file, new[] + { + "
", + "", + "@code { string Tone(string v) => v switch { \"ok\" => \"bg-green-500/20\", _ => \"bg-background\" }; }", + }); + try + { + var classes = Program.GenerateSafelist(new[] { file }); + foreach (var expected in new[] { "translate-y-[-50%]", "sm:rounded-lg", "text-destructive", "text-muted-foreground", "bg-green-500/20", "bg-background" }) + Assert.Contains(expected, classes); + } + finally + { + File.Delete(file); + } + } + [Fact] public void GeneratedTargetsFile_IsValidXml() { diff --git a/ShellUI.Tests/ShellUI.Tests.csproj b/ShellUI.Tests/ShellUI.Tests.csproj index f7fee2a..0fda3c1 100644 --- a/ShellUI.Tests/ShellUI.Tests.csproj +++ b/ShellUI.Tests/ShellUI.Tests.csproj @@ -1,7 +1,7 @@  - net10.0 + net8.0;net10.0 enable enable false diff --git a/VERSIONING_STRATEGY.md b/VERSIONING_STRATEGY.md index 33221b5..6ab7987 100644 --- a/VERSIONING_STRATEGY.md +++ b/VERSIONING_STRATEGY.md @@ -6,8 +6,8 @@ ShellUI uses one centralized version for the source templates, CLI tool, and pac | Scope | Value | |---|---| -| Version | `0.4.0-alpha.3` (prerelease; latest stable `0.3.4`) | -| Target framework | .NET 10 | +| Version | `0.4.0-alpha.3` (prerelease; latest stable `0.3.5`) | +| Target framework | .NET 8 and .NET 10 (`ShellUI.Components` also .NET 9) | | Tailwind version | `4.3.2` | `0.3.0-rc.1` was the last release targeting .NET 9. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5ea66e5..df6d89c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,7 +7,7 @@ This document describes the ShellUI source tree. | Area | Current value | |---|---| | Version | See `Directory.Build.props` | -| Target framework | .NET 10 (`net10.0`) | +| Target framework | .NET 8 and .NET 10 (`net8.0;net10.0`); `ShellUI.Components` also `net9.0` | | Tailwind CSS | `4.3.2` | | Solution | `ShellUI.slnx` | | Solution projects | 6 | @@ -30,7 +30,7 @@ flowchart LR Safelist[ShellUI.SafelistGenerator
internal build utility] end - Project[Consumer project
net10.0] + Project[Consumer project
net8.0 to net10.0] Demo[NET10/BlazorInteractiveServer
demo outside the solution] Developer --> CLI diff --git a/docs/CLI_INSTALLATION.md b/docs/CLI_INSTALLATION.md index b6e52c8..b4bd853 100644 --- a/docs/CLI_INSTALLATION.md +++ b/docs/CLI_INSTALLATION.md @@ -4,12 +4,12 @@ | Context | Version | |---|---| -| Latest stable (recommended) | `0.3.4`, needs the .NET 10 runtime | +| Latest stable (recommended) | `0.3.5`, needs the .NET 10 runtime | | Prerelease | `0.4.0-alpha.3`, install with `--version 0.4.0-alpha.3` | | Previous stable | `0.2.1` | | Tailwind | `4.3.2` | -The tool needs the .NET 10 runtime. The projects it sets up can target .NET 8, 9 or 10. +The tool runs on .NET 8 or later (`0.4.0-alpha.3` and the 0.3 line need the .NET 10 runtime). The projects it sets up can target .NET 8, 9 or 10. ## Global installation @@ -44,7 +44,7 @@ The manifest is `.config/dotnet-tools.json` and uses the installed package versi "isRoot": true, "tools": { "shellui.cli": { - "version": "0.3.4", + "version": "0.3.5", "commands": ["shellui"] } } @@ -65,7 +65,7 @@ Commit the manifest so every developer and CI job uses the same tool version. ## Building from source -The repository targets `net10.0`. Build it with the .NET 10 SDK: +The repository targets `net8.0` and `net10.0`. Build it with the .NET 10 SDK; running the tests on both frameworks also needs the .NET 8 runtime: ```bash dotnet --version diff --git a/docs/CLI_SYNTAX.md b/docs/CLI_SYNTAX.md index e5b937c..b776b02 100644 --- a/docs/CLI_SYNTAX.md +++ b/docs/CLI_SYNTAX.md @@ -1,6 +1,6 @@ # ShellUI CLI Syntax -This reference covers CLI `0.4.0-alpha.3` (prerelease): it runs on .NET 10, sets up Blazor projects on .NET 8, 9 and 10, uses Tailwind CSS `4.3.2`, and has 93 direct component targets. Some options, such as `--dashboard`, are not in the stable 0.3.4; see the [v0.3.4 reference](https://github.com/shellui-dev/shellui/blob/v0.3.4/docs/CLI_SYNTAX.md) for that version. +This reference covers CLI `0.4.0-alpha.3` (prerelease): it runs on .NET 8 or later (`0.4.0-alpha.3` itself needs .NET 10), sets up Blazor projects on .NET 8, 9 and 10, uses Tailwind CSS `4.3.2`, and has 93 direct component targets. Some options, such as `--dashboard`, are not in the stable 0.3.4; see the [v0.3.4 reference](https://github.com/shellui-dev/shellui/blob/v0.3.4/docs/CLI_SYNTAX.md) for that version. ## Command prefix @@ -17,10 +17,11 @@ shellui --version ```text shellui init [--force] [--style