From b665d8c6c5dce9fd649007a1d801b9d9675b0e37 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 22 Oct 2026 11:53:38 +0200 Subject: [PATCH 1/6] feat: target .NET 8 as well as .NET 10 The CLI and ShellUI.Components only targeted net10.0, so teams on .NET 8 (LTS) could use neither the package nor the tool. - ShellUI.CLI (with Core and Templates) targets net8.0 and net10.0, so the tool runs on the .NET 8 runtime. ContentHash avoids the .NET 9-only Convert.ToHexStringLower. - ShellUI.Components targets net8.0, net9.0 and net10.0, with Microsoft.AspNetCore.Components.Web 8.0.13, 9.0.2 and 10.0.0. The net9.0 build is needed because Blazor-ApexCharts asks for 9.0.2 in a .NET 9 app, which a net8.0 build would downgrade. - Tests and the safelist generator run on net8.0 and net10.0; CI and the release workflow install both SDKs, and CI builds .NET 8 and 9 apps that use the package. - Docs drop the .NET 10-only requirement. Fixes #57 --- .github/workflows/ci.yml | 15 +++++++++++++++ .github/workflows/release.yml | 4 +++- README.md | 6 +++--- ShellUI.Tests/ShellUI.Tests.csproj | 2 +- VERSIONING_STRATEGY.md | 2 +- docs/ARCHITECTURE.md | 4 ++-- docs/CLI_INSTALLATION.md | 4 ++-- docs/COMPARISON.md | 2 +- docs/CONTRIBUTING.md | 2 +- docs/FAQ.md | 2 +- docs/PROJECT_STATUS.md | 2 +- docs/QUICKSTART.md | 4 ++-- src/ShellUI.CLI/README.md | 2 +- src/ShellUI.CLI/Services/ComponentInstaller.cs | 2 +- src/ShellUI.CLI/ShellUI.CLI.csproj | 2 +- src/ShellUI.Components/README.md | 4 +--- src/ShellUI.Components/ShellUI.Components.csproj | 15 +++++++++++++-- src/ShellUI.Core/ShellUI.Core.csproj | 2 +- src/ShellUI.Templates/ShellUI.Templates.csproj | 2 +- .../ShellUI.SafelistGenerator.csproj | 2 +- 20 files changed, 53 insertions(+), 27 deletions(-) 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..a9d7ec1 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ | Latest stable (recommended) | `0.3.4` | 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. @@ -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/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..14c63c1 100644 --- a/VERSIONING_STRATEGY.md +++ b/VERSIONING_STRATEGY.md @@ -7,7 +7,7 @@ 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 | +| 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..42fed6a 100644 --- a/docs/CLI_INSTALLATION.md +++ b/docs/CLI_INSTALLATION.md @@ -9,7 +9,7 @@ | 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 @@ -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/COMPARISON.md b/docs/COMPARISON.md index e110d4c..77d47ea 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -4,7 +4,7 @@ This is a qualitative comparison, not a benchmark report. Library features, pack ## Current ShellUI snapshot -- **Target framework:** .NET 10 +- **Target framework:** .NET 8, 9 and 10 - **Tailwind CSS:** `4.3.2` - **CLI inventory:** 93 direct targets, 243 registry entries, and 150 hidden entries - **Distribution:** the CLI copies source into an application; the repository also contains the packable `ShellUI.Components` package diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index aa42cfd..96c8659 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -32,7 +32,7 @@ The runnable demo at `NET10/BlazorInteractiveServer/BlazorInteractiveServer.cspr ## Prerequisites -- .NET 10 SDK. `global.json` requests SDK `10.0.100` with feature-band roll-forward. +- .NET 10 SDK. `global.json` requests SDK `10.0.100` with feature-band roll-forward. The projects also target `net8.0`, so running the tests on both frameworks needs the .NET 8 runtime. - Git. - Node.js and npm only when using the npm Tailwind method. The standalone method does not require Node.js. diff --git a/docs/FAQ.md b/docs/FAQ.md index b38ad30..f1e398c 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -12,7 +12,7 @@ Use the latest stable release, `0.3.4`. It uses Tailwind CSS `4.3.2`. The `0.4.0 dotnet tool install -g ShellUI.CLI ``` -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.) ### Which command prefix should I use? diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 98a8fd9..7e9c282 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -5,7 +5,7 @@ | Area | Current fact | |---|---| | Version | See `Directory.Build.props` | -| Framework | .NET 10 (`net10.0`) | +| Framework | .NET 8 and .NET 10 (`net8.0;net10.0`); `ShellUI.Components` also `net9.0` | | Tailwind CSS | `4.3.2` | | Solution | `ShellUI.slnx` with six projects | | Demo | `NET10/BlazorInteractiveServer/BlazorInteractiveServer.csproj`, outside the solution | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 2113a23..3aa00aa 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -1,10 +1,10 @@ # ShellUI Quick Start -This quick start uses ShellUI `0.4.0-alpha.3`, a prerelease, with Tailwind CSS `4.3.2`. The CLI needs the .NET 10 SDK and works in Blazor projects on .NET 8, 9 and 10. +This quick start uses ShellUI `0.4.0-alpha.3`, a prerelease, with Tailwind CSS `4.3.2`. The CLI runs on .NET 8 or later and works in Blazor projects on .NET 8, 9 and 10. ## Prerequisites -- .NET 10 SDK +- .NET 8 SDK or later - A Blazor project - The `ShellUI.CLI` tool - Node.js and npm only when choosing `--tailwind npm` diff --git a/src/ShellUI.CLI/README.md b/src/ShellUI.CLI/README.md index 0bff09f..7908c11 100644 --- a/src/ShellUI.CLI/README.md +++ b/src/ShellUI.CLI/README.md @@ -6,7 +6,7 @@ ## Install -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. The projects it sets up can target .NET 8, 9 or 10: ```bash dotnet tool install --global ShellUI.CLI diff --git a/src/ShellUI.CLI/Services/ComponentInstaller.cs b/src/ShellUI.CLI/Services/ComponentInstaller.cs index 57cb0c5..69ebbd8 100644 --- a/src/ShellUI.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.CLI/Services/ComponentInstaller.cs @@ -259,7 +259,7 @@ internal static string InstalledFilePath(ComponentMetadata metadata, ShellUIConf public static string ContentHash(string content) { var bytes = System.Text.Encoding.UTF8.GetBytes(content.Replace("\r\n", "\n")); - return Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(bytes)); + return Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(bytes)).ToLowerInvariant(); } private static void RecordInstalledComponent( diff --git a/src/ShellUI.CLI/ShellUI.CLI.csproj b/src/ShellUI.CLI/ShellUI.CLI.csproj index cbeaba9..19fcd70 100644 --- a/src/ShellUI.CLI/ShellUI.CLI.csproj +++ b/src/ShellUI.CLI/ShellUI.CLI.csproj @@ -12,7 +12,7 @@ Exe - net10.0 + net8.0;net10.0 enable enable diff --git a/src/ShellUI.Components/README.md b/src/ShellUI.Components/README.md index 5111602..eea881f 100644 --- a/src/ShellUI.Components/README.md +++ b/src/ShellUI.Components/README.md @@ -8,14 +8,12 @@ Prefer to own and edit the component source? Use the [ShellUI CLI](https://www.n ## Install -Requires a .NET 10 project: +Works in .NET 8, 9 and 10 projects: ```bash dotnet add package ShellUI.Components ``` -On .NET 8 or 9, use the [ShellUI CLI](https://www.nuget.org/packages/ShellUI.CLI) instead. The source it installs builds on .NET 8, 9 and 10. - ## Set up 1. Import the namespace in `Components/_Imports.razor`: diff --git a/src/ShellUI.Components/ShellUI.Components.csproj b/src/ShellUI.Components/ShellUI.Components.csproj index 16db844..00b47d8 100644 --- a/src/ShellUI.Components/ShellUI.Components.csproj +++ b/src/ShellUI.Components/ShellUI.Components.csproj @@ -1,7 +1,7 @@ - net10.0 + net8.0;net9.0;net10.0 enable enable @@ -28,12 +28,23 @@ - + + + + + + + + + + + + diff --git a/src/ShellUI.Core/ShellUI.Core.csproj b/src/ShellUI.Core/ShellUI.Core.csproj index 5149b5e..6e0baaa 100644 --- a/src/ShellUI.Core/ShellUI.Core.csproj +++ b/src/ShellUI.Core/ShellUI.Core.csproj @@ -1,7 +1,7 @@  - net10.0 + net8.0;net10.0 enable enable diff --git a/src/ShellUI.Templates/ShellUI.Templates.csproj b/src/ShellUI.Templates/ShellUI.Templates.csproj index 801406e..e9b7806 100644 --- a/src/ShellUI.Templates/ShellUI.Templates.csproj +++ b/src/ShellUI.Templates/ShellUI.Templates.csproj @@ -5,7 +5,7 @@ - net10.0 + net8.0;net10.0 enable enable diff --git a/tools/ShellUI.SafelistGenerator/ShellUI.SafelistGenerator.csproj b/tools/ShellUI.SafelistGenerator/ShellUI.SafelistGenerator.csproj index 81ec651..f1c0252 100644 --- a/tools/ShellUI.SafelistGenerator/ShellUI.SafelistGenerator.csproj +++ b/tools/ShellUI.SafelistGenerator/ShellUI.SafelistGenerator.csproj @@ -2,7 +2,7 @@ Exe - net10.0 + net8.0;net10.0 enable enable false From 145997e4134907ea3dc67da99f02a7e7c9d7738f Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 22 Oct 2026 12:04:33 +0200 Subject: [PATCH 2/6] fix: safelist the classes components pass to Shell.Cn The generator read a .razor class attribute only up to its first quote, so class="@Shell.Cn("...", Class)" contributed nothing, and C# literals in ternaries and switch arms were skipped too. 198 classes components render were missing from shellui-classes.txt, which apps building their own Tailwind CSS from the package depend on: the dialog's centring (translate-x/y-[-50%]), sm:rounded-lg, the Sonner variant colours and more. The precompiled bundle was not affected, because Tailwind scans the .razor files itself, except for the data-[state=...] classes the safelist exists for. The generator now also harvests string literals that follow (, ,, ?, :, = or => in .razor files, with the same class-list filter as .cs files, and accepts class lists up to 2000 characters (DialogContent's is 545). --- ShellUI.Tests/SafelistDriftTests.cs | 22 ++ .../build/ShellUI.Components.targets | 198 ++++++++++++++++++ .../wwwroot/shellui-classes.txt | 198 ++++++++++++++++++ tools/ShellUI.SafelistGenerator/Program.cs | 17 +- 4 files changed, 434 insertions(+), 1 deletion(-) 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/src/ShellUI.Components/build/ShellUI.Components.targets b/src/ShellUI.Components/build/ShellUI.Components.targets index 2cc43ba..e420105 100644 --- a/src/ShellUI.Components/build/ShellUI.Components.targets +++ b/src/ShellUI.Components/build/ShellUI.Components.targets @@ -1,64 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -66,13 +121,23 @@ + + + + + + + + + + @@ -90,6 +155,7 @@ + @@ -103,14 +169,21 @@ + + + + + + + @@ -129,21 +202,37 @@ + + + + + + + + + + + + + + + + @@ -158,11 +247,16 @@ + + + + + @@ -170,16 +264,22 @@ + + + + + + @@ -189,23 +289,34 @@ + + + + + + + + + + + @@ -213,18 +324,35 @@ + + + + + + + + + + + + + + + + + @@ -233,11 +361,14 @@ + + + @@ -247,11 +378,13 @@ + + @@ -261,25 +394,35 @@ + + + + + + + + + + @@ -292,6 +435,7 @@ + @@ -301,14 +445,24 @@ + + + + + + + + + + @@ -322,6 +476,7 @@ + @@ -331,40 +486,61 @@ + + + + + + + + + + + + + + + + + + + + + @@ -372,27 +548,44 @@ + + + + + + + + + + + + + + + + + @@ -401,19 +594,24 @@ + + + + + diff --git a/src/ShellUI.Components/wwwroot/shellui-classes.txt b/src/ShellUI.Components/wwwroot/shellui-classes.txt index f671aa4..ba1da46 100644 --- a/src/ShellUI.Components/wwwroot/shellui-classes.txt +++ b/src/ShellUI.Components/wwwroot/shellui-classes.txt @@ -1,61 +1,116 @@ +active:scale-[0.98] +after:-inset-2 +after:absolute +after:md:hidden align-middle animate-bounce animate-in +animate-pulse animate-spin appearance-none aspect-square +backdrop-blur backdrop-blur-md backdrop-blur-sm +bars-vertical bg-accent +bg-accent/30 +bg-accent/50 bg-background bg-background/80 bg-background/95 bg-black/50 bg-black/80 bg-blue-500 +bg-blue-500/20 bg-border bg-card bg-current bg-destructive +bg-emerald-500/10 +bg-green-50 bg-green-500 +bg-green-500/20 +bg-input bg-muted +bg-muted-foreground bg-muted/30 bg-muted/40 bg-popover bg-primary +bg-primary/10 +bg-red-500/10 bg-secondary bg-sidebar +bg-sidebar-accent +bg-sidebar-border bg-sidebar-primary bg-transparent +bg-white/10 +bg-yellow-50 bg-yellow-500 +bg-yellow-500/20 border-0 +border-2 border-b border-border border-border/40 +border-dashed border-destructive +border-green-500 border-input border-l +border-muted +border-primary border-r +border-ring +border-sidebar-border border-t border-transparent +border-yellow-500 bottom-0 bottom-4 bottom-full +bottom-left +bottom-right +break-words caption-bottom cursor-default +cursor-not-allowed cursor-pointer +dark:bg-blue-900/30 +dark:bg-green-900/20 +dark:bg-green-900/30 +dark:bg-yellow-900/20 +dark:bg-yellow-900/30 +dark:text-blue-100 +dark:text-emerald-400 +dark:text-green-100 +dark:text-red-400 +dark:text-yellow-100 +data-[disabled]:opacity-50 +data-[disabled]:pointer-events-none data-[selected]:bg-accent data-[selected]:text-accent-foreground data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=closed]:fade-out-0 +data-[state=closed]:slide-out-to-left-1/2 +data-[state=closed]:slide-out-to-top-[48%] +data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:bg-accent data-[state=open]:duration-500 data-[state=open]:fade-in-0 data-[state=open]:grid-rows-[1fr] +data-[state=open]:opacity-100 +data-[state=open]:slide-in-from-left-1/2 +data-[state=open]:slide-in-from-top-[48%] data-[state=open]:text-muted-foreground +data-[state=open]:zoom-in-95 +data-[state=selected]:bg-muted disabled:cursor-not-allowed +disabled:opacity-40 disabled:opacity-50 disabled:pointer-events-none duration-150 @@ -63,13 +118,23 @@ duration-200 duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] ease-in-out +ease-linear ease-out fa-bell +fa-book +fa-bullhorn fa-chevron-right fa-circle-check fa-circle-plus +fa-code fa-credit-card fa-ellipsis +fa-gear +fa-life-ring +fa-location-dot +fa-microchip +fa-paper-plane +fa-pen-ruler fa-right-from-bracket fa-solid fa-sort @@ -87,6 +152,7 @@ flex-1 flex-col flex-col-reverse flex-row +flex-row-reverse flex-shrink-0 flex-wrap focus-visible:[&::-moz-range-thumb]:ring-2 @@ -100,14 +166,21 @@ focus-visible:ring-1 focus-visible:ring-2 focus-visible:ring-destructive focus-visible:ring-offset-2 +focus-visible:ring-offset-background focus-visible:ring-ring +focus-within:ring-2 +focus-within:ring-offset-2 +focus-within:ring-ring focus:bg-accent +focus:bg-primary +focus:border-ring focus:outline-none focus:ring-1 focus:ring-2 focus:ring-offset-2 focus:ring-ring focus:text-accent-foreground +focus:text-primary-foreground font-bold font-medium font-mono @@ -126,21 +199,37 @@ grid-cols-3 grid-cols-5 grid-cols-7 grid-rows-[0fr] +group-content +group-data-[collapsible=icon]:!bg-sidebar-primary group-data-[collapsible=icon]:!flex group-data-[collapsible=icon]:!items-center group-data-[collapsible=icon]:!justify-center group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:!size-8 +group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:hidden +group-data-[collapsible=icon]:opacity-0 +group-data-[collapsible=icon]:overflow-hidden +group-data-[collapsible=icon]:text-sidebar-primary-foreground +group-data-[collapsible=offcanvas]:after:left-full +group-data-[collapsible=offcanvas]:hover:bg-sidebar +group-data-[collapsible=offcanvas]:translate-x-0 +group-data-[side=right]:group-data-[collapsible=offcanvas]:-translate-x-full group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow group-first:top-3.5 +group-focus-within/menu-item:opacity-100 +group-hover/menu-item:opacity-100 +group-hover:scale-105 +group-label group-last:bottom-auto group-last:h-3.5 group-only:hidden +group/menu-item +group/sidebar-wrapper h-1.5 h-1/2 h-10 @@ -155,11 +244,16 @@ h-3 h-3.5 h-4 h-5 +h-6 h-7 h-8 h-9 +h-[1px] h-full h-px +h-svh +h1v1h-1z +has-[[data-variant=inset]]:bg-sidebar hover:bg-accent hover:bg-accent/50 hover:bg-blue-600 @@ -167,16 +261,22 @@ hover:bg-destructive/80 hover:bg-destructive/90 hover:bg-green-600 hover:bg-muted +hover:bg-muted/50 +hover:bg-primary hover:bg-primary/80 hover:bg-primary/90 hover:bg-secondary/80 hover:bg-sidebar-accent +hover:bg-white/20 hover:bg-yellow-600 +hover:border-border +hover:border-primary/50 hover:opacity-100 hover:text-accent-foreground hover:text-destructive hover:text-foreground hover:text-muted-foreground +hover:text-primary-foreground hover:text-sidebar-accent-foreground hover:underline inline-block @@ -186,23 +286,34 @@ inset-x-0 inset-y-0 items-baseline items-center +items-end items-start justify-between justify-center justify-end +last:border-b-0 +last:pb-0 leading-none +leading-relaxed leading-tight left-0 left-1/2 left-2 left-3.5 +left-[50%] +left-full lg:space-x-8 list-none max-h-48 max-h-60 +max-h-[300px] max-h-[90vh] max-h-[min(360px,55vh)] +max-h-screen +max-w-[80%] max-w-[90vw] +max-w-lg +max-w-max max-w-sm max-w-xl mb-1 @@ -210,18 +321,35 @@ mb-2 mb-4 mb-6 md:block +md:flex +md:max-w-[420px] +md:opacity-0 +md:peer-data-[variant=inset]:m-2 +md:peer-data-[variant=inset]:ml-0 +md:peer-data-[variant=inset]:rounded-xl +md:peer-data-[variant=inset]:shadow +menu-action +menu-badge +menu-button +menu-item +menu-sub +menu-sub-button +menu-sub-item min-h-0 min-h-10 min-h-7 min-h-8 min-h-[80px] +min-h-svh min-w-0 min-w-24 min-w-5 min-w-8 +min-w-[12rem] min-w-[640px] min-w-[8rem] ml-1 +ml-2 ml-4 ml-auto motion-reduce:animate-none @@ -230,11 +358,14 @@ mr-2 mr-3 mt-0.5 mt-1 +mt-10 mt-2 mt-4 mt-6 mt-8 mt-auto +mx-2 +mx-3.5 mx-auto my-1 object-contain @@ -244,11 +375,13 @@ opacity-50 opacity-70 opacity-75 opacity-90 +opacity:.12 origin-top-right outline-none overflow-auto overflow-hidden overflow-x-auto +overflow-x-hidden overflow-y-auto overscroll-contain p-0.5 @@ -258,25 +391,35 @@ p-2 p-3 p-4 p-6 +p-8 p-[3px] pb-0.5 pb-1 pb-4 +pb-8 +peer-data-[active=true]/menu-button:text-sidebar-accent-foreground +peer-data-[variant=inset]:min-h-[calc(100svh-1rem)] peer-disabled:cursor-not-allowed peer-disabled:opacity-70 +peer-hover/menu-button:text-sidebar-accent-foreground peer/menu-button pl-2 +pl-2.5 pl-3 pl-4 +pl-8 placeholder:text-muted-foreground pointer-events-auto pointer-events-none pr-2 +pr-2.5 pr-3 pr-6 pr-8 +pr-9 pt-0 pt-3 +pt-6 px-1 px-1.5 px-2 @@ -289,6 +432,7 @@ py-0.5 py-1 py-1.5 py-10 +py-12 py-2 py-3 py-4 @@ -298,14 +442,24 @@ right-0 right-1 right-2 right-4 +right-full +ring-0 +ring-2 +ring-4 +ring-offset-2 ring-offset-background +ring-primary/20 +ring-ring ring-sidebar-ring +rotate-180 +rotate-90 rounded-[2px] rounded-full rounded-lg rounded-md rounded-sm rounded-xl +scale-[1.02] select-none shadow-2xl shadow-lg @@ -319,6 +473,7 @@ shellui-chart-tooltip-marker shellui-chart-tooltip-row shellui-chart-tooltip-title shellui-chart-tooltip-value +shellui-loading shrink-0 size-4 size-8 @@ -328,40 +483,61 @@ slide-in-from-left slide-in-from-right slide-in-from-top slide-in-from-top-2 +sm:flex-col sm:flex-row +sm:gap-2.5 sm:inline sm:inline-flex sm:items-center sm:justify-between +sm:justify-end sm:max-w-sm +sm:pl-2.5 +sm:pr-2.5 +sm:px-4 +sm:rounded-lg +sm:space-x-2 sm:space-x-6 sm:text-left space-x-1 space-x-2 +space-x-4 space-y-0.5 +space-y-1.5 space-y-2 space-y-4 +space-y-6 sr-only starting:opacity-0 starting:scale-95 +supports-[backdrop-filter]:bg-background/60 tabular-nums text-2xl text-[10px] +text-[11px] text-[8px] text-accent-foreground +text-base +text-blue-700 text-card-foreground text-center +text-destructive text-destructive-foreground +text-emerald-600 text-foreground text-foreground/70 +text-green-700 +text-green-900 text-left text-lg text-muted-foreground text-popover-foreground text-primary text-primary-foreground +text-red-600 text-right text-secondary-foreground +text-sidebar-accent-foreground text-sidebar-foreground text-sidebar-foreground/70 text-sidebar-primary-foreground @@ -369,27 +545,44 @@ text-sm text-white text-xl text-xs +text-yellow-800 +text-yellow-900 top-0 top-2 top-4 top-8 top-[15vh] +top-[1px] +top-[50%] top-full +top-right +touch-none tracking-tight transition-[grid-template-rows] +transition-[left,right,width] transition-[width,height,padding] +transition-[width] transition-all transition-colors transition-opacity transition-transform +translate-x-0 +translate-x-5 +translate-x-[-50%] +translate-x-px +translate-y-[-50%] underline-offset-4 var(--border var(--muted +var(--sidebar-width +var(--sidebar-width-icon w-1.5 w-1/2 w-10 +w-11 w-12 w-16 +w-2 w-2.5 w-2/5 w-3 @@ -398,19 +591,24 @@ w-3/4 w-4 w-5 w-56 +w-6 w-64 w-7 +w-72 w-8 w-9 w-[100px] +w-[1px] w-[70px] w-[calc(100%-2rem)] +w-auto w-fit w-full w-max w-px whitespace-nowrap whitespace-pre-line +width: z-10 z-40 z-50 diff --git a/tools/ShellUI.SafelistGenerator/Program.cs b/tools/ShellUI.SafelistGenerator/Program.cs index 1ad88ea..c19d33c 100644 --- a/tools/ShellUI.SafelistGenerator/Program.cs +++ b/tools/ShellUI.SafelistGenerator/Program.cs @@ -64,6 +64,7 @@ public static SortedSet GenerateSafelist(IEnumerable sourceFileP if (file.EndsWith(".razor", StringComparison.OrdinalIgnoreCase)) { ExtractClasses(text, classes); + ExtractFromRazorExpressions(text, classes); } else if (file.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) { @@ -86,7 +87,7 @@ private static void ExtractFromCSharp(string content, SortedSet sink) private static bool LooksLikeClassList(string s) { - if (s.Length == 0 || s.Length > 500) return false; + if (s.Length == 0 || s.Length > 2000) return false; // `=` outside `[…]` implies key=value pairs, not class strings. if (s.Contains("://") || s.Contains('<') || s.Contains('>') || s.Contains('{') || s.Contains(';') || s.Contains('=') && !s.Contains('[') @@ -151,6 +152,20 @@ private static string XmlEscape(string s) "class\\s*=\\s*\"(?[^\"]*)\"", RegexOptions.Compiled); + // Shell.Cn arguments, ternaries and switch arms, which the class attribute regex cuts off at their first quote. + private static readonly Regex RazorExpressionLiteralRegex = new( + "(?<=[(,?:=>]\\s*)\"(?[^\"@]*)\"", + RegexOptions.Compiled); + + private static void ExtractFromRazorExpressions(string content, SortedSet sink) + { + foreach (Match lit in RazorExpressionLiteralRegex.Matches(content)) + { + var value = lit.Groups["lit"].Value; + if (LooksLikeClassList(value)) HarvestTokens(value, sink); + } + } + private static readonly Regex StringLiteralRegex = new( "\"(?[^\"]*)\"", RegexOptions.Compiled); From 6ee240d3de3848eb81c9f907136eb175074d4fc4 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 22 Oct 2026 12:06:19 +0200 Subject: [PATCH 3/6] feat: SonnerService.Clear() and destructive context menu items - SonnerService.Clear() dismisses every toast. Dismissed toasts (Remove, Clear, auto-dismiss) now fade out over 300 ms before they're removed, instead of disappearing at once; SonnerToastItem.Dismissing marks them. - ContextMenuItem.Destructive and ContextMenuOption's Destructive parameter draw the item in the destructive colour, with matching hover and focus colours, for actions like Delete. Package and CLI templates alike, with tests. Fixes #61 --- ShellUI.Tests/PackageBehaviorTests.cs | 44 +++++++++++++++++++ .../Components/ContextMenu.razor | 4 +- .../Components/ContextMenuOption.razor | 5 ++- .../Components/Sonner.razor | 2 +- .../Models/ContextMenuModels.cs | 1 + .../Services/SonnerService.cs | 21 +++++++-- .../build/ShellUI.Components.targets | 4 ++ .../wwwroot/shellui-classes.txt | 4 ++ .../Templates/ContextMenuModelsTemplate.cs | 1 + .../Templates/ContextMenuOptionTemplate.cs | 5 ++- .../Templates/ContextMenuTemplate.cs | 4 +- .../Templates/SonnerServiceTemplate.cs | 21 +++++++-- .../Templates/SonnerTemplate.cs | 2 +- 13 files changed, 102 insertions(+), 16 deletions(-) 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/src/ShellUI.Components/Components/ContextMenu.razor b/src/ShellUI.Components/Components/ContextMenu.razor index 2014cc5..e323285 100644 --- a/src/ShellUI.Components/Components/ContextMenu.razor +++ b/src/ShellUI.Components/Components/ContextMenu.razor @@ -25,12 +25,12 @@ } else { -