diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6257afb..015acb0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,39 @@ jobs:
- name: Restore dependencies
run: dotnet restore ShellUI.slnx
+ # Generates src/ShellUI.Components/wwwroot/shellui-all.css fresh on every run.
+ # File is gitignored — the Windows + Linux Tailwind CLI binaries produce
+ # byte-different output, so drift-checking a committed file failed spuriously.
+ # Instead we regenerate and validate content properties.
+ - name: Build precompiled CSS bundle
+ # Invoke via `bash` (not `./script.sh`) so we don't rely on the file's
+ # executable bit. Windows contributors can edit and commit without
+ # remembering `git update-index --chmod=+x`.
+ run: |
+ set -euxo pipefail
+ bash scripts/rebuild-precompiled-css.sh
+ BUNDLE=src/ShellUI.Components/wwwroot/shellui-all.css
+ test -f "$BUNDLE" || (echo "ERROR: bundle was not produced"; exit 1)
+
+ # Content assertions using grep -F (fixed-string, no regex escaping headaches).
+ # These catch classes of bugs we've actually shipped before — worth the paranoia.
+ grep -Fq -- '--background' "$BUNDLE" || (echo "ERROR: theme vars not in bundle"; exit 1)
+ grep -Fq '.bg-background' "$BUNDLE" || (echo "ERROR: core utility rule missing"; exit 1)
+ grep -Fq 'hover\:bg-accent' "$BUNDLE" || (echo "ERROR: hover: modifier rules missing"; exit 1)
+ grep -Fq 'state=open' "$BUNDLE" || (echo "ERROR: data-[state=open] arbitrary value not compiled"; exit 1)
+ grep -Fq 'px-2\.5' "$BUNDLE" || (echo "ERROR: Badge padding class px-2.5 missing (variant .cs helpers not scanned?)"; exit 1)
+ grep -Fq 'border-transparent' "$BUNDLE" || (echo "ERROR: border-transparent missing (variant .cs helpers not scanned?)"; exit 1)
+
+ # Size guard — well above the ~77KB current 68 components emit, tight
+ # enough to catch a runaway (someone disabling minify, dumping the whole
+ # tailwind base without tree-shake, etc.).
+ size=$(wc -c < "$BUNDLE")
+ echo "precompiled bundle size: ${size} bytes"
+ if [ "$size" -gt 150000 ]; then
+ echo "ERROR: bundle exceeded 150KB — investigate before shipping."
+ exit 1
+ fi
+
- name: Build
run: dotnet build ShellUI.slnx --no-restore --configuration Release
@@ -97,6 +130,13 @@ jobs:
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
+ # DIAGNOSTIC: dump what's actually inside the fresh nupkg. The precompiled
+ # bundle must ship as a static web asset for consumers to `` it.
+ echo "---nupkg contents (looking for shellui-all.css):---"
+ unzip -l "$LOCAL_FEED"/ShellUI.Components*.nupkg | grep -E 'shellui-all|staticwebassets/' | head -10 || echo " (no matches — bundle not in nupkg!)"
+ echo "---source wwwroot check:---"
+ ls -la "$GITHUB_WORKSPACE/src/ShellUI.Components/wwwroot/" | head -10
+
TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/app" && cd "$TMPDIR/app"
dotnet new blazor -o NuGetSmoke --no-restore
@@ -114,6 +154,14 @@ jobs:
EOF
+ # Evict every cached ShellUI.Components from ~/.nuget/packages before restore.
+ # The runner-level cache is restored from actions/cache and may contain a
+ # stale copy of the current version from an earlier CI run (before we added
+ # the precompiled bundle, .cs scan, etc.). NuGet prefers cached over local
+ # feeds when version matches, so a stale cache silently masks the fresh
+ # nupkg. Wildcard covers any version — future-proof.
+ rm -rf ~/.nuget/packages/shellui.components
+
dotnet add package ShellUI.Components --prerelease
# The safelist gets copied into wwwroot/ during the consumer's build by
@@ -133,6 +181,31 @@ jobs:
grep -q 'bg-background' "$SAFELIST" || (echo "safelist appears malformed — missing core Tailwind class 'bg-background'"; exit 1)
wc -l "$SAFELIST"
+ # In .NET 10, MapStaticAssets() doesn't physically copy static web assets
+ # from referenced packages into the consumer's bin/ during `dotnet build`
+ # — it uses a manifest and reads from the extracted NuGet cache at runtime.
+ # So the correct assertion is "does the file exist in the extracted package
+ # cache after restore", which proves it'll be served at
+ # `_content/ShellUI.Components/shellui-all.css` in the running app.
+ BUNDLE="$(find ~/.nuget/packages/shellui.components -name 'shellui-all.css' -type f | head -1)"
+ if [ -z "$BUNDLE" ]; then
+ echo "ERROR: shellui-all.css not in consumer's extracted NuGet cache — the pure-NuGet install path is broken."
+ echo "---consumer's extracted NuGet cache tree:---"
+ find ~/.nuget/packages/shellui.components -type f 2>/dev/null | head -30
+ exit 1
+ fi
+ echo "found bundle at: $BUNDLE"
+ BUNDLE_SIZE=$(wc -c < "$BUNDLE")
+ echo "bundle size: $BUNDLE_SIZE bytes"
+
+ # Spot-check a handful of representative rules using fixed-string grep
+ # (no regex escaping headaches). These catch classes of bugs we've hit.
+ grep -Fq -- '--background' "$BUNDLE" || (echo "ERROR: bundle missing --background theme var"; exit 1)
+ grep -Fq '.bg-background' "$BUNDLE" || (echo "ERROR: bundle missing .bg-background utility"; exit 1)
+ grep -Fq 'hover\:bg-accent' "$BUNDLE" || (echo "ERROR: bundle missing hover:bg-accent modifier rule"; exit 1)
+ 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)
+
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7325da4..823d79b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -36,6 +36,19 @@ jobs:
- name: Restore dependencies
run: dotnet restore ShellUI.slnx
+ # Generate the precompiled CSS bundle before build so releases always ship
+ # a fresh one. File is gitignored — regenerated on every CI/release run.
+ - name: Build precompiled CSS bundle
+ run: |
+ set -euxo pipefail
+ bash scripts/rebuild-precompiled-css.sh
+ BUNDLE=src/ShellUI.Components/wwwroot/shellui-all.css
+ test -f "$BUNDLE" || (echo "ERROR: bundle was not produced"; exit 1)
+ # Content sanity — same assertions ci.yml runs.
+ grep -q -- '--background:' "$BUNDLE" || (echo "ERROR: theme vars missing"; exit 1)
+ grep -q '\.bg-background' "$BUNDLE" || (echo "ERROR: utilities missing"; exit 1)
+ echo "release bundle size: $(wc -c < "$BUNDLE") bytes"
+
- name: Build
run: dotnet build ShellUI.slnx --no-restore --configuration Release
diff --git a/.gitignore b/.gitignore
index e3bf44c..db285ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -365,4 +365,9 @@ FodyWeavers.xsd
shellui-installation-tests/
# Preview publish output (testing artifacts)
-preview-publish/
\ No newline at end of file
+preview-publish/
+# Generated Tailwind bundle — regenerated in CI, not committed.
+# Windows and Linux Tailwind CLI produce byte-different output; committing
+# would cause spurious drift-check failures. Local devs: run
+# `bash scripts/rebuild-precompiled-css.sh` before running Path A tests.
+src/ShellUI.Components/wwwroot/shellui-all.css
diff --git a/README.md b/README.md
index d24c395..ec29254 100644
--- a/README.md
+++ b/README.md
@@ -315,46 +315,185 @@ ShellUI ships two NuGet packages — the CLI is the primary install path; the ru
## Installation
-Two paths — pick based on whether you already have Tailwind set up.
+**Four install paths.** Pick one — they cover every "shape" of Blazor / static project.
-### Path A — NuGet + your existing Tailwind setup (new in 0.4.x)
+### 🚦 Quick decision matrix
-If your project already builds Tailwind, install the runtime DLL via NuGet and add one line to your `input.css`:
+| I want to… | Use |
+|---|---|
+| Ship the fastest — one `` tag, no build config | **Path A** — NuGet + precompiled bundle |
+| Tree-shaken CSS, I already run Tailwind for my own utilities | **Path B** — NuGet + safelist |
+| Own the component source code, restyle by editing `.razor` files | **Path C** — CLI |
+| Prototype in a static HTML page or JSFiddle without NuGet at all | **Path D** — CDN |
+
+The four paths coexist cleanly. You can even mix them (use CLI for a few customized components and NuGet for the rest).
+
+---
+
+### Path A — NuGet + precompiled CSS bundle (simplest, new in 0.4.x)
+
+**Who it's for:** you want components on screen with minimum ceremony. No Tailwind config, no npm, no CLI. Just NuGet and a `` tag.
+
+**Setup (2 lines of code):**
+
+```bash
+dotnet add package ShellUI.Components
+```
+
+```razor
+@* App.razor
*@
+
+
+@* _Imports.razor *@
+@using ShellUI.Components
+```
+
+Done. `