Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x

- name: Restore dependencies
Expand Down Expand Up @@ -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 '<Button>Save</Button> <Card><CardContent>ok</CardContent></Card>' >> 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:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions ShellUI.Tests/AddAllTests.cs
Original file line number Diff line number Diff line change
@@ -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<string>(StringComparer.OrdinalIgnoreCase) { excluded };
Assert.Equal(expected, ComponentInstaller.DependsOnAny(name, names));
}
}
115 changes: 115 additions & 0 deletions ShellUI.Tests/DoctorTests.cs
Original file line number Diff line number Diff line change
@@ -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"), "<Project Sdk=\"Microsoft.NET.Sdk.Web\"></Project>");
File.WriteAllText(Path.Combine(_root, "Build", "ShellUI.targets"), "<Project />");
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"),
"<head><link rel=\"stylesheet\" href=\"app.css\" /><script>// ShellUI theme bootstrap</script><HeadOutlet @rendermode=\"InteractiveServer\" /></head>\n" +
"<body><Routes @rendermode=\"InteractiveServer\" /><script src=\"shellui.js\"></script></body>");
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<DoctorFinding> 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("<script src=\"shellui.js\"></script>", ""));

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"),
"<Project Sdk=\"Microsoft.NET.Sdk.Web\"><ItemGroup><PackageReference Include=\"ShellUI.Components\" Version=\"0.4.0\" /></ItemGroup></Project>");

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"));
}
}
44 changes: 44 additions & 0 deletions ShellUI.Tests/PackageBehaviorTests.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<ShellUI.Components.Services.SonnerService>();
var sonner = ctx.Render<Sonner>();

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<ContextMenuOption>(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<ContextMenu>(p => p
.Add(c => c.IsOpen, true)
.Add(c => c.Items, new List<ContextMenuItem> { 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);
}
}
22 changes: 22 additions & 0 deletions ShellUI.Tests/SafelistDriftTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
{
"<div class=\"@Shell.Cn(\"fixed translate-y-[-50%] sm:rounded-lg\", Class)\"></div>",
"<span class=\"flex @(Active ? \"text-destructive\" : \"text-muted-foreground\")\"></span>",
"@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()
{
Expand Down
2 changes: 1 addition & 1 deletion ShellUI.Tests/ShellUI.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand Down
4 changes: 2 additions & 2 deletions VERSIONING_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -30,7 +30,7 @@ flowchart LR
Safelist[ShellUI.SafelistGenerator<br/>internal build utility]
end

Project[Consumer project<br/>net10.0]
Project[Consumer project<br/>net8.0 to net10.0]
Demo[NET10/BlazorInteractiveServer<br/>demo outside the solution]

Developer --> CLI
Expand Down
8 changes: 4 additions & 4 deletions docs/CLI_INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"]
}
}
Expand All @@ -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
Expand Down
Loading
Loading