diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8c0864..6257afb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 + + 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: diff --git a/README.md b/README.md index ae51ab5..d24c395 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ 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) 🎉 @@ -44,7 +44,7 @@ ShellUI transforms Blazor component development with a hybrid approach: **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** @@ -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:** @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 | ✅ | @@ -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 ` 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`: @@ -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!** --- diff --git a/ShellUI.Tests/SafelistDriftTests.cs b/ShellUI.Tests/SafelistDriftTests.cs new file mode 100644 index 0000000..c4c0c61 --- /dev/null +++ b/ShellUI.Tests/SafelistDriftTests.cs @@ -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(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 added, System.Collections.Generic.List 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"; + 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")); + } +} diff --git a/ShellUI.Tests/ShellUI.Tests.csproj b/ShellUI.Tests/ShellUI.Tests.csproj index 83c2647..c32e66e 100644 --- a/ShellUI.Tests/ShellUI.Tests.csproj +++ b/ShellUI.Tests/ShellUI.Tests.csproj @@ -23,6 +23,7 @@ + diff --git a/ShellUI.slnx b/ShellUI.slnx index 0f42a6f..36ca80f 100644 --- a/ShellUI.slnx +++ b/ShellUI.slnx @@ -10,5 +10,8 @@ + + + diff --git a/docs/COMPONENT_ROADMAP.md b/docs/COMPONENT_ROADMAP.md index db9cc00..281e156 100644 --- a/docs/COMPONENT_ROADMAP.md +++ b/docs/COMPONENT_ROADMAP.md @@ -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 diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 3ccd43b..251701f 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -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 ✅ @@ -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) @@ -78,7 +78,7 @@ Counts are top-level components users invoke directly via `shellui add `. | 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 | diff --git a/src/ShellUI.CLI/Services/InitService.cs b/src/ShellUI.CLI/Services/InitService.cs index f6c6d90..f4d2815 100644 --- a/src/ShellUI.CLI/Services/InitService.cs +++ b/src/ShellUI.CLI/Services/InitService.cs @@ -1,3 +1,4 @@ +using ShellUI.Core; using ShellUI.Core.Models; using ShellUI.Templates; using System.Text.Json; @@ -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" } @@ -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...[/]"); diff --git a/src/ShellUI.CLI/Services/TailwindDownloader.cs b/src/ShellUI.CLI/Services/TailwindDownloader.cs index 23420b9..4a789b0 100644 --- a/src/ShellUI.CLI/Services/TailwindDownloader.cs +++ b/src/ShellUI.CLI/Services/TailwindDownloader.cs @@ -1,12 +1,13 @@ using System.IO.Compression; using System.Runtime.InteropServices; +using ShellUI.Core; using Spectre.Console; namespace ShellUI.CLI.Services; public class TailwindDownloader { - private const string TailwindVersion = "v4.1.18"; + private const string TailwindVersion = TailwindConstants.GitHubTag; private const string BaseUrl = "https://github.com/tailwindlabs/tailwindcss/releases/download"; public static async Task EnsureTailwindCliAsync(string projectRoot) diff --git a/src/ShellUI.Components/README.md b/src/ShellUI.Components/README.md index a24e278..dd69470 100644 --- a/src/ShellUI.Components/README.md +++ b/src/ShellUI.Components/README.md @@ -11,11 +11,37 @@ Beautiful, accessible Blazor components inspired by shadcn/ui. A CLI-first compo - 📱 **Responsive design** - Mobile-first approach - 🔧 **Fully customizable** - Copy components to your project for full control -## ⚠️ Read this first +## Two install paths -This package ships the component DLLs, JS interop, and helpers (`Shell.Cn`). **It does not produce styled components on its own.** Tailwind v4 builds CSS by scanning `.razor` source files at compile time — the component source lives inside this DLL, so Tailwind never sees the utility classes used inside ShellUI components. +Pick one based on whether you have Tailwind set up in your project. -The supported way to get styled components is the **`ShellUI.CLI` global tool**: +### Path A — NuGet + your existing Tailwind setup (new in 0.4.x) + +If your project already uses Tailwind, install the NuGet package and add one `@source` directive to your input.css. The package ships a `shellui-classes.txt` safelist that Tailwind scans at build time so utility classes used inside ShellUI components end up in your compiled CSS — tree-shaken, only what's used: + +```bash +dotnet add package ShellUI.Components +``` + +After restore, the safelist appears at `wwwroot/shellui-classes.txt` in your project. In your `wwwroot/input.css`: + +```css +@import "tailwindcss"; +@source "./shellui-classes.txt"; + +/* ... your theme variables, custom layers, etc. ... */ +``` + +Then in your `App.razor` or `_Host.cshtml`: +```html +@using ShellUI.Components +``` + +That's it. `