From b1e2c68fa353c0d5bc566008429c2edd8a4164a7 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 24 Sep 2026 14:25:46 +0200 Subject: [PATCH 01/12] refactor(CopyButtonTemplate): simplify clipboard copy logic by removing redundant try-catch block Streamlined the clipboard copy functionality in the CopyButtonTemplate by eliminating the nested try-catch structure. The code now directly invokes the `copyToClipboard` method, improving readability and maintainability. --- src/ShellUI.Templates/Templates/CopyButtonTemplate.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ShellUI.Templates/Templates/CopyButtonTemplate.cs b/src/ShellUI.Templates/Templates/CopyButtonTemplate.cs index cc533e7..79630a7 100644 --- a/src/ShellUI.Templates/Templates/CopyButtonTemplate.cs +++ b/src/ShellUI.Templates/Templates/CopyButtonTemplate.cs @@ -68,15 +68,7 @@ private async Task CopyAsync() try { - try - { - var module = await JSRuntime.InvokeAsync(""import"", ""./_content/ShellUI.Components/shellui.js""); - await module.InvokeVoidAsync(""copyToClipboard"", Text); - } - catch - { - await JSRuntime.InvokeVoidAsync(""ShellUI.copyToClipboard"", Text); - } + await JSRuntime.InvokeVoidAsync(""ShellUI.copyToClipboard"", Text); _copied = true; await OnCopied.InvokeAsync(); _ = Task.Run(async () => From bc875d5bb5f12ad57c3bbcfc589e3d16824eba5a Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 24 Sep 2026 14:26:39 +0200 Subject: [PATCH 02/12] feat(ComponentInstaller): enhance component installation logic and add ShellUI JS integration - Introduced a constant for the ShellUI JS sidebar API marker to facilitate checks during component installation. - Updated `InstallComponentForInitAsync` to return a boolean indicating success or failure, improving error handling. - Added `EnsureShellUiJs` method to ensure the ShellUI JS component is installed correctly, including updates to the configuration file when necessary. - Enhanced existing installation logic to handle cases where components already exist, providing feedback on updates and installations. - Improved overall readability and maintainability of the component installation process. --- .../Services/ComponentInstaller.cs | 185 +++++++++++++----- 1 file changed, 133 insertions(+), 52 deletions(-) diff --git a/src/ShellUI.CLI/Services/ComponentInstaller.cs b/src/ShellUI.CLI/Services/ComponentInstaller.cs index b82ee64..a9835ec 100644 --- a/src/ShellUI.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.CLI/Services/ComponentInstaller.cs @@ -7,6 +7,8 @@ namespace ShellUI.CLI.Services; public class ComponentInstaller { + private const string ShellUiJsSidebarApiMarker = "initSidebar: function (handle, dotNetRef)"; + public static async Task InstallComponents(string[] components, bool force) { var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); @@ -108,26 +110,33 @@ await AnsiConsole.Status() AnsiConsole.MarkupLine($"[red]Failed: {string.Join(", ", failedComponents)}[/]"); } - public static Task InstallComponentForInitAsync(string componentName, ProjectInfo projectInfo) + public static Task InstallComponentForInitAsync( + string componentName, + ProjectInfo projectInfo, + ShellUIConfig? config = null) { var metadata = ComponentRegistry.Components.GetValueOrDefault(componentName.ToLower()); - if (metadata == null) return Task.CompletedTask; + if (metadata == null) return Task.FromResult(false); - var config = new ShellUIConfig + var installConfig = config ?? new ShellUIConfig { ComponentsPath = "Components/UI", ProjectType = projectInfo.ProjectType, Style = "default" }; - var result = InstallComponentInternal(componentName, config, projectInfo, false); + var result = InstallComponentInternal(componentName, installConfig, projectInfo, false); if (result == InstallResult.Success) { AnsiConsole.MarkupLine($"[green]✅ Installed:[/] {componentName}"); } + else if (result == InstallResult.Failed) + { + AnsiConsole.MarkupLine($"[red]Failed to install:[/] {componentName}"); + } - return Task.CompletedTask; + return Task.FromResult(result != InstallResult.Failed); } public static void InstallComponent(string componentName, ComponentMetadata metadata, bool force, bool skipConfig = false) @@ -135,22 +144,36 @@ public static void InstallComponent(string componentName, ComponentMetadata meta var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); var configJson = File.ReadAllText(configPath); var config = JsonSerializer.Deserialize(configJson); - + if (config == null) return; - + var projectInfo = ProjectDetector.DetectProject(); var result = InstallComponentInternal(componentName, config, projectInfo, force); - - if (!skipConfig && result == InstallResult.Success) + + if (!skipConfig && result != InstallResult.Failed) { var updatedJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(configPath, updatedJson); } } - private static InstallResult InstallComponent(string componentName, ShellUIConfig config, ProjectInfo projectInfo, bool force) + public static bool EnsureShellUiJs() { - return InstallComponentInternal(componentName, config, projectInfo, force); + var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); + if (!File.Exists(configPath)) return false; + + var config = JsonSerializer.Deserialize(File.ReadAllText(configPath)); + if (config == null) return false; + + var metadata = ComponentRegistry.GetMetadata("shellui-js"); + if (metadata == null) return false; + + var result = InstallComponentInternal("shellui-js", config, ProjectDetector.DetectProject(), force: false); + if (result == InstallResult.Failed) return false; + + var updatedJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(configPath, updatedJson); + return true; } private static InstallResult InstallComponentInternal(string componentName, ShellUIConfig config, ProjectInfo projectInfo, bool force) @@ -168,66 +191,92 @@ private static InstallResult InstallComponentInternal(string componentName, Shel return InstallResult.Failed; } + var content = ComponentRegistry.GetComponentContent(componentName); + if (content == null) + { + AnsiConsole.MarkupLine($"[red]Failed to get content for '{componentName}'[/]"); + return InstallResult.Failed; + } + var basePath = metadata.IsLayoutBlock ? Path.Combine(Directory.GetCurrentDirectory(), config.LayoutPath ?? "Components/Layout") : Path.Combine(Directory.GetCurrentDirectory(), config.ComponentsPath); var componentPath = Path.GetFullPath(Path.Combine(basePath, metadata.FilePath)); - - // Check if already exists + var existing = config.InstalledComponents.FirstOrDefault(c => c.Name == componentName); + if (File.Exists(componentPath) && !force) { + if (componentName.Equals("shellui-js", StringComparison.OrdinalIgnoreCase) && + !File.ReadAllText(componentPath).Contains(ShellUiJsSidebarApiMarker, StringComparison.Ordinal)) + { + if (existing?.IsCustomized == true) + { + AnsiConsole.MarkupLine("[yellow]shellui.js is customized and does not expose the sidebar API; update it before installing Sidebar.[/]"); + return InstallResult.Failed; + } + + content = content.Replace("YourProjectNamespace", projectInfo.RootNamespace); + File.WriteAllText(componentPath, content); + RecordInstalledComponent(config, metadata, componentName, resetCustomization: true); + AnsiConsole.MarkupLine($"[green]Updated '{componentName}'[/] [dim](added sidebar interop)[/]"); + return InstallResult.Success; + } + + RecordInstalledComponent(config, metadata, componentName, resetCustomization: false); AnsiConsole.MarkupLine($"[yellow]Skipped '{componentName}' (already exists)[/]"); return InstallResult.Skipped; } - // Get component content - var content = ComponentRegistry.GetComponentContent(componentName); - if (content == null) - { - AnsiConsole.MarkupLine($"[red]Failed to get content for '{componentName}'[/]"); - return InstallResult.Failed; - } - - // Replace namespace placeholder content = content.Replace("YourProjectNamespace", projectInfo.RootNamespace); - // Ensure directory exists var directory = Path.GetDirectoryName(componentPath); if (directory != null) { Directory.CreateDirectory(directory); } - // Write file File.WriteAllText(componentPath, content); + RecordInstalledComponent(config, metadata, componentName, resetCustomization: true); + AnsiConsole.MarkupLine($"[green]Installed '{componentName}'[/] [dim]({metadata.FilePath})[/]"); + return InstallResult.Success; + } - // Update config + private static void RecordInstalledComponent( + ShellUIConfig config, + ComponentMetadata metadata, + string componentName, + bool resetCustomization) + { var existing = config.InstalledComponents.FirstOrDefault(c => c.Name == componentName); if (existing != null) { existing.Version = metadata.Version; - existing.InstalledAt = DateTime.UtcNow; - existing.IsCustomized = false; - } - else - { - config.InstalledComponents.Add(new InstalledComponent - { - Name = componentName, - Version = metadata.Version, - InstalledAt = DateTime.UtcNow, - IsCustomized = false - }); + if (resetCustomization) existing.IsCustomized = false; + return; } - AnsiConsole.MarkupLine($"[green]Installed '{componentName}'[/] [dim]({metadata.FilePath})[/]"); - return InstallResult.Success; + config.InstalledComponents.Add(new InstalledComponent + { + Name = componentName, + Version = metadata.Version, + InstalledAt = DateTime.UtcNow, + IsCustomized = false + }); } - private static void InstallComponentWithDependencies(string componentName, ShellUIConfig config, ProjectInfo projectInfo, bool force, HashSet installedSet, HashSet requestedPackages, List pendingNuGetDeps, ref int successCount, ref int skippedCount, List failedComponents) + private static bool InstallComponentWithDependencies( + string componentName, + ShellUIConfig config, + ProjectInfo projectInfo, + bool force, + HashSet installedSet, + HashSet requestedPackages, + List pendingNuGetDeps, + ref int successCount, + ref int skippedCount, + List failedComponents) { - if (installedSet.Contains(componentName)) - return; // Already processed + if (installedSet.Contains(componentName)) return true; if (!ComponentRegistry.Exists(componentName)) { @@ -238,7 +287,7 @@ private static void InstallComponentWithDependencies(string componentName, Shell AnsiConsole.MarkupLine($"[yellow]Did you mean '[bold]{suggestion}[/]'?[/]"); } failedComponents.Add(componentName); - return; + return false; } var metadata = ComponentRegistry.GetMetadata(componentName); @@ -246,25 +295,34 @@ private static void InstallComponentWithDependencies(string componentName, Shell { AnsiConsole.MarkupLine($"[red]Failed to get metadata for '{componentName}'[/]"); failedComponents.Add(componentName); - return; + return false; } - // Install dependencies first if (metadata.Dependencies?.Any() == true) { AnsiConsole.MarkupLine($"[dim]Installing dependencies for [bold]{componentName}[/]: {string.Join(", ", metadata.Dependencies)}[/]"); foreach (var dep in metadata.Dependencies) { - if (!installedSet.Contains(dep)) + if (!installedSet.Contains(dep) && + !InstallComponentWithDependencies( + dep, + config, + projectInfo, + force, + installedSet, + requestedPackages, + pendingNuGetDeps, + ref successCount, + ref skippedCount, + failedComponents)) { - InstallComponentWithDependencies(dep, config, projectInfo, force, installedSet, requestedPackages, pendingNuGetDeps, ref successCount, ref skippedCount, failedComponents); + failedComponents.Add(componentName); + return false; } } } - // Install the component itself var result = InstallComponentInternal(componentName, config, projectInfo, force); - if (result == InstallResult.Success) { successCount++; @@ -278,11 +336,10 @@ private static void InstallComponentWithDependencies(string componentName, Shell else { failedComponents.Add(componentName); + return false; } - // Collect NuGet deps regardless of source-file install result — even a `Skipped` - // file still requires its NuGet packages to compile. - if (result != InstallResult.Failed && metadata.NuGetDependencies?.Any() == true) + if (metadata.NuGetDependencies?.Any() == true) { foreach (var pkg in metadata.NuGetDependencies) { @@ -292,6 +349,8 @@ private static void InstallComponentWithDependencies(string componentName, Shell } } } + + return true; } private static async Task InstallNuGetDependenciesAsync(ProjectInfo projectInfo, List deps) @@ -341,6 +400,28 @@ private static async Task InstallNuGetDependenciesAsync(ProjectInfo projectInfo, } } + internal static bool IsShellUiJsCompatible() + { + var metadata = ComponentRegistry.GetMetadata("shellui-js"); + var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); + if (metadata == null || !File.Exists(configPath)) return false; + + try + { + var config = JsonSerializer.Deserialize(File.ReadAllText(configPath)); + if (config == null) return false; + + var basePath = Path.Combine(Directory.GetCurrentDirectory(), config.ComponentsPath); + var shellUiPath = Path.GetFullPath(Path.Combine(basePath, metadata.FilePath)); + return File.Exists(shellUiPath) && + File.ReadAllText(shellUiPath).Contains(ShellUiJsSidebarApiMarker, StringComparison.Ordinal); + } + catch + { + return false; + } + } + // Returns the href that should appear in the host's tag, or null if the // component's FilePath isn't a CSS asset under wwwroot/. Strips the `../../wwwroot/` // prefix that asset templates use to escape Components/UI/. From 6d43e54534a13ed393528c541abd3366583227e0 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Thu, 24 Sep 2026 14:27:34 +0200 Subject: [PATCH 03/12] feat(tests): add new tests for sidebar interop and relative JS module imports - Introduced `RelativeJsModuleImportTests` to ensure templates do not dynamically import relative JS modules, which can lead to 404 errors when compiled into Razor Class Libraries. - Added `SidebarInteropTests` to verify that the sidebar provider uses global lifecycle interop and that the sidebar JS is retained only as a hidden legacy alias. - Updated `TemplateCompileTests` to include the `sidebar-provider` component, ensuring comprehensive coverage of Razor template parsing. --- ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs | 98 +++++++++++++++++++ ShellUI.Tests/TemplateCompileTests.cs | 1 + 2 files changed, 99 insertions(+) diff --git a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs index a99bde6..e97891f 100644 --- a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs +++ b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs @@ -1,4 +1,6 @@ +using System.IO; using System.Linq; +using System.Text.RegularExpressions; using ShellUI.Templates; using Xunit; @@ -160,6 +162,102 @@ public void NoTemplate_DependsOnExternalIconFont(string cssClass) } } +public class RelativeJsModuleImportTests +{ + // A component whose C# does JSRuntime.InvokeAsync("import", "./foo.js") resolves that + // path against the current page URL. That only works when ShellUI is installed straight + // into the host app; the moment the generated component is compiled into a consumer's + // own Razor Class Library, the asset is served from _content// instead and the + // import 404s — silently, since every one of these calls is wrapped in try/catch. + // ShellUI's established fix for this shape of bug (see ThemeToggle, InputOTP, + // CommandPalette, Combobox, ...) is to route through the already-loaded global + // `window.ShellUI` object (shellui.js, loaded via one host-controlled - + ``` -**Pros:** Instant setup, zero config. -**Cons:** Full Tailwind (~500KB+), no purging, **not for production**. Use for demos, prototypes, or Playground-style apps only. - -For Blazor, add this to `App.razor` or your HTML host. Note: Design tokens (CSS variables) still need to be in your CSS for components to look correct. - -## Method 4: Using npm (Alternative) +## Method 3: npm and `@tailwindcss/cli` -If you prefer using npm and Node.js: +Use npm when the project already has a Node.js toolchain or needs npm-based Tailwind integrations. -### Step 1: Install Tailwind CSS +### Install the packages -```bash -npm install -D tailwindcss -npx tailwindcss init +```text +npm install -D tailwindcss@4.3.2 @tailwindcss/cli@4.3.2 ``` -### Step 2: Update tailwind.config.js - -```javascript -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - './Components/**/*.{razor,html,cshtml}', - './Pages/**/*.{razor,html,cshtml}', - './wwwroot/**/*.html', - ], - darkMode: 'class', - theme: { - extend: { - // ... same theme configuration as above - }, - }, - plugins: [], -} +Tailwind v4 does not require a separate initialization command. Create or edit `wwwroot/input.css` directly: + +```css +@import "tailwindcss"; +@custom-variant dark (&:is(.dark *)); ``` -### Step 3: Create input.css +Tailwind v4 can discover source files from the project. If a JavaScript config is required for compatibility or plugins, load it explicitly from the CSS entry point: ```css -@import 'tailwindcss/base'; -@import 'tailwindcss/components'; -@import 'tailwindcss/utilities'; +@import "tailwindcss"; +@config "../tailwind.config.js"; ``` -*Note: This creates a minimal Tailwind setup. Add your custom CSS variables, colors, and design tokens as needed.* +Place a JavaScript config at the project root when using that example. Keep the theme variables and `@theme inline` mappings in the CSS entry point. -### Step 4: Build CSS +### Build with `@tailwindcss/cli` -```bash -npx tailwindcss -i wwwroot/input.css -o wwwroot/app.css +```text +npx @tailwindcss/cli -i wwwroot/input.css -o wwwroot/app.css ``` -## Verification +Minify release output with: -After setup, verify everything works: - -1. **Build the project:** - ```bash - dotnet build - ``` - -2. **Check for Tailwind CSS output:** - - `wwwroot/app.css` should contain generated CSS - - Look for Tailwind utility classes in the output +```text +npx @tailwindcss/cli -i wwwroot/input.css -o wwwroot/app.css --minify +``` -3. **Test in browser:** - - Add some Tailwind classes to your components - - Verify they're styled correctly +For MSBuild, create a target that invokes the package: -## Troubleshooting +```xml + + + + npx + $(MSBuildProjectDirectory)\wwwroot\input.css + $(MSBuildProjectDirectory)\wwwroot\app.css + --minify + + -### Common Issues: + + + + + + +``` -1. **Tailwind CSS not building:** - - Check that `tailwind.config.js` exists - - Verify content paths include your Razor files - - Ensure MSBuild targets are imported +Use one Tailwind build path per project: either the standalone target or the npm target. Running both can produce competing writes to `wwwroot/app.css`. -2. **Styles not applying:** - - Check that `app.css` is linked in your layout - - Verify the CSS file is being generated - - Clear browser cache +## Method 4: Play CDN for prototypes -3. **Build errors:** - - Ensure Tailwind CLI is executable - - Check file paths in configuration - - Verify MSBuild targets syntax +The browser build is useful for a quick prototype only: -### Getting Help: +```html + + + +``` -- Check the [Tailwind CSS documentation](https://tailwindcss.com/docs) -- Review the [ShellUI documentation](https://shellui.dev) -- Open an issue on [GitHub](https://github.com/shellui-dev/shellui/issues) +It does not replace a version-pinned local build for production. Keep the application's theme variables and component CSS available when testing a prototype. -## Custom Themes & Fonts +## Verification -### 🎨 Using Custom Themes from tweakcn +After setup: -You can customize your theme similar to shadcn/ui. Copy theme configurations from [tweakcn](https://tweakcn.com/) or similar tools and paste them into your `wwwroot/input.css`. +```text +dotnet build +``` -**Example - Adding a custom theme:** +Check that: -1. Visit [tweakcn](https://tweakcn.com/) -2. Customize colors, fonts, radius, etc. -3. Copy the generated CSS -4. Paste it into your `wwwroot/input.css` (replace the existing `:root` and `.dark` sections) +- `wwwroot/app.css` contains generated utilities. +- The host includes the generated stylesheet. +- A Razor component using a ShellUI utility is styled in the browser. +- The standalone executable is at `.shellui/bin/tailwindcss.exe` on Windows or `.shellui/bin/tailwindcss` on macOS/Linux. +- The npm project can run `npx @tailwindcss/cli -i wwwroot/input.css -o wwwroot/app.css`. -### 🔤 Installing Custom Fonts +## Troubleshooting -To use custom fonts in your theme: +### Tailwind CSS is not building -#### Method 1: Google Fonts (Recommended) -```html - - - - -``` +- Confirm `wwwroot/input.css` contains `@import "tailwindcss";`. +- Confirm the standalone executable is in the project-local `.shellui/bin` directory. +- Confirm `Build/ShellUI.targets` is imported by the project. +- Confirm the input and output paths exist. +- For npm, confirm `tailwindcss@4.3.2` and `@tailwindcss/cli@4.3.2` are installed and run `npx @tailwindcss/cli --help`. -Then update your `input.css`: -```css -:root { - --font-sans: 'Kode Mono', ui-monospace, monospace; - /* ... other variables ... */ -} -``` +### Styles are not applying +- Confirm `app.css` is linked from the Blazor host. +- Rebuild after changing `input.css` or Razor markup. +- Clear the browser cache while testing. +- Check the browser console for missing JavaScript assets separately from CSS generation. -#### Method 2: Local Font Files -```css -/* Add to your input.css */ -@import url('./fonts/kode-mono.css'); /* Your local font CSS */ +### Version or syntax errors -:root { - --font-sans: 'Kode Mono', ui-monospace, monospace; - /* ... other variables ... */ -} -``` +- Use Tailwind `4.3.2` for both the standalone download and the npm packages. +- Use `@import "tailwindcss";`, not the v3 layer imports. +- Use `npx @tailwindcss/cli` with the version-pinned packages and CSS entry point. -#### Method 3: Font CDN -```html - - -``` +## Custom themes and fonts -### 📝 Font Fallbacks +Tailwind v4 themes are CSS-first. Keep the color variables in `input.css`, then map them into Tailwind with `@theme inline`: -Always include fallbacks in your font definitions: ```css -:root { - --font-sans: 'Custom Font', ui-sans-serif, system-ui, sans-serif; - --font-mono: 'Custom Mono', ui-monospace, 'SF Mono', monospace; - --font-serif: 'Custom Serif', ui-serif, serif; -} -``` - -This ensures your design looks good even if the custom font fails to load. - -### 🎯 Theme Examples +@import "tailwindcss"; -**Dark Theme with Custom Colors:** -```css :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - /* ... customize all colors ... */ + --background: oklch(0.99 0 0); + --foreground: oklch(0 0 0); + --primary: oklch(0.55 0.22 264.53); + --primary-foreground: oklch(1 0 0); + --radius: 0.5rem; } .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - /* ... dark mode colors ... */ + --background: oklch(0 0 0); + --foreground: oklch(1 0 0); + --primary: oklch(0.81 0.17 75.35); + --primary-foreground: oklch(0 0 0); } -``` -**Custom Border Radius:** -```css -:root { - --radius: 0.75rem; /* More rounded */ +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --radius-lg: var(--radius); } ``` -**Custom Shadows:** +A theme tool such as tweakcn can provide a replacement `:root` and `.dark` block. Paste the generated variables into `input.css`, then rebuild Tailwind. + +For fonts, use a project-local stylesheet or a font provider and retain fallbacks: + ```css :root { - --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --font-sans: 'Custom Sans', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Custom Mono', ui-monospace, monospace; } ``` -## Next Steps +## Next steps -Once Tailwind CSS is set up: +1. Initialize the project with `shellui init`. +2. Choose the standalone or npm Tailwind method. +3. Add components with `shellui add`. +4. Customize the variables in `wwwroot/input.css`. +5. Build the project and verify the generated stylesheet. -1. **Add ShellUI components** (if using ShellUI) using `dotnet shellui add` -2. **Customize the design system** in `input.css` (use tweakcn for themes!) -3. **Install custom fonts** for the perfect look -4. **Build your Blazor app** with beautiful components -5. **Deploy** using your preferred hosting solution +Useful references: -Happy coding! 🚀 +- [Tailwind CSS documentation](https://tailwindcss.com/docs) +- [Tailwind CSS v4 upgrade guide](https://tailwindcss.com/docs/upgrade-guide) +- [ShellUI documentation](https://shellui.dev) +- [ShellUI GitHub repository](https://github.com/shellui-dev/shellui) From fde588820dac42799306de72879367cbad5dc29d Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Sat, 26 Sep 2026 14:00:21 +0200 Subject: [PATCH 11/12] refactor(ci): improve validation checks for ShellUI initialization and bundle size - Updated CI workflow to enhance validation for ShellUI initialization, ensuring that the correct JavaScript files are referenced and that the sidebar integration is properly handled. - Revised size guard comments for clarity and adjusted the bundle size threshold to prevent runaway outputs. - Added checks to confirm the presence of necessary functions in the global ShellUI JavaScript and to ensure legacy sidebar JS is not included in fresh installs. --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 015acb0..1b55eaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,9 +53,8 @@ jobs: 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 guard — keep the threshold above the current component bundle, + # but below runaway output (for example, an unminified full Tailwind base). size=$(wc -c < "$BUNDLE") echo "precompiled bundle size: ${size} bytes" if [ "$size" -gt 150000 ]; then @@ -91,7 +90,10 @@ jobs: grep -q 'Routes @rendermode="InteractiveServer"' Components/App.razor || (echo "init did not patch Routes @rendermode"; exit 1) grep -q 'ShellUI theme bootstrap' Components/App.razor || (echo "init did not inject theme bootstrap"; exit 1) grep -q '' Components/App.razor || (echo "init did not inject shellui.js script tag"; exit 1) - grep -q 'shellui-sidebar.js' Components/App.razor && (echo "init incorrectly injected shellui-sidebar.js script tag (sidebar JS is dynamically imported)"; exit 1) || true + if grep -Fq 'shellui-sidebar.js' Components/App.razor; then + echo "init incorrectly injected a shellui-sidebar.js script tag (sidebar JS lives in the global shellui.js now)" + exit 1 + fi # Assert input.css has the full theme, not just @import "tailwindcss"; grep -q '@theme inline' wwwroot/input.css || (echo "init did not write full theme to input.css"; exit 1) @@ -114,6 +116,22 @@ jobs: test -f wwwroot/css/charts.css || (echo "shellui add chart did not install chart-styles CSS"; exit 1) grep -q '/, not the root. + grep -q 'ShellUI.initSidebar' Components/UI/SidebarProvider.razor || (echo "SidebarProvider did not call ShellUI.initSidebar"; exit 1) + if grep -Fq 'shellui-sidebar.js' Components/UI/SidebarProvider.razor; then + echo "SidebarProvider still dynamically imports shellui-sidebar.js" + exit 1 + fi + + test -f wwwroot/shellui.js || (echo "shellui.js was not installed"; exit 1) + grep -Fq 'initSidebar: function (handle, dotNetRef)' wwwroot/shellui.js || (echo "shellui.js does not expose initSidebar"; exit 1) + grep -Fq 'disposeSidebar: function (handle)' wwwroot/shellui.js || (echo "shellui.js does not expose disposeSidebar"; exit 1) + test ! -e wwwroot/shellui-sidebar.js || (echo "legacy sidebar JS was installed for a fresh sidebar install"; exit 1) + dotnet build -c Debug # Pure-NuGet install path — `dotnet add package ShellUI.Components` without From 7d442b450a6bd1d0da812cbf9eb025967e45ec69 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Sat, 26 Sep 2026 14:00:52 +0200 Subject: [PATCH 12/12] fix(tests): update safelist regeneration command in test messages - Modified the safelist drift test to include an additional target file in the regeneration command, ensuring accurate instructions for updating the safelist. - This change enhances the clarity of the error message when the safelist is out of date, providing users with the correct command to regenerate the safelist. --- ShellUI.Tests/SafelistDriftTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShellUI.Tests/SafelistDriftTests.cs b/ShellUI.Tests/SafelistDriftTests.cs index f50503b..3c84242 100644 --- a/ShellUI.Tests/SafelistDriftTests.cs +++ b/ShellUI.Tests/SafelistDriftTests.cs @@ -88,7 +88,7 @@ public void GeneratedTargetsFile_EmbedsSameClassesAsSafelist() 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"; + msg += "Regenerate with:\n dotnet run --project tools/ShellUI.SafelistGenerator -- src/ShellUI.Components/Components src/ShellUI.Components/wwwroot/shellui-classes.txt src/ShellUI.Components/build/ShellUI.Components.targets\n\n"; if (added.Count > 0) { msg += $"New classes in razor sources missing from safelist (first {added.Count}):\n";