Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0bdb202
feat: add safelist drift tests and CI verification for NuGet package …
Shewart Jun 21, 2026
fbd29c7
feat: add CI step to verify NuGet package safelist installation
Shewart Jun 21, 2026
51863f3
feat: add Tailwind CSS safelist classes to shellui-classes.txt
Shewart Jun 21, 2026
0c5cdab
refactor: update Tailwind CSS versioning and installation process
Shewart Jun 21, 2026
d4923ae
chore: update Tailwind CSS version to 4.3.1 in documentation
Shewart Jun 21, 2026
c13c517
refactor: replace hardcoded Tailwind CSS version with constant
Shewart Jun 24, 2026
0659a6c
feat: add Tailwind CSS safelist generator tool
Shewart Jun 24, 2026
e90aa5e
feat: add ShellUI.SafelistGenerator project to tools folder
Shewart Jun 24, 2026
1b6d1b8
chore: update Tailwind CSS version to 4.3.1 in documentation
Shewart Jun 24, 2026
dd88168
feat: enhance CI workflow for NuGet package safelist verification
Shewart Jun 24, 2026
5494936
feat: add build targets for Tailwind CSS safelist integration
Shewart Jun 24, 2026
d938832
fix: update CI workflow to ensure safelist file is generated after pr…
Shewart Jun 24, 2026
51ab68f
feat: enhance SafelistDriftTests with new validation checks
Shewart Jun 30, 2026
cf82c08
refactor: embed Tailwind CSS safelist directly in build targets
Shewart Jun 30, 2026
c13b719
chore: update Tailwind CSS version to 4.3.2 in TailwindConstants
Shewart Jun 30, 2026
f256eb6
feat: enhance ShellUI.SafelistGenerator to output multiple artifacts
Shewart Jun 30, 2026
dfbc095
feat: add XML validation test for generated targets file
Shewart Jul 1, 2026
909b5d8
refactor: improve comments in ShellUI.Components.targets for clarity
Shewart Jul 1, 2026
867fae5
refactor: improve comments in ShellUI.SafelistGenerator for XML compl…
Shewart Jul 1, 2026
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
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,56 @@ jobs:

dotnet build -c Debug

# Pure-NuGet install path — `dotnet add package ShellUI.Components` without
# the CLI. Uses a one-off NuGet.config that whitelists ONLY the local feed,
# so a missing local package can't silently fall back to nuget.org and pull
# an older version (which would mask whether the current PR's package is
# correct). $GITHUB_WORKSPACE is absolute — relative paths from inside the
# temp consumer dir would resolve to the wrong root.
- name: NuGet-only install — verify safelist ships
shell: bash
run: |
set -euxo pipefail
LOCAL_FEED="$GITHUB_WORKSPACE/src/ShellUI.Components/bin/Release"
test -d "$LOCAL_FEED" || (echo "local feed not found at $LOCAL_FEED — did the pack step run?"; exit 1)
ls "$LOCAL_FEED"/*.nupkg | head -3

TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/app" && cd "$TMPDIR/app"
dotnet new blazor -o NuGetSmoke --no-restore
cd NuGetSmoke

# Pin restore to only the local feed; nuget.org explicitly disabled so a
# broken local feed can't be papered over by a public-published older version.
cat > NuGet.config <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="local" value="$LOCAL_FEED" />
</packageSources>
</configuration>
EOF

dotnet add package ShellUI.Components --prerelease

# The safelist gets copied into wwwroot/ during the consumer's build by
# the package's build/ShellUI.Components.targets — auto-imported by NuGet.
# Need to actually build the project, not just restore, for the Copy to run.
dotnet build -c Debug

SAFELIST="wwwroot/shellui-classes.txt"
test -f "$SAFELIST" || (
echo "ERROR: $SAFELIST not in consumer project after dotnet build."
echo "The package's build/ShellUI.Components.targets should have copied it."
echo "Files in wwwroot/:"; ls -la wwwroot/ 2>/dev/null || echo " (no wwwroot/ directory)"
exit 1
)

# Sanity: the safelist contains real Tailwind classes
grep -q 'bg-background' "$SAFELIST" || (echo "safelist appears malformed — missing core Tailwind class 'bg-background'"; exit 1)
wc -l "$SAFELIST"

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
Expand Down
62 changes: 39 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ ShellUI transforms Blazor component development with a hybrid approach:
- **CLI-First**: Copy components to YOUR codebase for full control (`shellui add button`)
- **NuGet Option**: Traditional package install for quick starts (`dotnet add package ShellUI.Components`)
- **Choose your workflow**: Use CLI for customization, NuGet for speed, or mix both
- Powered by Tailwind CSS v4.1.18 (standalone CLI - no Node.js required!)
- Powered by Tailwind CSS v4.3.1 (standalone CLI - no Node.js required!)
- Best of both worlds: flexibility when you need it, convenience when you want it

## Current Status: 68 Components (Alpha) 🎉

**ShellUI is in alpha!** Test and provide feedback before we ship stable. We've completed:
- ✅ **CLI Tool** (`dotnet tool install -g ShellUI.CLI`)
- ✅ **NuGet Package** (`dotnet add package ShellUI.Components`)
- ✅ **68 Installable Components** with Tailwind v4.1.18 *(top-level components you `shellui add` — sub-components, variants, models, and services auto-install as dependencies)*
- ✅ **68 Installable Components** with Tailwind v4.3.1 *(top-level components you `shellui add` — sub-components, variants, models, and services auto-install as dependencies)*
- ✅ **Hybrid Workflow** (CLI + NuGet)
- ✅ **No Node.js Required** (Standalone Tailwind CLI)
- ✅ **Comprehensive Documentation**
Expand Down Expand Up @@ -141,7 +141,7 @@ Alert, Callout, EmptyState, Loading, Progress, Skeleton, Sonner, Toast, Tooltip
**Utility (2):**
CopyButton, ThemeToggle

### ✅ Tailwind CSS v4.1.18 Integration
### ✅ Tailwind CSS v4.3.1 Integration

**Two Setup Methods:**

Expand All @@ -159,7 +159,7 @@ shellui init # Choose "standalone"
shellui init # Choose "npm"
# Or: dotnet shellui init
```
- Installs `tailwindcss@^4.1.18` + `@tailwindcss/cli@^4.1.18`
- Installs `tailwindcss@^4.3.1` + `@tailwindcss/cli@^4.3.1`
- Uses `npx @tailwindcss/cli` for builds
- Requires Node.js

Expand Down Expand Up @@ -198,7 +198,7 @@ shellui init # Choose "npm"
## Design Principles

1. **Copy, Don't Install**: Components are copied to your project, not imported from a package
2. **Tailwind-First**: All styling uses Tailwind CSS v4.1.18 utility classes
2. **Tailwind-First**: All styling uses Tailwind CSS v4.3.1 utility classes
3. **Accessible by Default**: WCAG 2.1 AA compliant out of the box
4. **Composable**: Build complex components from simple ones
5. **Customizable**: Modify any component to fit your needs
Expand All @@ -225,7 +225,7 @@ shellui init # Choose "npm"

**Use both:** Start with NuGet, migrate to CLI for components you customize heavily!

### Why Tailwind v4.1.18?
### Why Tailwind v4.3.1?
- Latest stable version with v4 features
- Better performance than v3
- Improved dark mode support
Expand Down Expand Up @@ -285,7 +285,7 @@ Simply edit the component file in `Components/UI/` - it's yours to modify!
- .NET 8.0 or higher
- **Choice of Tailwind setup:**
- **Standalone CLI** (recommended): No Node.js required
- **npm**: Requires Node.js, uses `tailwindcss@^4.1.18`
- **npm**: Requires Node.js, uses `tailwindcss@^4.3.1`

## Comparison with Existing Solutions

Expand All @@ -294,7 +294,7 @@ Simply edit the component file in `Components/UI/` - it's yours to modify!
| CLI Installation | ✅ | ❌ | ❌ | ❌ |
| NuGet Package | ✅ | ✅ | ✅ | ✅ |
| Component Ownership (CLI) | ✅ | ❌ | ❌ | ❌ |
| Tailwind CSS | ✅ (v4.1.18) | ❌ | ❌ | ❌ |
| Tailwind CSS | ✅ (v4.3.1) | ❌ | ❌ | ❌ |
| No Node.js Required | ✅ | N/A | N/A | N/A |
| Hybrid Workflow | ✅ | ❌ | ❌ | ❌ |
| Free & Open Source | ✅ | ✅ | Partial | ✅ |
Expand All @@ -315,30 +315,46 @@ ShellUI ships two NuGet packages — the CLI is the primary install path; the ru

## Installation

ShellUI is a **CLI-first** library. The CLI is what wires up Tailwind, drops the theme into `wwwroot/input.css`, patches `App.razor` with the render mode and theme bootstrap, and copies component source into your project so Tailwind can scan it.
Two paths — pick based on whether you already have Tailwind set up.

### Path A — NuGet + your existing Tailwind setup (new in 0.4.x)

If your project already builds Tailwind, install the runtime DLL via NuGet and add one line to your `input.css`:

```bash
dotnet tool install -g ShellUI.CLI
shellui init # one-time setup
shellui add button card dialog # any time you want more components
dotnet add package ShellUI.Components
```

That's it. Components render styled out of the box. Re-run `shellui add` whenever you want more.
The package ships a `shellui-classes.txt` safelist at `wwwroot/shellui-classes.txt` in your project. Wire Tailwind to scan it:

### Where does `ShellUI.Components` (the NuGet package) fit?
```css
/* wwwroot/input.css */
@import "tailwindcss";
@source "./shellui-classes.txt";
```

The NuGet package ships the same component DLLs, the JS interop (`shellui.js`), and helpers like `Shell.Cn`. It's useful when you want the runtime types referenced directly — e.g. consuming `Shell.Cn` from your own code, or shipping a library that re-exports ShellUI components.
`@using ShellUI.Components` in your `_Imports.razor` and you're done. Tailwind tree-shakes — only classes that actually appear in your code (yours + ShellUI's) end up in the compiled CSS.

**The NuGet package alone does not produce styled components.** Tailwind v4 builds the CSS at compile time by scanning `.razor` source files. The component source lives inside the DLL, so Tailwind never sees the utility classes used inside ShellUI components and emits no rules for them. The result: components render with the right HTML structure but with most styling missing.
### Path B — CLI tool (full source control, no Tailwind setup required)

If you want zero Tailwind config of your own, or want to edit component source directly, use the CLI:

```bash
dotnet tool install -g ShellUI.CLI
shellui init # one-time setup — installs Tailwind, theme CSS, patches App.razor
shellui add button card dialog # any time you want more components
```

The supported way to get styled components is:
The CLI copies the `.razor` files into your project. You own them — edit them however you like.

1. `dotnet tool install -g ShellUI.CLI`
2. `shellui init` — sets up Tailwind, theme CSS, and patches `App.razor`
3. `shellui add <component-name>` for every component you want styled — this copies the `.razor` source into your project so Tailwind can scan it
4. *(Optional)* `dotnet add package ShellUI.Components --prerelease` if you also want the runtime DLL on hand (most projects don't need both — pick one)
### Which one?

We're tracking a proper "NuGet-only with auto-CSS" path for a future release (`v0.4.x`). Until then, run the CLI.
| You want… | Use |
|---|---|
| Full source control over every component | CLI |
| Restyle by editing the component source | CLI |
| Minimal install, you already use Tailwind | NuGet |
| Mix — NuGet for most, CLI for ones you customize | both (no conflict; NuGet's classes are tree-shaken via the safelist, CLI-copied components Tailwind scans directly)

### Configure Tailwind CSS manually (advanced)
Create/update `wwwroot/tailwind.config.js`:
Expand Down Expand Up @@ -575,7 +591,7 @@ MIT License - See LICENSE.txt for details

## Status

**Alpha:** 68 components, CLI + NuGet, Tailwind v4.1.18. Test before stable.
**Alpha:** 68 components, CLI + NuGet, Tailwind v4.3.1. Test before stable.
**🚀 Ready to use today!**

---
Expand Down
122 changes: 122 additions & 0 deletions ShellUI.Tests/SafelistDriftTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Xml;
using ShellUI.SafelistGenerator;
using Xunit;

namespace ShellUI.Tests;

/// Regenerates the safelist in-process and diffs against the committed file.
/// If a contributor adds a new Tailwind class to a component but forgets to
/// regenerate the safelist, this test fails with a precise diff of which
/// classes are missing — so NuGet consumers don't ship with broken styles.
public class SafelistDriftTests
{
private const string RegenerateCommand =
"dotnet run --project tools/ShellUI.SafelistGenerator -- "
+ "src/ShellUI.Components/Components "
+ "src/ShellUI.Components/wwwroot/shellui-classes.txt "
+ "src/ShellUI.Components/build/ShellUI.Components.targets";

[Fact]
public void Safelist_MatchesGeneratedFromCurrentRazorSources()
{
var componentsDir = ResolveComponentsDir();
var safelistPath = ResolveSafelistPath();

Assert.True(Directory.Exists(componentsDir), $"components dir not found: {componentsDir}");
Assert.True(File.Exists(safelistPath), $"safelist not found at {safelistPath}. Run: {RegenerateCommand}");

var razorFiles = Directory.GetFiles(componentsDir, "*.razor", SearchOption.AllDirectories);
var freshlyGenerated = Program.GenerateSafelist(razorFiles);
var committed = File.ReadAllLines(safelistPath)
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToHashSet();

var addedSinceCommit = freshlyGenerated.Except(committed).Take(10).ToList();
var removedSinceCommit = committed.Except(freshlyGenerated).Take(10).ToList();

Assert.True(addedSinceCommit.Count == 0 && removedSinceCommit.Count == 0,
BuildDiffMessage(addedSinceCommit, removedSinceCommit));
}

[Fact]
public void GeneratedTargetsFile_IsValidXml()
{
// XML 1.0 disallows `--` inside comment bodies. Some MSBuild versions are
// permissive (Windows .NET 10.300 accepts it); strict parsers reject the
// whole file with MSB4024. Run an actual XML parse so we catch this and
// any other malformed-XML class of bug before the consumer's build does.
var targetsPath = ResolveTargetsPath();
Assert.True(File.Exists(targetsPath), $"targets file not found at {targetsPath}");

var doc = new XmlDocument();
var ex = Record.Exception(() => doc.Load(targetsPath));
Assert.True(ex is null,
$"build/ShellUI.Components.targets is not valid XML — strict MSBuild parsers will reject it.\n" +
$"Likely cause: a `--` sequence inside an XML comment body. See the SafelistGenerator's comment template.\n\n" +
$"Underlying error: {ex?.Message}");
}

[Fact]
public void GeneratedTargetsFile_EmbedsSameClassesAsSafelist()
{
// The build/ShellUI.Components.targets file ships in the NuGet package and
// writes the safelist into the consumer's wwwroot/ at build time. If the
// embedded ItemGroup drifts from the .txt source-of-truth, NuGet consumers
// see different classes than CLI consumers — bug class we want to never
// ship.
var safelistPath = ResolveSafelistPath();
var targetsPath = ResolveTargetsPath();

Assert.True(File.Exists(targetsPath), $"targets file not found at {targetsPath}. Run: {RegenerateCommand}");

var committedClasses = File.ReadAllLines(safelistPath)
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToHashSet();
var freshTargetsContent = Program.BuildTargetsFileContent(new SortedSet<string>(committedClasses, StringComparer.Ordinal));
var committedTargetsContent = File.ReadAllText(targetsPath);

Assert.True(freshTargetsContent == committedTargetsContent,
$"build/ShellUI.Components.targets is out of sync with wwwroot/shellui-classes.txt. " +
$"Regenerate with:\n {RegenerateCommand}");
}

private static string BuildDiffMessage(System.Collections.Generic.List<string> added, System.Collections.Generic.List<string> removed)
{
var msg = "Safelist is out of date.\n";
msg += "Regenerate with:\n dotnet run --project tools/ShellUI.SafelistGenerator -- src/ShellUI.Components/Components src/ShellUI.Components/wwwroot/shellui-classes.txt\n\n";
Comment on lines +88 to +89
if (added.Count > 0)
{
msg += $"New classes in razor sources missing from safelist (first {added.Count}):\n";
foreach (var c in added) msg += $" + {c}\n";
}
if (removed.Count > 0)
{
msg += $"\nClasses in safelist but no longer used in razor sources (first {removed.Count}):\n";
foreach (var c in removed) msg += $" - {c}\n";
}
return msg;
}

private static string GetThisFilePath([CallerFilePath] string path = "") => path;

private static string ResolveComponentsDir()
{
var testDir = Path.GetDirectoryName(GetThisFilePath())!;
return Path.GetFullPath(Path.Combine(testDir, "..", "src", "ShellUI.Components", "Components"));
}

private static string ResolveSafelistPath()
{
var testDir = Path.GetDirectoryName(GetThisFilePath())!;
return Path.GetFullPath(Path.Combine(testDir, "..", "src", "ShellUI.Components", "wwwroot", "shellui-classes.txt"));
}

private static string ResolveTargetsPath()
{
var testDir = Path.GetDirectoryName(GetThisFilePath())!;
return Path.GetFullPath(Path.Combine(testDir, "..", "src", "ShellUI.Components", "build", "ShellUI.Components.targets"));
}
}
1 change: 1 addition & 0 deletions ShellUI.Tests/ShellUI.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ProjectReference Include="..\src\ShellUI.Core\ShellUI.Core.csproj" />
<ProjectReference Include="..\src\ShellUI.Templates\ShellUI.Templates.csproj" />
<ProjectReference Include="..\src\ShellUI.CLI\ShellUI.CLI.csproj" />
<ProjectReference Include="..\tools\ShellUI.SafelistGenerator\ShellUI.SafelistGenerator.csproj" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions ShellUI.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
<Project Path="src/ShellUI.Core/ShellUI.Core.csproj" />
<Project Path="src/ShellUI.Templates/ShellUI.Templates.csproj" />
</Folder>
<Folder Name="/tools/">
<Project Path="tools/ShellUI.SafelistGenerator/ShellUI.SafelistGenerator.csproj" />
</Folder>
<Project Path="ShellUI.Tests/ShellUI.Tests.csproj" />
</Solution>
2 changes: 1 addition & 1 deletion docs/COMPONENT_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ ShellDocs will need:

- v0.1.0 (Dec 2025) - 73 components, CLI + NuGet
- v0.1.1 (Dec 2025) - Hotfix, package publishing
- **v0.3.0-alpha (Feb 2026) - Current alpha: Charts, 68 installable components, Tailwind 4.1.18**
- **v0.3.0-alpha (Feb 2026) - Current alpha: Charts, 68 installable components, Tailwind 4.3.1**
- v0.3.0 (Q1 2026) - ShellDocs components (CodeBlock, MDX, Callout, Steps, etc.)
- v0.4.0 (Q2 2026) - ShellDocs site launch
- v1.0.0 (Q2-Q3 2026) - .NET 10, stable API, comprehensive docs
6 changes: 3 additions & 3 deletions docs/PROJECT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- ✅ **NuGet Packages Published** - `ShellUI.Components`, `ShellUI.CLI`, `ShellUI.Templates`
- ✅ **Hybrid Distribution** - CLI + NuGet packages (best of both worlds!)
- ✅ **No Node.js Required** - Using Tailwind standalone CLI (zero JavaScript dependencies!)
- ✅ **Tailwind CSS v4.1.18** - Latest version with standalone CLI support
- ✅ **Tailwind CSS v4.3.1** - Latest version with standalone CLI support

## What's Complete ✅

Expand All @@ -19,7 +19,7 @@
- [x] Components Package (`ShellUI.Components`) - Published to NuGet
- [x] Templates Package (`ShellUI.Templates`) - Embedded in CLI
- [x] Component Registry - 167 templates (68 installable; rest are sub-components/variants/models/services that auto-install)
- [x] Tailwind CSS Integration - v4.1.18 standalone CLI support
- [x] Tailwind CSS Integration - v4.3.1 standalone CLI support
- [x] MSBuild Integration - Automatic CSS compilation

### Components (68 Installable)
Expand Down Expand Up @@ -78,7 +78,7 @@ Counts are top-level components users invoke directly via `shellui add <name>`.
| Milestone | Status |
|-----------|--------|
| v0.3.0-alpha - 68 installable components, CLI, NuGet | ✅ Done |
| Tailwind v4.1.18 standalone | ✅ Done |
| Tailwind v4.3.1 standalone | ✅ Done |
| Alpha testing & validation | 🔄 In progress |
| v0.3.0 stable (after testing) | 🎯 Next |
| v1.0 - Full release | 🎯 Target Q2-Q3 2026 |
Expand Down
7 changes: 4 additions & 3 deletions src/ShellUI.CLI/Services/InitService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ShellUI.Core;
using ShellUI.Core.Models;
using ShellUI.Templates;
using System.Text.Json;
Expand Down Expand Up @@ -109,7 +110,7 @@ await AnsiConsole.Status()
Tailwind = new TailwindConfig
{
Enabled = true,
Version = "4.1.18",
Version = TailwindConstants.Version,
Method = method,
CssPath = "wwwroot/app.css"
}
Expand Down Expand Up @@ -198,8 +199,8 @@ private static async Task SetupTailwindNpmAsync()

// Install Tailwind CSS packages (v4 with @tailwindcss/cli)
AnsiConsole.MarkupLine("[cyan]Installing Tailwind CSS packages...[/]");
await RunNpmCommandAsync("install", "-D", "tailwindcss@^4.1.18", "@tailwindcss/cli@^4.1.18");
AnsiConsole.MarkupLine("[green]Installed:[/] tailwindcss v4.1.18, @tailwindcss/cli");
await RunNpmCommandAsync("install", "-D", $"tailwindcss@{TailwindConstants.NpmRange}", $"@tailwindcss/cli@{TailwindConstants.NpmRange}");
AnsiConsole.MarkupLine($"[green]Installed:[/] tailwindcss v{TailwindConstants.Version}, @tailwindcss/cli");

// Create CSS files
AnsiConsole.MarkupLine("[cyan]Creating CSS files...[/]");
Expand Down
Loading
Loading