Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fceafb5
feat: add shellui-all.css and update shellui-classes.txt with new Tai…
Shewart Jul 1, 2026
c4ba293
feat: expand installation options and enhance documentation for Shell…
Shewart Jul 1, 2026
fdd1b58
feat: add script to rebuild precompiled Tailwind CSS for ShellUI comp…
Shewart Jul 1, 2026
cd3e67b
feat: enhance safelist generation to include C# components
Shewart Jul 1, 2026
97e9873
refactor: update SafelistDriftTests to include C# source files in saf…
Shewart Jul 1, 2026
e07d430
feat: expand Tailwind safelist in ShellUI.Components.targets
Shewart Jul 1, 2026
b559709
feat: add verification for precompiled CSS bundle in CI and release w…
Shewart Jul 1, 2026
4806099
fix: update permissions for rebuild-precompiled-css.sh script
Shewart Jul 1, 2026
0003801
chore: update CI and release workflows to invoke rebuild-precompiled-…
Shewart Jul 1, 2026
b1d8e64
chore: update .gitignore and remove shellui-all.css
Shewart Jul 2, 2026
225574a
chore: update CI and release workflows to regenerate precompiled CSS …
Shewart Jul 2, 2026
07af080
chore: evict cached ShellUI.Components before package restore in CI w…
Shewart Jul 2, 2026
733b028
chore: enhance CI workflow with diagnostic checks for nupkg contents …
Shewart Jul 2, 2026
063d26c
chore: make mktemp usage portable in rebuild-precompiled-css.sh
Shewart Jul 2, 2026
ed07061
fix: update README and ShellUI.Components.targets for safelist clarity
Shewart Jul 2, 2026
c82d961
fix: clarify README and update shellui-classes.txt for Tailwind class…
Shewart Jul 2, 2026
1e7f1ab
refactor: streamline source file enumeration for Tailwind class safel…
Shewart Jul 3, 2026
bcfac3a
refactor: unify source file enumeration in SafelistDriftTests
Shewart Jul 3, 2026
6420a5d
fix: update README for clarity on shellui-classes.txt
Shewart Jul 3, 2026
c051335
chore: update CI workflow to reflect changes in static asset handling
Shewart Jul 3, 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
73 changes: 73 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 `<link>` 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
Expand All @@ -114,6 +154,14 @@ jobs:
</configuration>
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
Expand All @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,9 @@ FodyWeavers.xsd
shellui-installation-tests/

# Preview publish output (testing artifacts)
preview-publish/
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
171 changes: 155 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<link>` 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 `<link>` tag.

**Setup (2 lines of code):**

```bash
dotnet add package ShellUI.Components
```

```razor
@* App.razor <head> *@
<link href="_content/ShellUI.Components/shellui-all.css" rel="stylesheet" />

@* _Imports.razor *@
@using ShellUI.Components
```

Done. `<Button>`, `<Card>`, `<Dialog>` etc. all render fully styled.

**Benefits:**
- ✅ **Fastest to set up** — 30 seconds from `dotnet new blazor` to styled components
- ✅ **Zero build step for CSS** — the package ships a pre-compiled ~77KB bundle
- ✅ **No downloads at build time** — no Tailwind binary, no npm install
- ✅ **Deterministic** — the exact CSS you see locally is the exact CSS that ships
- ✅ **Works offline** — no network dep, ever
- ✅ **All 68 components styled** — regardless of which ones you use

**Trade-offs:**
- ❌ **No tree-shaking** — you pay ~77KB even if you only use 3 components (negligible for most sites)
- ❌ **Theming via override only** — theme vars are baked into the bundle. You customize by *overriding* CSS variables in a later `<style>` block, not by editing an `input.css`. Works fine for color tweaks; less flexible for structural theme changes.

**Best for:** new projects, prototypes, teams that don't already run Tailwind, "just get me components" scenarios.

---

### Path B — NuGet + your existing Tailwind setup (tree-shaken, new in 0.4.x)

**Who it's for:** your project already runs Tailwind for your own utility classes, and you want ShellUI's classes tree-shaken into the same compiled output.

**Setup:**

```bash
dotnet add package ShellUI.Components
```

The package ships a `shellui-classes.txt` safelist at `wwwroot/shellui-classes.txt` in your project. Wire Tailwind to scan it:
The package copies `shellui-classes.txt` (auto-generated safelist of every Tailwind class ShellUI uses) into your `wwwroot/`. Point Tailwind at it:

```css
/* wwwroot/input.css */
@import "tailwindcss";
@source "./shellui-classes.txt";

/* Your own theme variables — full source of truth, paste tweakcn output here */
:root {
--background: oklch(0.99 0 0);
--foreground: oklch(0 0 0);
/* ... */
}
```

```razor
@* _Imports.razor *@
@using 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.
**Benefits:**
- ✅ **Tree-shaken CSS** — only classes your compiled app *actually uses* are emitted (~30-50KB for a typical site)
- ✅ **Single Tailwind build** — ShellUI's classes and your own classes compile in one pass
- ✅ **Your `input.css` is the theme source of truth** — paste tweakcn output directly; no cascade tricks
- ✅ **Composable** — add your own utilities, `@apply`, custom `@theme` blocks freely

### Path B — CLI tool (full source control, no Tailwind setup required)
**Trade-offs:**
- ❌ Requires you to already have (or set up) a Tailwind build pipeline
- ❌ Slightly more setup than Path A

If you want zero Tailwind config of your own, or want to edit component source directly, use the CLI:
**Best for:** existing Blazor projects that already run Tailwind, teams that want CSS efficiency, projects with heavy custom styling on top of ShellUI.

---

### Path C — CLI (source ownership, shadcn-style)

**Who it's for:** you want to *own* the component source code. Edit any `.razor` file directly to change behavior or styling. This is the shadcn philosophy — you don't import components, you copy them.

**Setup:**

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

The CLI copies the `.razor` files into your project. You own them — edit them however you like.
`shellui init` automatically:
- Downloads Tailwind standalone CLI (no Node.js needed) — or uses your npm install if you prefer
- Writes the full default theme to `wwwroot/input.css` (`:root`, `.dark`, `@theme inline`)
- Patches `App.razor` with `@rendermode="InteractiveServer"`, theme bootstrap script, `shellui.js` script tag
- Sets up MSBuild integration — Tailwind rebuilds on every `dotnet build`

### Which one?
`shellui add <component>` copies the `.razor` source into `Components/UI/` in your project. Edit freely.

| 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)
**Benefits:**
- ✅ **Source-level ownership** — every component is your code, editable, versionable, forkable
- ✅ **Best theming experience** — you own `wwwroot/input.css`, edit directly, tweakcn pastes straight in
- ✅ **Zero external runtime deps** — Tailwind rebuilds on `dotnet build`; no network per build
- ✅ **Automatic host wiring** — `shellui init` patches App.razor for you
- ✅ **Composable with NuGet** — mix CLI-installed components with NuGet-provided ones

**Trade-offs:**
- ❌ First `shellui init` downloads ~25MB Tailwind binary (cached in `.shellui/bin/`)
- ❌ Component updates are manual — you own the source, so you also merge upstream changes

**Best for:** custom design systems, projects that heavily restyle ShellUI, teams that want everything in their own repo, anyone following the shadcn workflow.

---

### Path D — CDN (no NuGet, static HTML)

**Who it's for:** prototypes, blog embeds, JSFiddle/CodePen/CodeSandbox demos, static marketing pages, or any scenario where NuGet is overkill.

**Setup (one line):**

```html
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/shellui-dev/shellui@v0.4.0/dist/shellui-all.css" />
```

jsdelivr auto-serves any file from a tagged GitHub release. Free, global edge cache, no account needed.

Now write raw HTML with ShellUI's Tailwind classes:

```html
<button class="inline-flex items-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2">
Click me
</button>
```

**Benefits:**
- ✅ **Zero installation** — no `dotnet add`, no CLI, no npm
- ✅ **Zero build step** — one `<link>` tag and you're done
- ✅ **Global edge cache** — jsdelivr serves from the nearest datacenter
- ✅ **Works in any HTML context** — static sites, WordPress, MDX docs, any framework
- ✅ **Same bundle as Path A** — theming, size, features are identical

**Trade-offs:**
- ❌ **Runtime dep on jsdelivr** — extremely reliable but not zero-risk
- ❌ **No components** — this ships CSS only. For Blazor with components use Path A instead.
- ❌ **Needs network** — no good for airgapped deployments

**Best for:** landing pages, demos, "look at this cool ShellUI component" tweets, non-Blazor contexts.

---

### Theming across paths

All four paths use the same CSS variable system, so theming works uniformly — just with different edit surfaces:

| Path | Where the theme lives | How you edit it |
|---|---|---|
| A | Baked in `shellui-all.css` | Override CSS vars in a `<style>` block *after* the link |
| B | Your `wwwroot/input.css` | Edit directly, paste tweakcn output over `:root`/`.dark` blocks |
| C | `wwwroot/input.css` created by `shellui init` | Edit directly — Tailwind auto-rebuilds on `dotnet build` |
| D | Baked in the CDN-served CSS | Override in a `<style>` tag |

Change one CSS variable (e.g. `--primary`) and every component updates immediately. See each install-path README under `shellui-installation-tests/` for concrete theming examples.

### Configure Tailwind CSS manually (advanced)
Create/update `wwwroot/tailwind.config.js`:
Expand Down
8 changes: 5 additions & 3 deletions ShellUI.Tests/SafelistDriftTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ public class SafelistDriftTests
+ "src/ShellUI.Components/build/ShellUI.Components.targets";

[Fact]
public void Safelist_MatchesGeneratedFromCurrentRazorSources()
public void Safelist_MatchesGeneratedFromCurrentSources()
{
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);
// Use the same source enumeration the CLI uses (dual .razor + .cs, minus
// bin/obj). Diverging file lists would produce false-positive drifts.
var (razorFiles, csFiles) = Program.EnumerateSources(componentsDir);
var freshlyGenerated = Program.GenerateSafelist(razorFiles.Concat(csFiles));
var committed = File.ReadAllLines(safelistPath)
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToHashSet();
Expand Down
Loading
Loading