From 897b6332c1cd49a8cfe9bd2f166aaba283046b76 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 10:34:34 +0200 Subject: [PATCH 01/11] fix(cli): read component versions from the assembly An installed tool has no Directory.Build.props next to it, so every component was recorded as the 0.1.0 fallback and 'update' could never see a newer version. The build already stamps the package version into the assembly; read it from there. --- .../Models/ComponentMetadata.cs | 39 +++---------------- .../ComponentRegistryTests.cs | 17 ++++++++ 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/src/ShellUI.Native.Core/Models/ComponentMetadata.cs b/src/ShellUI.Native.Core/Models/ComponentMetadata.cs index f631fa8..498946e 100644 --- a/src/ShellUI.Native.Core/Models/ComponentMetadata.cs +++ b/src/ShellUI.Native.Core/Models/ComponentMetadata.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Reflection; namespace ShellUI.Native.Core.Models; @@ -28,40 +28,11 @@ public class ComponentMetadata // Searchable tags for the component public List Tags { get; set; } = new(); + // Directory.Build.props stamps the package version into the assembly; an installed tool has no props file to read. private static string GetCurrentVersion() { - // Try to read version from Directory.Build.props - try - { - var currentDir = AppDomain.CurrentDomain.BaseDirectory; - var dir = new DirectoryInfo(currentDir); - - while (dir != null) - { - var propsFile = Path.Combine(dir.FullName, "Directory.Build.props"); - if (File.Exists(propsFile)) - { - var content = File.ReadAllText(propsFile); - var match = Regex.Match(content, @"([^<]+)"); - if (match.Success) - { - var version = match.Groups[1].Value.Trim(); - var suffixMatch = Regex.Match(content, @"([^<]*)"); - if (suffixMatch.Success && !string.IsNullOrEmpty(suffixMatch.Groups[1].Value.Trim())) - { - version += "-" + suffixMatch.Groups[1].Value.Trim(); - } - return version; - } - } - dir = dir.Parent; - } - } - catch - { - // Ignore errors, return fallback - } - - return "0.1.0"; + var informational = typeof(ComponentMetadata).Assembly + .GetCustomAttribute()?.InformationalVersion; + return string.IsNullOrEmpty(informational) ? "0.0.0" : informational.Split('+')[0]; } } diff --git a/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs b/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs index 9423174..7ca17f6 100644 --- a/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs +++ b/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs @@ -125,4 +125,21 @@ public void GetSupportedPlatforms_returns_empty_for_unknown_component() var supported = ComponentRegistry.GetSupportedPlatforms("nonexistent-xyz"); Assert.Empty(supported); } + + // The installed tool has no Directory.Build.props, so the version must reach it through the assembly. + [Fact] + public void Component_version_is_the_package_version_from_Directory_Build_props() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null && !File.Exists(Path.Combine(dir.FullName, "Directory.Build.props"))) + dir = dir.Parent; + Assert.NotNull(dir); + + var props = File.ReadAllText(Path.Combine(dir!.FullName, "Directory.Build.props")); + var version = System.Text.RegularExpressions.Regex.Match(props, "([^<]+)<").Groups[1].Value; + var suffix = System.Text.RegularExpressions.Regex.Match(props, "([^<]*)<").Groups[1].Value; + var expected = suffix.Length == 0 ? version : $"{version}-{suffix}"; + + Assert.All(ComponentRegistry.Components.Values, m => Assert.Equal(expected, m.Version)); + } } From 429067bca890d647c50d052164f9269253e5ddda Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 10:34:35 +0200 Subject: [PATCH 02/11] fix(cli): install the whole family when a part is added Parts such as dialog-trigger look up their parent type at runtime, and parents depend on their parts, so 17 parts did not compile when added alone. 'add ' now installs the part's family instead. The install summary also printed nothing, because the success and skip counters were passed by value. --- docs/COMPONENTS.md | 4 ++ .../Services/ComponentInstaller.cs | 41 +++++++++++-------- .../ComponentRegistry.cs | 13 ++++++ .../ComponentRegistryTests.cs | 31 ++++++++++++++ 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index ba39daa..47a380f 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -1158,6 +1158,10 @@ shellui-native list --available Dependencies install automatically. Almost every component depends on `shell` (theme tokens and core helpers); components that draw icons also depend on `icon`. +Parts of a compositional family (`dialog-trigger`, `accordion-item`, `card-header`, โ€ฆ) find their +parent at runtime, so they only build next to it. Adding a part on its own installs the whole +family: `shellui-native add dialog-trigger` installs `dialog` and all its parts. + | Component | Auto-installs | |-----------|---------------| | button | shell, icon, button-variants | diff --git a/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs b/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs index 73b8550..32f6faa 100644 --- a/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs @@ -30,15 +30,19 @@ public static async Task InstallComponents(string[] components, bool force) var projectInfo = ProjectDetector.DetectProject(); - // Parse comma-separated components + // Parse comma-separated components; a part installs its whole family var componentList = new List(); - foreach (var comp in components) + foreach (var comp in components.SelectMany(c => c.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))) { - componentList.AddRange(comp.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + var family = ComponentRegistry.GetFamily(comp); + if (family != null) + AnsiConsole.MarkupLine($"[dim]'{comp}' is part of '{family}', installing '{family}'[/]"); + var name = family ?? comp; + if (!componentList.Contains(name)) + componentList.Add(name); } - var successCount = 0; - var skippedCount = 0; + var tally = new InstallTally(); var failedComponents = new List(); var installedSet = new HashSet(); @@ -63,8 +67,8 @@ await AnsiConsole.Status() { ctx.Status($"Installing {componentName}..."); await InstallComponentWithDependenciesAsync( - componentName, config, projectInfo, force, - installedSet, successCount, skippedCount, failedComponents); + componentName, config, projectInfo, force, + installedSet, tally, failedComponents); } }); @@ -74,10 +78,10 @@ await InstallComponentWithDependenciesAsync( // Summary AnsiConsole.MarkupLine(""); - if (successCount > 0) - AnsiConsole.MarkupLine($"[green]Installed {successCount} component(s) successfully![/]"); - if (skippedCount > 0) - AnsiConsole.MarkupLine($"[yellow]Skipped {skippedCount} component(s) (already exists, use --force to overwrite)[/]"); + if (tally.Success > 0) + AnsiConsole.MarkupLine($"[green]Installed {tally.Success} component(s) successfully![/]"); + if (tally.Skipped > 0) + AnsiConsole.MarkupLine($"[yellow]Skipped {tally.Skipped} component(s) (already exists, use --force to overwrite)[/]"); if (failedComponents.Count > 0) AnsiConsole.MarkupLine($"[red]Failed: {string.Join(", ", failedComponents)}[/]"); } @@ -88,8 +92,7 @@ private static async Task InstallComponentWithDependenciesAsync( ProjectInfo projectInfo, bool force, HashSet installedSet, - int successCount, - int skippedCount, + InstallTally tally, List failedComponents) { if (installedSet.Contains(componentName)) @@ -118,7 +121,7 @@ private static async Task InstallComponentWithDependenciesAsync( { if (!installedSet.Contains(dep)) { - await InstallComponentWithDependenciesAsync(dep, config, projectInfo, force, installedSet, successCount, skippedCount, failedComponents); + await InstallComponentWithDependenciesAsync(dep, config, projectInfo, force, installedSet, tally, failedComponents); } } } @@ -128,12 +131,12 @@ private static async Task InstallComponentWithDependenciesAsync( if (result == InstallResult.Success) { - successCount++; + tally.Success++; installedSet.Add(componentName); } else if (result == InstallResult.Skipped) { - skippedCount++; + tally.Skipped++; installedSet.Add(componentName); } else @@ -216,4 +219,10 @@ private enum InstallResult Skipped, Failed } + + private sealed class InstallTally + { + public int Success; + public int Skipped; + } } diff --git a/src/ShellUI.Native.Templates/ComponentRegistry.cs b/src/ShellUI.Native.Templates/ComponentRegistry.cs index 1c3e2c2..84960e3 100644 --- a/src/ShellUI.Native.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Native.Templates/ComponentRegistry.cs @@ -172,4 +172,17 @@ public static bool Exists(string componentName) { return _templates.ContainsKey(componentName.ToLower()); } + + /* The family a compositional part belongs to (`dialog-trigger` โ†’ `dialog`), or null. + Parts find their parent type at runtime and parents depend on their parts, so a part + only builds when its family is installed. */ + public static string? GetFamily(string componentName) + { + var name = componentName.ToLower(); + return _templates + .Where(t => name.StartsWith(t.Key + "-") && t.Value.Meta.Dependencies.Contains(name)) + .Select(t => t.Key) + .OrderByDescending(k => k.Length) + .FirstOrDefault(); + } } diff --git a/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs b/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs index 7ca17f6..2da6951 100644 --- a/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs +++ b/tests/ShellUI.Native.Tests/ComponentRegistryTests.cs @@ -126,6 +126,37 @@ public void GetSupportedPlatforms_returns_empty_for_unknown_component() Assert.Empty(supported); } + [Theory] + [InlineData("dialog-trigger", "dialog")] + [InlineData("dialog-close", "dialog")] + [InlineData("drawer-content", "drawer")] + [InlineData("sheet-trigger", "sheet")] + [InlineData("dropdown-item", "dropdown")] + [InlineData("popover-trigger", "popover")] + [InlineData("hover-card-trigger", "hover-card")] + [InlineData("radio-group-item", "radio-group")] + [InlineData("collapsible-trigger", "collapsible")] + [InlineData("accordion-item", "accordion")] + [InlineData("tabs-list", "tabs")] + [InlineData("card-header", "card")] + public void GetFamily_maps_a_part_to_its_parent(string part, string family) + { + Assert.Equal(family, ComponentRegistry.GetFamily(part)); + } + + [Theory] + [InlineData("dialog")] + [InlineData("alert-dialog")] + [InlineData("toggle-group")] + [InlineData("tag-input")] + [InlineData("date-picker")] + [InlineData("hover-card")] + [InlineData("nonexistent-xyz")] + public void GetFamily_is_null_for_standalone_components(string name) + { + Assert.Null(ComponentRegistry.GetFamily(name)); + } + // The installed tool has no Directory.Build.props, so the version must reach it through the assembly. [Fact] public void Component_version_is_the_package_version_from_Directory_Build_props() From 1136db55fef1342817ca121ba3ba008026871d9b Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 10:34:44 +0200 Subject: [PATCH 03/11] ci(release): build the CLI and tests, check the tag version, use the release notes The workflow restored and built the bare src/ folder, which has no project file, so no release could have succeeded. It now builds the CLI and test projects, fails when the tag doesn't match the Directory.Build.props version, runs the tests, and uses the version's section of docs/RELEASE_NOTES.md as the GitHub release body. prepare-release.ps1 packs only the CLI; Core and Templates ship inside it. --- .gitattributes | 1 + .github/workflows/release.yml | 64 ++++++++++++++++++-------------- prepare-release.ps1 | 44 +++++++++------------- scripts/extract-release-notes.sh | 28 ++++++++++++++ 4 files changed, 83 insertions(+), 54 deletions(-) create mode 100644 .gitattributes create mode 100644 scripts/extract-release-notes.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00fec8d..e22d85a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,14 @@ name: Release on: push: tags: - - 'v*.*.*' + - 'v*' + +permissions: + contents: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false jobs: publish: @@ -13,48 +20,51 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Extract release notes for this version + run: bash scripts/extract-release-notes.sh "${{ steps.version.outputs.VERSION }}" > "$RUNNER_TEMP/release-body.md" + - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 10.0.x - - name: Extract version from tag - id: version - run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + - name: Check package version matches tag + run: | + v=$(dotnet msbuild src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj -getProperty:Version) + if [ "$v" != "${{ steps.version.outputs.VERSION }}" ]; then + echo "::error::The CLI builds version $v but the tag is v${{ steps.version.outputs.VERSION }}. Update Directory.Build.props." + exit 1 + fi - - name: Restore dependencies - run: dotnet restore src/ + - name: Restore dependencies (src + tests) + run: | + dotnet restore src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj + dotnet restore tests/ShellUI.Native.Tests/ShellUI.Native.Tests.csproj - name: Build - run: dotnet build src/ --no-restore --configuration Release + run: | + dotnet build src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj --no-restore --configuration Release + dotnet build tests/ShellUI.Native.Tests/ShellUI.Native.Tests.csproj --no-restore --configuration Release + + - name: Run tests + run: dotnet test tests/ShellUI.Native.Tests/ShellUI.Native.Tests.csproj --no-restore --no-build --configuration Release + # Core and Templates ship inside the CLI tool package; only the CLI is published. - name: Pack CLI tool - run: dotnet pack src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj --no-restore --configuration Release -o ./nupkg + run: dotnet pack src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj --no-build --configuration Release -p:ContinuousIntegrationBuild=true -o ./nupkg - name: Publish CLI to NuGet - run: | - dotnet nuget push ./nupkg/ShellUI.Native.CLI.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + run: dotnet nuget push ./nupkg/ShellUI.Native.CLI.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: name: ShellUI Native v${{ steps.version.outputs.VERSION }} - body: | - ## ShellUI Native v${{ steps.version.outputs.VERSION }} - - ### Installation - ```bash - dotnet tool install -g ShellUI.Native.CLI - ``` - - ### Quick Start - ```bash - cd YourMAUIProject - shellui-native init --yes - shellui-native add button input card - ``` - - See the [documentation](https://github.com/shellui-dev/shellui-native) for usage. + body_path: ${{ runner.temp }}/release-body.md draft: false prerelease: ${{ contains(github.ref, '-') }} files: | diff --git a/prepare-release.ps1 b/prepare-release.ps1 index c7b6f27..ab43261 100644 --- a/prepare-release.ps1 +++ b/prepare-release.ps1 @@ -22,40 +22,30 @@ $content = $content -replace '[^<]* [notes-file] +set -euo pipefail + +version="${1:?usage: extract-release-notes.sh [notes-file]}" +file="${2:-docs/RELEASE_NOTES.md}" + +if ! awk -v ver="v$version" ' + { line = $0; sub(/\r$/, "", line) } + line ~ /^```/ { fence = !fence } + !fence && line ~ /^# ShellUI Native v/ { + if (found) exit + split(line, parts, " ") + if (parts[4] == ver) found = 1 + } + found { buf[++n] = $0 } + END { + if (!found) exit 1 + while (n > 0) { + last = buf[n]; sub(/\r$/, "", last) + if (last ~ /^[[:space:]]*(---)?[[:space:]]*$/) n--; else break + } + for (i = 1; i <= n; i++) print buf[i] + } +' "$file"; then + echo "No '# ShellUI Native v$version' section in $file โ€” add release notes before tagging." >&2 + exit 1 +fi From 3eb1c5de80b68aab098e498e2f38442a72b82d7d Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 10:34:44 +0200 Subject: [PATCH 04/11] chore(release): v0.1.0-alpha.1 --- Directory.Build.props | 4 +-- docs/RELEASE_NOTES.md | 57 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 docs/RELEASE_NOTES.md diff --git a/Directory.Build.props b/Directory.Build.props index 5a98a54..fbff96b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,8 +2,8 @@ - 0.0.1 - + 0.1.0 + alpha.1 diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md new file mode 100644 index 0000000..e75e0a0 --- /dev/null +++ b/docs/RELEASE_NOTES.md @@ -0,0 +1,57 @@ +# ShellUI Native Release Notes + +# ShellUI Native v0.1.0-alpha.1 ๐Ÿงช + +> The first public prerelease of ShellUI Native: shadcn-style, copy-and-own components for .NET MAUI, with the same design tokens as [ShellUI](https://shellui.dev/) for Blazor. It is a prerelease, so install with `--prerelease`. Report issues via [GitHub Issues](https://github.com/shellui-dev/shellui-native/issues). + +## ๐Ÿ“ฆ Install + +```bash +dotnet tool install -g ShellUI.Native.CLI --prerelease + +cd YourMauiApp +shellui-native init --yes +shellui-native add button input card +``` + +Requires the .NET 10 SDK and a .NET MAUI project. Components are plain C# files written to `Components/UI/`; edit them freely. + +## โœจ Components + +58 component families, 89 CLI targets. Parts such as `dialog-trigger` belong to a family; adding one installs the whole family. + +| Category | Components | +|---|---| +| Form | `button`, `input`, `label`, `textarea`, `checkbox`, `switch`, `radio-group`, `select`, `combobox`, `multi-select`, `slider`, `date-picker`, `time-picker`, `calendar`, `input-otp`, `number-input`, `tag-input`, `toggle`, `toggle-group` | +| Layout | `card`, `separator`, `accordion`, `collapsible`, `scroll-area`, `aspect-ratio`, `wrap-layout` | +| Feedback | `alert`, `callout`, `toast`, `spinner`, `skeleton`, `progress` | +| Overlay | `dialog`, `alert-dialog`, `drawer`, `sheet`, `dropdown`, `popover`, `hover-card`, `tooltip`, `context-menu` | +| Navigation | `tabs`, `breadcrumb`, `pagination`, `stepper`, `tree-view`, `link-card` | +| Data display | `badge`, `avatar`, `table`, `empty-state`, `stat-card`, `timeline`, `carousel`, `kbd` | +| Utility | `icon`, `theme-toggle`, `copy-button` | + +Compositional families (`dialog`, `drawer`, `sheet`, `dropdown`, `popover`, `hover-card`, `accordion`, `collapsible`, `tabs`, `breadcrumb`, `card`, `radio-group`) follow the shadcn Trigger/Content pattern and install their parts automatically. + +## ๐ŸŽจ Design system + +- **Theme tokens:** `ShellTheme` holds light and dark palettes keyed like ShellUI's CSS variables (`Background`, `Primary`, `Border`, โ€ฆ) and publishes them as `ShellUI` / `ShellUIBrush` resources. Switching theme repaints every component; tokens can be overridden at startup. +- **Sizing:** every single-line form control is 40px high, matching shadcn's `h-10`. +- **Icons:** about 110 Lucide icons (from ShellIcons) drawn with MAUI shapes, with no icon font or package. + +## ๐ŸชŸ Overlays + +- Dialogs, drawers, sheets, menus, selects, tooltips, hover cards and toasts float in a page-level layer (`ShellPortal`), so you can declare them next to their trigger, inside a `ScrollView`, and they are never clipped. +- Select, Combobox, Date Picker and Time Picker are custom-drawn, with no native picker chrome. +- Escape on Windows and the back button on Android close the overlay on top (`ShellDismiss`). +- Optional `ShellTheme.SyncSystemBars` makes the Android status and navigation bars follow the theme. + +## ๐Ÿงฐ CLI + +`shellui-native init`, `add`, `list`, `remove` and `update`. `init` writes the `shell` utility, `shellui-native.json` and a theme resource dictionary; `add` resolves dependencies and replaces the namespace with your project's. + +## โš ๏ธ Known limitations + +- **MAUI only.** Avalonia is the next phase; WinUI 3 is conditional. On those projects `init` and `add` say the components have no template for that platform yet. +- Tested on Windows and Android. iOS and Mac Catalyst build but have not been run on a device; Escape does not close overlays on Mac Catalyst yet. +- Very large pages on Windows can hit WinUI's layout-pass limit (`Layout cycle detected`). Split the page or keep sections hidden until needed; see [COMPONENTS.md](https://github.com/shellui-dev/shellui-native/blob/main/docs/COMPONENTS.md#long-pages-on-windows). +- The docs site (native.shellui.dev) is not live yet; see [COMPONENTS.md](https://github.com/shellui-dev/shellui-native/blob/main/docs/COMPONENTS.md) for usage. From 9b7395f3d12a9ab9202115edabdbb20af28b564f Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 10:34:45 +0200 Subject: [PATCH 05/11] docs: refresh install steps, examples, roadmap and plan for the first prerelease Install with --prerelease, since only a prerelease is published. README and QUICKSTART examples now use the real API (no Variant="Primary" or CardTitle), QUICKSTART loads the theme, and the roadmap and development plan match what is merged. Avalonia target is 12.x. --- docs/ARCHITECTURE.md | 12 +++++- docs/BLAZOR_HYBRID.md | 2 +- docs/COMPONENTS_ROADMAP.md | 70 ++++++++++++-------------------- docs/DEVELOPMENT_PLAN.md | 43 ++++++++++++++------ docs/PLAN.md | 14 +++---- docs/QUICKSTART.md | 40 +++++++++++++----- docs/README.md | 24 +++++------ src/ShellUI.Native.CLI/README.md | 14 +++++-- 8 files changed, 126 insertions(+), 93 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6c1108f..d10dab4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -201,7 +201,15 @@ Not implemented yet โ€” this illustrates the target shape once Template System v All packages share a single version defined in `Directory.Build.props`: ```xml -0.0.1 +0.1.0 +alpha.1 ``` -Component metadata reads this version at runtime, ensuring consistency across all installed components. +The build stamps it into the assemblies, and component metadata reads it from there at runtime, +so `shellui-native.json` records the CLI version each component was installed with. + +**Releasing:** set the version in `Directory.Build.props`, add a `# ShellUI Native v` +section to [RELEASE_NOTES.md](./RELEASE_NOTES.md), merge to `main`, then push a `v` tag. +`.github/workflows/release.yml` checks that the tag matches the props version, runs the tests, +publishes `ShellUI.Native.CLI` to NuGet and creates the GitHub release (a prerelease when the +version has a `-`) with the notes as its body. diff --git a/docs/BLAZOR_HYBRID.md b/docs/BLAZOR_HYBRID.md index e64822f..e1eb5e7 100644 --- a/docs/BLAZOR_HYBRID.md +++ b/docs/BLAZOR_HYBRID.md @@ -65,7 +65,7 @@ shellui init --yes shellui add button input card # Install ShellUI Native (for native MAUI controls) -dotnet tool install -g ShellUI.Native.CLI +dotnet tool install -g ShellUI.Native.CLI --prerelease shellui-native init --yes shellui-native add button dialog ``` diff --git a/docs/COMPONENTS_ROADMAP.md b/docs/COMPONENTS_ROADMAP.md index 8b0553d..86b4bfd 100644 --- a/docs/COMPONENTS_ROADMAP.md +++ b/docs/COMPONENTS_ROADMAP.md @@ -2,9 +2,9 @@ Prioritized list of components to create for ShellUI Native, aligned with [ShellUI Components](https://github.com/shellui/shell-ui) patterns. All components should follow **compositional patterns** using Dependencies (parent + sub-components) instead of monolithic ChildContent. -Last revised: **2026-09-27** (theme tokens, icons and component polish on `feat/p3-navigation-layout`). +Last revised: **2026-10-07** (first prerelease, `0.1.0-alpha.1`). -**Status at a glance:** P0 โœ… done ยท P1 โœ… done ยท P2 โœ… done ยท **P3 โญ๏ธ next (`feat/p3-navigation-layout`)** ยท P4โ€“P7 backlog ยท Avalonia (Phase 2) unblocked, sequenced after P3 to avoid a moving target. +**Status at a glance:** P0โ€“P5 โœ… done ยท P6 partly done (resizable, navbar, sidebar open) ยท P7 partly done ยท shipped as `0.1.0-alpha.1` (58 families, 89 CLI targets) ยท **Avalonia (Phase 2) โญ๏ธ next**. --- @@ -142,29 +142,29 @@ Icons: `python scripts/generate-icons.py` regenerates `Icon.cs` from the ShellIc | Component | Status | Dependencies | ShellUI Ref | Notes | |-----------|--------|--------------|-------------|-------| | **textarea** | โœ… Done | โ€” | Textarea | Multi-line text input | -| **select** | โœ… Done | โ€” | Select | Native `Picker` wrapper at 40px baseline. Trigger/Content split deferred until custom-dropdown variant is needed | +| **select** | โœ… Done | โ€” | Select | Custom-drawn trigger + floating list at the 40px baseline (native `Picker` dropped 2026-09-27) | | **slider** | โœ… Done | โ€” | Slider | Range input | | **radio-group** | โœ… Done | radio-group-item | RadioGroup | Radio button group. Item checked-border fixed 2026-08-29 | -| **date-picker** | โœ… Done | โ€” | DatePicker | Date selection | -| **time-picker** | โœ… Done | โ€” | TimePicker | Time selection | +| **date-picker** | โœ… Done | calendar | DatePicker | Custom trigger + floating `calendar` (2026-10-02) | +| **time-picker** | โœ… Done | โ€” | TimePicker | Custom trigger + hour / minute / AM-PM columns (2026-10-04) | --- -### P3 โ€” Medium (Navigation & Layout) โญ๏ธ Next โ€” `feat/p3-navigation-layout` +### P3 โ€” Medium (Navigation & Layout) โœ… Done โ€” merged in PR #3 *See [DEVELOPMENT_PLAN.md ยง Phase 1c](./DEVELOPMENT_PLAN.md) for scope, order, and exit criteria.* | Component | Priority | Dependencies | ShellUI Ref | Notes | |-----------|----------|--------------|-------------|-------| -| **collapsible** | P3.1 (build first โ€” primitive) | collapsible-trigger, collapsible-content, element-extensions | Collapsible | Single expand/collapse โ€” the `IsOpen` + animation primitive `accordion-item` composes on top of. | -| **accordion** | P3.2 | accordion-item, accordion-trigger, accordion-content, element-extensions | Accordion | Multiple sections. `Type=Single` (radio-style) or `Multiple`. | -| **tabs** | P3.3 | tabs-list, tabs-trigger, tabs-content, element-extensions | Tabs | Tab bar + one visible panel. Trigger MUST render at the 40px baseline (row-level control). | -| **breadcrumb** | P3.4 | breadcrumb-item | Breadcrumb | Nav trail with separator between items. | -| **skeleton** | P3.5 | โ€” | Skeleton | Animated grey block, sized by parent โ€” use for perceived-perf on lists. | -| **scroll-area** | P3.6 | โ€” | ScrollArea | `ScrollView` wrapper. Cross-platform scrollbar styling is thin โ€” consider whether this is worth the wrapper vs documenting native `ScrollView`. | +| **collapsible** โœ… | P3.1 (build first โ€” primitive) | collapsible-trigger, collapsible-content, element-extensions | Collapsible | Single expand/collapse โ€” the `IsOpen` + animation primitive `accordion-item` composes on top of. | +| **accordion** โœ… | P3.2 | accordion-item, accordion-trigger, accordion-content, element-extensions | Accordion | Multiple sections. `Type=Single` (radio-style) or `Multiple`. | +| **tabs** โœ… | P3.3 | tabs-list, tabs-trigger, tabs-content, element-extensions | Tabs | Tab bar + one visible panel. Trigger MUST render at the 40px baseline (row-level control). | +| **breadcrumb** โœ… | P3.4 | breadcrumb-item | Breadcrumb | Nav trail with separator between items. | +| **skeleton** โœ… | P3.5 | โ€” | Skeleton | Animated grey block, sized by parent โ€” use for perceived-perf on lists. | +| **scroll-area** โœ… | P3.6 | โ€” | ScrollArea | `ScrollView` wrapper. Cross-platform scrollbar styling is thin โ€” consider whether this is worth the wrapper vs documenting native `ScrollView`. | --- -### P4 โ€” Medium (Feedback & Overlays) +### P4 โ€” Medium (Feedback & Overlays) โœ… Done | Component | Priority | Dependencies | ShellUI Ref | Notes | |-----------|----------|--------------|-------------|-------| | **tooltip** โœ… | P4.1 | shell | Tooltip | Done 2026-10-02 (floats in the page layer) | @@ -175,7 +175,7 @@ Icons: `python scripts/generate-icons.py` regenerates `Icon.cs` from the ShellIc --- -### P5 โ€” Lower (Data Display) +### P5 โ€” Lower (Data Display) โœ… Done | Component | Priority | Dependencies | ShellUI Ref | Notes | |-----------|----------|--------------|-------------|-------| | **avatar** โœ… | P5.1 | shell, icon | Avatar | Done 2026-09-27 | @@ -187,7 +187,7 @@ Icons: `python scripts/generate-icons.py` regenerates `Icon.cs` from the ShellIc --- -### P6 โ€” Lower (Advanced) +### P6 โ€” Lower (Advanced) โ€” partly done | Component | Priority | Dependencies | ShellUI Ref | Notes | |-----------|----------|--------------|-------------|-------| | **context-menu** โœ… | P6.1 | shell, icon, element-extensions | ContextMenu | Done 2026-10-05 โ€” one template; opens at the pointer on right-click, under the trigger on long-press | @@ -219,9 +219,7 @@ Icons: `python scripts/generate-icons.py` regenerates `Icon.cs` from the ShellIc | link-card โœ… | โ€” | Done 2026-10-05 โ€” tappable card that can open a URL | | aspect-ratio โœ… | โ€” | Done 2026-10-05 | | wrap-layout โœ… | โ€” | Done 2026-10-05 โ€” wrapping layout used by tag-input and the demo's button rows | -| theme-toggle | P7 | Already in demo; consider as component | -| copy-button | P7 | Copy to clipboard | -| toggle | P7 | Toggle button (vs Switch) | +| theme-toggle โœ… | P7 | Done โ€” `ThemeToggle` component | --- @@ -287,28 +285,14 @@ public static T? FindParentOfType(this Element element) where T : Element ## Order of Implementation -**Shipped on `main`** (Phase 1a + 1b โ€” see [DEVELOPMENT_PLAN.md](./DEVELOPMENT_PLAN.md)): -P0 (15 components) โ†’ P1 (14 components across dialog / drawer / sheet / dropdown / popover -families) โ†’ P2 (6 components including textarea, select, slider, radio-group, date-picker, -time-picker). Total 44 templates, all platform-keyed via Template System v2. - -**Up next โ€” `feat/p3-navigation-layout`** (Phase 1c). Build the primitive before the -composite: - -1. **collapsible** + collapsible-trigger, collapsible-content โ€” establishes the `IsOpen` + - animation pattern the rest of the tier reuses -2. **accordion** + accordion-item, accordion-trigger, accordion-content โ€” accordion-item - is collapsible with sibling coordination via `FindParentOfType()` -3. **tabs** + tabs-list, tabs-trigger, tabs-content โ€” same show/hide pattern as accordion - but always exactly one visible -4. **breadcrumb** + breadcrumb-item โ€” standalone, no shared state -5. **skeleton** โ€” standalone, animation primitive worth locking early so P5 (avatar, - table, empty-state) can compose it -6. **scroll-area** โ€” decide during implementation whether it's worth the wrapper vs - documenting native `ScrollView` - -**After Phase 1c โ†’ Phase 2 (Avalonia).** Do not open Phase 2 until Phase 1c merges โ€” the -whole point of finishing P3 on MAUI first is to freeze the vocabulary before it doubles. - -**Then Phase 1d / P4+ (feedback, data display, advanced).** Sequenced after Avalonia has -Avalonia-parity on P0/P1 at least, so new MAUI components don't get too far ahead again. +**Shipped in `0.1.0-alpha.1`** (see [DEVELOPMENT_PLAN.md](./DEVELOPMENT_PLAN.md) and +[RELEASE_NOTES.md](./RELEASE_NOTES.md)): P0โ€“P5, P6 context-menu / carousel / stepper, and the +P7 items marked โœ… above โ€” 58 families, 89 CLI targets, MAUI only. + +**Next โ€” Phase 2 (Avalonia).** Port in the same order as MAUI (P0 โ†’ P1 first), reusing the +component APIs, tokens and sizing contract. MAUI is far ahead, so new MAUI components wait +until Avalonia has P0 + P1. + +**MAUI backlog after that:** resizable, navbar, sidebar (P6); chart, file-upload, +date-range-picker (P7); and the ShellUI Blazor targets not yet ported (button-group, +input-group, command / command-palette, menubar, navigation-menu, data-table, โ€ฆ). diff --git a/docs/DEVELOPMENT_PLAN.md b/docs/DEVELOPMENT_PLAN.md index c57a4c5..e17bea8 100644 --- a/docs/DEVELOPMENT_PLAN.md +++ b/docs/DEVELOPMENT_PLAN.md @@ -4,7 +4,7 @@ Living document tracking **branches**, **phases**, and **feature implementations ShellUI Native. Companion to [PLAN.md](./PLAN.md) (long-term strategy) and [COMPONENTS_ROADMAP.md](./COMPONENTS_ROADMAP.md) (prioritized component backlog). -Last revised: **2026-08-30** (Phase 1c in flight, P4 polish scoped). +Last revised: **2026-10-07** (Phase 1e merged; `0.1.0-alpha.1` release prep). --- @@ -16,9 +16,9 @@ Short-lived sub-branches (`feat//`) cut off the phase branch and m into it, not directly into `main`. ``` -main โ† Phase 1a (2026-07-05), 1b (2026-08-29, PR #2), 1c + 1d (2026-10-05, PR #3) merged - โ””โ”€ feat/p5-p6-components โ† Phase 1e (active) โ€” MAUI P5/P6 tier (table, context menu, carousel, stepper, โ€ฆ) - โ””โ”€ feat/avalonia-implementation โ† Phase 2 (planned) โ€” Avalonia templates + reference impl +main โ† Phase 1a (2026-07-05), 1b (PR #2), 1c + 1d (PR #3), 1e (PR #4), brand mark (PR #5) merged + โ””โ”€ chore/release-v0.1.0-alpha.1 โ† first prerelease (active) + โ””โ”€ feat/avalonia-implementation โ† Phase 2 (next) โ€” Avalonia templates + reference impl โ””โ”€ feat/winui โ† Phase 3 (conditional) ``` @@ -35,8 +35,9 @@ main โ† Phase 1a (2026-07-05), 1b (2026-08-29, PR #2), 1c + 1d (2026-10-05, P | Branch | Base | Status | Purpose | |--------|------|--------|---------| -| `main` | โ€” | Phase 1aโ€“1d merged | 76 platform-keyed components, theme tokens, overlay portal, xUnit 408/408 | -| `feat/p5-p6-components` | `main` | **Active** | Phase 1e โ€” MAUI P5/P6 (table, context-menu, carousel, stepper; 80 components, xUnit 428/428) | +| `main` | โ€” | Phase 1aโ€“1e merged | 92 registry entries (89 CLI targets, 58 families), MAUI only | +| `chore/release-v0.1.0-alpha.1` | `main` | **Active** | Version `0.1.0-alpha.1`, fixed release workflow, release notes, docs refresh, per-component CLI check | +| `feat/avalonia-implementation` | `main` | Next | Phase 2 | --- @@ -379,7 +380,7 @@ Date/Time pickers inside a themed border with the native frame stripped. --- -## Phase 1e โ€” `feat/p5-p6-components` (active) +## Phase 1e โ€” `feat/p5-p6-components` (**merged via [PR #4](https://github.com/shellui-dev/shellui-native/pull/4)**) The remaining MAUI data-display and advanced tiers, built demo-first like Phase 1d. @@ -402,9 +403,9 @@ The remaining MAUI data-display and advanced tiers, built demo-first like Phase and the tag-input keyboard / height - [x] `multi-select`, `tree-view`, `copy-button`, `link-card`, `aspect-ratio` โ€” run on Windows and Android -- [ ] iOS / Mac Catalyst pass for everything -- [ ] Escape closes the overlay on top on Mac Catalyst -- [ ] `resizable`, `navbar`, `sidebar` (P6.4โ€“P6.6) +- [ ] iOS / Mac Catalyst pass for everything (carried over) +- [ ] Escape closes the overlay on top on Mac Catalyst (carried over) +- [ ] `resizable`, `navbar`, `sidebar` (P6.4โ€“P6.6) (moved to the MAUI backlog, after Phase 2 starts) Each family ships as one template (e.g. `table` holds Table, TableHeader, TableRow, TableHead and TableCell) rather than one template per part. @@ -417,15 +418,31 @@ and TableCell) rather than one template per part. --- -## Phase 2 โ€” `feat/avalonia-implementation` (queued after Phase 1e) +## Release `0.1.0-alpha.1` โ€” `chore/release-v0.1.0-alpha.1` (active) + +First NuGet prerelease of `ShellUI.Native.CLI`, MAUI only. + +- [x] `Directory.Build.props` โ†’ `0.1.0` + `alpha.1` +- [x] Component versions read from the assembly (an installed tool has no `Directory.Build.props`) +- [x] `release.yml` builds the CLI and tests (not the bare `src/` folder), checks the tag against + the props version, runs the tests and uses [RELEASE_NOTES.md](./RELEASE_NOTES.md) as the + GitHub release body (`scripts/extract-release-notes.sh`) +- [x] Install docs use `--prerelease`; roadmap and plan brought up to date +- [x] Every CLI target added alone to a fresh MAUI library, plus all together in a fresh MAUI app + +After merge: tag `v0.1.0-alpha.1` on `main` and push the tag. + +--- + +## Phase 2 โ€” `feat/avalonia-implementation` (next, after `0.1.0-alpha.1`) Cross-desktop (Windows + macOS + Linux) from one XAML codebase. ### Deliverables -- [ ] New `src/ShellUI.Native.Avalonia/` reference project (Avalonia 11.x) +- [ ] New `src/ShellUI.Native.Avalonia/` reference project (Avalonia 12.x) - [ ] Avalonia design-token `ResourceDictionary` mirroring [`StyleTemplates.cs`](../src/ShellUI.Native.Templates/StyleTemplates.cs) -- [ ] Add `AvaloniaContent` to each of the ~40 templates, using the +- [ ] Add Avalonia content to each template (P0 + P1 first), using the `TemplatedControl`/`StyledProperty` pattern documented in [ARCHITECTURE.md ยง Component Pattern (Avalonia)](./ARCHITECTURE.md) - [ ] `examples/Avalonia.Demo/` reference app mirroring the MAUI demo diff --git a/docs/PLAN.md b/docs/PLAN.md index 7ec579b..67d1124 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -25,8 +25,8 @@ ShellUI Native brings the same beautiful, accessible, and customizable component ### Phase 1: MAUI (Primary Focus) - **Target:** .NET MAUI (iOS, Android, Windows, macOS) on .NET 10 - **Components:** 50+ native controls -- **Status:** In Development โ€” foundational set (~40 components) landed on - `feat/initial-foundation`; overlay + form completeness tracked in +- **Status:** First prerelease `0.1.0-alpha.1` โ€” 58 component families (89 CLI targets), + P0โ€“P5 complete, P6 partly; remaining work tracked in [COMPONENTS_ROADMAP.md](./COMPONENTS_ROADMAP.md). ### Phase 2: Avalonia UI @@ -140,14 +140,14 @@ regardless of detected platform (see ### Installation ```bash -dotnet tool install -g ShellUI.Native.CLI +dotnet tool install -g ShellUI.Native.CLI --prerelease ``` ### Commands ```bash shellui-native init # Initialize project (creates shellui-native.json) shellui-native add # Add components (copies to Components/UI/) -shellui-native list # List available components (70+ components) +shellui-native list # List available components (89 targets) shellui-native remove # Remove components shellui-native update # Update components to latest version ``` @@ -162,7 +162,7 @@ shellui-native update # Update components to latest version "installedComponents": [ { "name": "button", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "platform": "MAUI", "installedAt": "2026-01-11T...", "isCustomized": false @@ -176,7 +176,7 @@ shellui-native update # Update components to latest version ### For MAUI Projects ```bash # Install CLI -dotnet tool install -g ShellUI.Native.CLI +dotnet tool install -g ShellUI.Native.CLI --prerelease # Create new MAUI project dotnet new maui -n MyApp @@ -219,7 +219,7 @@ dotnet run ### .NET Version Support - **MAUI:** .NET 10.0 (unified across all libraries โ€” see [global.json](../global.json)) -- **Avalonia:** .NET 10.0 (Avalonia 11+) +- **Avalonia:** .NET 10.0 (Avalonia 12) - **WinUI:** .NET 10.0 (WinUI 3) โ€” conditional phase ### Platform Requirements diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 3564771..5de469e 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -5,14 +5,16 @@ Get started with ShellUI Native in under 5 minutes. ## Prerequisites - .NET 10.0 SDK (see [global.json](../global.json)) -- A MAUI project today; Avalonia support is landing in Phase 2 (see [DEVELOPMENT_PLAN.md](./DEVELOPMENT_PLAN.md)) +- A .NET MAUI project (`dotnet new maui`); Avalonia support is the next phase (see [DEVELOPMENT_PLAN.md](./DEVELOPMENT_PLAN.md)) ## Installation ### 1. Install the CLI Tool +ShellUI Native is in prerelease, so include `--prerelease`: + ```bash -dotnet tool install -g ShellUI.Native.CLI +dotnet tool install -g ShellUI.Native.CLI --prerelease ``` ### 2. Navigate to Your Project @@ -34,8 +36,8 @@ This will: - Create `shellui-native.json` configuration - Generate theme resources (MAUI) -> If the CLI detects Avalonia/WinUI/WPF, `shellui-native add` will print a warning and still -> install MAUI-flavored code, since per-platform templates are the Phase 1b / Phase 2 workstream. +> Templates exist for MAUI only so far. In an Avalonia, WinUI or WPF project, `init` skips the +> `Shell.cs` utility and `add` reports that the component has no template for that platform. ### 4. Add Components @@ -50,18 +52,34 @@ shellui-native add button input card shellui-native add button,input,card ``` -### 5. Use Components in Your App +### 5. Load the Theme + +Publish the theme tokens before the first page loads, in `App.xaml.cs`: + +```csharp +public App() +{ + InitializeComponent(); + Components.UI.ShellTheme.EnsureInitialized(); +} +``` + +Components also do this on first use, but calling it early lets your own +`{DynamicResource ShellUI*}` references resolve on the first page. See +[COMPONENTS.md ยง Theming](./COMPONENTS.md#theming) for switching and customizing the theme. + +### 6. Use Components in Your App ```xml - - + + - - + ``` diff --git a/docs/README.md b/docs/README.md index 29668c6..183776e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,10 +39,10 @@ Unlike [ShellUI Blazor](https://shellui.dev/), **ShellUI Native does not use Tai ## Quick Start ```bash -# Install the CLI tool -dotnet tool install -g ShellUI.Native.CLI +# Install the CLI tool (prerelease) +dotnet tool install -g ShellUI.Native.CLI --prerelease -# Initialize in your MAUI or Avalonia project +# Initialize in your MAUI project shellui-native init --yes # Add components @@ -56,8 +56,8 @@ shellui-native list | Platform | Status | .NET Version | |----------|--------|--------------| -| .NET MAUI | Active (Phase 1) | .NET 10.0 | -| Avalonia UI | Planned (Phase 2) | .NET 10.0 (Avalonia 11+) | +| .NET MAUI | Available (`0.1.0-alpha.1`) โ€” Android, iOS, Mac Catalyst, Windows | .NET 10.0 | +| Avalonia UI | Planned (Phase 2) | .NET 10.0 (Avalonia 12) | | WinUI 3 | Conditional (Phase 3) | .NET 10.0 | WPF is intentionally not on this list โ€” it's recognized for project detection only, not an @@ -88,6 +88,7 @@ They're separate rendering contexts with no conflict. Install what you need base - [Getting Started](./QUICKSTART.md) - [Component List](./COMPONENTS.md) +- [Release Notes](./RELEASE_NOTES.md) - [Components Roadmap](./COMPONENTS_ROADMAP.md) โ€” prioritized P0โ€“P7 backlog - [Development Plan](./DEVELOPMENT_PLAN.md) โ€” branch strategy & phase breakdown - [Architecture](./ARCHITECTURE.md) @@ -99,14 +100,10 @@ They're separate rendering contexts with no conflict. Install what you need base - - - + + - - - + @@ -129,6 +126,9 @@ YourProject/ โ”‚ โ”œโ”€โ”€ Variants/ โ”‚ โ”‚ โ””โ”€โ”€ ButtonVariants.cs โ”‚ โ””โ”€โ”€ Shell.cs +โ”œโ”€โ”€ Resources/ +โ”‚ โ””โ”€โ”€ Styles/ +โ”‚ โ””โ”€โ”€ ShellUITheme.xaml โ””โ”€โ”€ shellui-native.json ``` diff --git a/src/ShellUI.Native.CLI/README.md b/src/ShellUI.Native.CLI/README.md index e0c6041..5f3e6fa 100644 --- a/src/ShellUI.Native.CLI/README.md +++ b/src/ShellUI.Native.CLI/README.md @@ -5,14 +5,16 @@ # ShellUI Native CLI -Command-line interface for ShellUI Native component library. +Command-line interface for ShellUI Native: shadcn-style, copy-and-own components for .NET MAUI, with the same design tokens as [ShellUI](https://shellui.dev/) for Blazor. ## Installation ```bash -dotnet tool install -g ShellUI.Native.CLI +dotnet tool install -g ShellUI.Native.CLI --prerelease ``` +Requires the .NET 10 SDK. + ## Commands ```bash @@ -26,7 +28,7 @@ shellui-native update [--all] # Update components ## Quick Start ```bash -# Navigate to your MAUI/WinUI/WPF project +# Navigate to your MAUI project cd YourProject # Initialize ShellUI Native @@ -36,6 +38,10 @@ shellui-native init --yes shellui-native add button input card ``` +Components are written to `Components/UI/` as plain C# files you own. Avalonia support is the next phase. + ## More Information -See the full documentation at [native.shellui.dev](https://native.shellui.dev) +- [Component reference](https://github.com/shellui-dev/shellui-native/blob/main/docs/COMPONENTS.md) +- [Release notes](https://github.com/shellui-dev/shellui-native/blob/main/docs/RELEASE_NOTES.md) +- Docs site: [native.shellui.dev](https://native.shellui.dev) (coming soon) From 3fc8e8ab13567f9779569374dd5d98d65d86aa81 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 10:35:46 +0200 Subject: [PATCH 06/11] style(cli): fix whitespace so dotnet format passes pr-check.yml runs 'dotnet format --verify-no-changes' on the CLI project, which failed on main because of trailing whitespace in five files. Whitespace only. --- src/ShellUI.Native.CLI/Program.cs | 10 ++++----- .../Services/ComponentInstaller.cs | 22 +++++++++---------- .../Services/ComponentManager.cs | 22 +++++++++---------- .../Services/InitService.cs | 6 ++--- .../Services/ProjectDetector.cs | 14 ++++++------ 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/ShellUI.Native.CLI/Program.cs b/src/ShellUI.Native.CLI/Program.cs index 2d5e751..5932bdf 100644 --- a/src/ShellUI.Native.CLI/Program.cs +++ b/src/ShellUI.Native.CLI/Program.cs @@ -27,7 +27,7 @@ static async Task Main(string[] args) static Command CreateInitCommand() { var command = new Command("init", "Initialize ShellUI Native in your project"); - + var forceOption = new Option("--force", "Reinitialize even if already initialized"); var styleOption = new Option("--style", () => "default", "Choose component style (default, minimal)"); var nonInteractiveOption = new Option("--yes", "Run in non-interactive mode with default options"); @@ -70,7 +70,7 @@ static Command CreateInitCommand() static Command CreateAddCommand() { var command = new Command("add", "Add component(s) to your project"); - + var componentsArg = new Argument("components", "Component name(s) to add (space or comma-separated)") { Arity = ArgumentArity.OneOrMore @@ -101,7 +101,7 @@ static Command CreateListCommand() var installedOption = new Option("--installed", "Show only installed components"); var availableOption = new Option("--available", "Show only available components"); - + command.AddOption(installedOption); command.AddOption(availableOption); @@ -123,7 +123,7 @@ static Command CreateListCommand() static Command CreateRemoveCommand() { var command = new Command("remove", "Remove component(s) from your project"); - + var componentsArg = new Argument("components", "Component name(s) to remove") { Arity = ArgumentArity.OneOrMore @@ -148,7 +148,7 @@ static Command CreateRemoveCommand() static Command CreateUpdateCommand() { var command = new Command("update", "Update component(s) to latest version"); - + var componentsArg = new Argument("components", "Component name(s) to update (empty = all)") { Arity = ArgumentArity.ZeroOrMore diff --git a/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs b/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs index 32f6faa..efed7f9 100644 --- a/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs @@ -11,7 +11,7 @@ public static class ComponentInstaller public static async Task InstallComponents(string[] components, bool force) { var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui-native.json"); - + if (!File.Exists(configPath)) { AnsiConsole.MarkupLine("[red]ShellUI Native not initialized![/]"); @@ -21,7 +21,7 @@ public static async Task InstallComponents(string[] components, bool force) var configJson = await File.ReadAllTextAsync(configPath); var config = JsonSerializer.Deserialize(configJson); - + if (config == null) { AnsiConsole.MarkupLine("[red]Failed to read shellui-native.json[/]"); @@ -45,7 +45,7 @@ public static async Task InstallComponents(string[] components, bool force) var tally = new InstallTally(); var failedComponents = new List(); var installedSet = new HashSet(); - + // Show dependency information foreach (var componentName in componentList) { @@ -55,9 +55,9 @@ public static async Task InstallComponents(string[] components, bool force) AnsiConsole.MarkupLine($"[green]โ—[/] [bold]{componentName}[/] requires: [yellow]{string.Join(", ", metadata.Dependencies)}[/]"); } } - + AnsiConsole.MarkupLine(""); - + await AnsiConsole.Status() .Spinner(Spinner.Known.Dots) .SpinnerStyle(Style.Parse("green")) @@ -87,9 +87,9 @@ await InstallComponentWithDependenciesAsync( } private static async Task InstallComponentWithDependenciesAsync( - string componentName, - ShellUINativeConfig config, - ProjectInfo projectInfo, + string componentName, + ShellUINativeConfig config, + ProjectInfo projectInfo, bool force, HashSet installedSet, InstallTally tally, @@ -97,7 +97,7 @@ private static async Task InstallComponentWithDependenciesAsync( { if (installedSet.Contains(componentName)) return; - + if (!ComponentRegistry.Exists(componentName)) { AnsiConsole.MarkupLine($"[red]Component '{componentName}' not found[/]"); @@ -128,7 +128,7 @@ private static async Task InstallComponentWithDependenciesAsync( // Install the component var result = await InstallComponentInternalAsync(componentName, metadata, config, projectInfo, force); - + if (result == InstallResult.Success) { tally.Success++; @@ -153,7 +153,7 @@ private static async Task InstallComponentInternalAsync( bool force) { var componentPath = Path.Combine(Directory.GetCurrentDirectory(), config.ComponentsPath, metadata.FilePath); - + if (File.Exists(componentPath) && !force) { AnsiConsole.MarkupLine($"[yellow]Skipped '{componentName}' (already exists)[/]"); diff --git a/src/ShellUI.Native.CLI/Services/ComponentManager.cs b/src/ShellUI.Native.CLI/Services/ComponentManager.cs index f1f2d92..ed32719 100644 --- a/src/ShellUI.Native.CLI/Services/ComponentManager.cs +++ b/src/ShellUI.Native.CLI/Services/ComponentManager.cs @@ -44,15 +44,15 @@ public static void ListComponents(bool installedOnly, bool availableOnly) foreach (var component in components) { - var status = installedNames.Contains(component.Name) - ? "[green]installed[/]" + var status = installedNames.Contains(component.Name) + ? "[green]installed[/]" : "[dim]available[/]"; table.AddRow( $"[bold]{component.Name}[/]", component.Category.ToString(), - component.Description.Length > 40 - ? component.Description[..37] + "..." + component.Description.Length > 40 + ? component.Description[..37] + "..." : component.Description, status ); @@ -65,7 +65,7 @@ public static void ListComponents(bool installedOnly, bool availableOnly) public static void RemoveComponents(string[] components) { var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui-native.json"); - + if (!File.Exists(configPath)) { AnsiConsole.MarkupLine("[red]ShellUI Native not initialized![/]"); @@ -74,7 +74,7 @@ public static void RemoveComponents(string[] components) var json = File.ReadAllText(configPath); var config = JsonSerializer.Deserialize(json); - + if (config == null) { AnsiConsole.MarkupLine("[red]Failed to read shellui-native.json[/]"); @@ -82,7 +82,7 @@ public static void RemoveComponents(string[] components) } var removedCount = 0; - + foreach (var componentName in components) { var metadata = ComponentRegistry.GetMetadata(componentName); @@ -93,7 +93,7 @@ public static void RemoveComponents(string[] components) } var componentPath = Path.Combine(Directory.GetCurrentDirectory(), config.ComponentsPath, metadata.FilePath); - + if (File.Exists(componentPath)) { File.Delete(componentPath); @@ -117,7 +117,7 @@ public static void RemoveComponents(string[] components) public static void UpdateComponents(string[] components, bool all) { var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui-native.json"); - + if (!File.Exists(configPath)) { AnsiConsole.MarkupLine("[red]ShellUI Native not initialized![/]"); @@ -126,7 +126,7 @@ public static void UpdateComponents(string[] components, bool all) var json = File.ReadAllText(configPath); var config = JsonSerializer.Deserialize(json); - + if (config == null) { AnsiConsole.MarkupLine("[red]Failed to read shellui-native.json[/]"); @@ -136,7 +136,7 @@ public static void UpdateComponents(string[] components, bool all) var projectInfo = ProjectDetector.DetectProject(); IEnumerable componentsToUpdate; - + if (all || components.Length == 0) { componentsToUpdate = config.InstalledComponents.Select(c => c.Name); diff --git a/src/ShellUI.Native.CLI/Services/InitService.cs b/src/ShellUI.Native.CLI/Services/InitService.cs index ab0bcc3..b9860aa 100644 --- a/src/ShellUI.Native.CLI/Services/InitService.cs +++ b/src/ShellUI.Native.CLI/Services/InitService.cs @@ -31,7 +31,7 @@ await AnsiConsole.Status() ctx.Status("Detecting project type..."); await Task.Delay(300); projectInfo = ProjectDetector.DetectProject(); - + var platformName = projectInfo.Platform switch { NativePlatform.MAUI => ".NET MAUI", @@ -40,7 +40,7 @@ await AnsiConsole.Status() NativePlatform.WPF => "WPF", _ => "Unknown" }; - + AnsiConsole.MarkupLine($"[green]โœ“ Detected:[/] {platformName}"); AnsiConsole.MarkupLine($"[dim]Project: {projectInfo.ProjectName}[/]"); AnsiConsole.MarkupLine($"[dim]Namespace: {projectInfo.RootNamespace}[/]"); @@ -142,7 +142,7 @@ private static async Task InstallShellUtilityAsync(ProjectInfo projectInfo, stri if (content == null) return; content = content.Replace("YourProjectNamespace", projectInfo.RootNamespace); - + var filePath = Path.Combine(componentsPath, "Shell.cs"); await File.WriteAllTextAsync(filePath, content); AnsiConsole.MarkupLine($"[green]โœ“ Installed:[/] Shell.cs"); diff --git a/src/ShellUI.Native.CLI/Services/ProjectDetector.cs b/src/ShellUI.Native.CLI/Services/ProjectDetector.cs index c8f246a..38339df 100644 --- a/src/ShellUI.Native.CLI/Services/ProjectDetector.cs +++ b/src/ShellUI.Native.CLI/Services/ProjectDetector.cs @@ -9,7 +9,7 @@ public static class ProjectDetector public static ProjectInfo DetectProject() { var csprojFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj"); - + if (csprojFiles.Length == 0) { throw new Exception("No .csproj file found in current directory. Please run this command from your project root."); @@ -17,9 +17,9 @@ public static ProjectInfo DetectProject() var csprojPath = csprojFiles[0]; var projectName = Path.GetFileNameWithoutExtension(csprojPath); - + var doc = XDocument.Load(csprojPath); - + var platform = DetectPlatform(doc, csprojPath); var rootNamespace = DetectRootNamespace(doc, projectName); @@ -35,11 +35,11 @@ public static ProjectInfo DetectProject() internal static NativePlatform DetectPlatform(XDocument doc, string csprojPath) { var sdk = doc.Root?.Attribute("Sdk")?.Value ?? ""; - + // Check for MAUI workload if (sdk.Contains("Maui", StringComparison.OrdinalIgnoreCase)) return NativePlatform.MAUI; - + var useMaui = doc.Descendants("UseMaui").FirstOrDefault()?.Value; if (useMaui?.Equals("true", StringComparison.OrdinalIgnoreCase) == true) return NativePlatform.MAUI; @@ -62,7 +62,7 @@ internal static NativePlatform DetectPlatform(XDocument doc, string csprojPath) // Check for WPF if (sdk.Contains("Wpf", StringComparison.OrdinalIgnoreCase)) return NativePlatform.WPF; - + var useWpf = doc.Descendants("UseWPF").FirstOrDefault()?.Value; if (useWpf?.Equals("true", StringComparison.OrdinalIgnoreCase) == true) return NativePlatform.WPF; @@ -70,7 +70,7 @@ internal static NativePlatform DetectPlatform(XDocument doc, string csprojPath) // Try to detect from output type and target framework var targetFramework = doc.Descendants("TargetFramework").FirstOrDefault()?.Value ?? ""; var targetFrameworks = doc.Descendants("TargetFrameworks").FirstOrDefault()?.Value ?? ""; - + // MAUI typically targets multiple platforms if (targetFrameworks.Contains("android") || targetFrameworks.Contains("ios")) return NativePlatform.MAUI; From 2332dd80c94b0baf083e6c2e9f5b8b941a6f5c14 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 10:52:02 +0200 Subject: [PATCH 07/11] feat(cli): show the ShellUI Native mark and ShellUI's loaders Ports ShellUI's terminal loaders with the ShellUI Native mark (two screens): init and list open with the mark, version and a subtitle in place of the ASCII banner; init's setup draws the mark dot by dot beside the current step; add uses the snake spinner. Without an interactive Unicode terminal they fall back to the snake spinner or plain text. init no longer waits 300 ms behind a spinner to detect the project. --- docs/RELEASE_NOTES.md | 2 +- src/ShellUI.Native.CLI/Program.cs | 20 +-- .../Services/ComponentInstaller.cs | 4 +- .../Services/ComponentManager.cs | 2 + .../Services/InitService.cs | 55 +++----- src/ShellUI.Native.CLI/Services/Loaders.cs | 117 ++++++++++++++++++ tests/ShellUI.Native.Tests/LoadersTests.cs | 43 +++++++ 7 files changed, 184 insertions(+), 59 deletions(-) create mode 100644 src/ShellUI.Native.CLI/Services/Loaders.cs create mode 100644 tests/ShellUI.Native.Tests/LoadersTests.cs diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index e75e0a0..58f8e07 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -47,7 +47,7 @@ Compositional families (`dialog`, `drawer`, `sheet`, `dropdown`, `popover`, `hov ## ๐Ÿงฐ CLI -`shellui-native init`, `add`, `list`, `remove` and `update`. `init` writes the `shell` utility, `shellui-native.json` and a theme resource dictionary; `add` resolves dependencies and replaces the namespace with your project's. +`shellui-native init`, `add`, `list`, `remove` and `update`. `init` writes the `shell` utility, `shellui-native.json` and a theme resource dictionary; `add` resolves dependencies and replaces the namespace with your project's. In a terminal, `init` and `list` show the ShellUI Native mark, and work in progress uses ShellUI's logo and snake loaders. ## โš ๏ธ Known limitations diff --git a/src/ShellUI.Native.CLI/Program.cs b/src/ShellUI.Native.CLI/Program.cs index 5932bdf..830ffbd 100644 --- a/src/ShellUI.Native.CLI/Program.cs +++ b/src/ShellUI.Native.CLI/Program.cs @@ -10,6 +10,9 @@ class Program { static async Task Main(string[] args) { + // The loaders draw Unicode dots and Braille. + Console.OutputEncoding = System.Text.Encoding.UTF8; + var rootCommand = new RootCommand("ShellUI Native - CLI-first cross-platform component library") { Description = "Add beautiful, accessible components to your MAUI or Avalonia app. Inspired by shadcn/ui." @@ -40,22 +43,7 @@ static Command CreateInitCommand() { try { - Console.OutputEncoding = System.Text.Encoding.UTF8; - var logo = @" - โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— - โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ - โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ - โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ - โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ - โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• - โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— - โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• - โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— - โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•”โ•โ•โ• - โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— - โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ• -"; - AnsiConsole.MarkupLine($"[blue]{logo}[/]"); + LogoLoader.WriteHeader("Setting up your MAUI project"); await InitService.InitializeAsync(style, force, nonInteractive); } catch (Exception ex) diff --git a/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs b/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs index efed7f9..ad21826 100644 --- a/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs @@ -58,9 +58,7 @@ public static async Task InstallComponents(string[] components, bool force) AnsiConsole.MarkupLine(""); - await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots) - .SpinnerStyle(Style.Parse("green")) + await Loaders.SnakeStatus() .StartAsync("Installing components...", async ctx => { foreach (var componentName in componentList) diff --git a/src/ShellUI.Native.CLI/Services/ComponentManager.cs b/src/ShellUI.Native.CLI/Services/ComponentManager.cs index ed32719..4b74496 100644 --- a/src/ShellUI.Native.CLI/Services/ComponentManager.cs +++ b/src/ShellUI.Native.CLI/Services/ComponentManager.cs @@ -36,6 +36,8 @@ public static void ListComponents(bool installedOnly, bool availableOnly) components = components.Where(c => !installedNames.Contains(c.Name)).OrderBy(c => c.Category).ThenBy(c => c.Name); } + LogoLoader.WriteHeader("Components"); + var table = new Table(); table.AddColumn("Component"); table.AddColumn("Category"); diff --git a/src/ShellUI.Native.CLI/Services/InitService.cs b/src/ShellUI.Native.CLI/Services/InitService.cs index b9860aa..08e766d 100644 --- a/src/ShellUI.Native.CLI/Services/InitService.cs +++ b/src/ShellUI.Native.CLI/Services/InitService.cs @@ -19,40 +19,18 @@ public static async Task InitializeAsync(string style, bool force, bool nonInter return; } - ProjectInfo projectInfo = null!; - - try - { - await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots) - .SpinnerStyle(Style.Parse("green")) - .StartAsync("Initializing ShellUI Native...", async ctx => - { - ctx.Status("Detecting project type..."); - await Task.Delay(300); - projectInfo = ProjectDetector.DetectProject(); - - var platformName = projectInfo.Platform switch - { - NativePlatform.MAUI => ".NET MAUI", - NativePlatform.Avalonia => "Avalonia UI", - NativePlatform.WinUI => "WinUI 3", - NativePlatform.WPF => "WPF", - _ => "Unknown" - }; - - AnsiConsole.MarkupLine($"[green]โœ“ Detected:[/] {platformName}"); - AnsiConsole.MarkupLine($"[dim]Project: {projectInfo.ProjectName}[/]"); - AnsiConsole.MarkupLine($"[dim]Namespace: {projectInfo.RootNamespace}[/]"); - }); - } - catch + var projectInfo = ProjectDetector.DetectProject(); + var platformName = projectInfo.Platform switch { - projectInfo = ProjectDetector.DetectProject(); - AnsiConsole.MarkupLine($"[green]โœ“ Detected:[/] {projectInfo.Platform}"); - AnsiConsole.MarkupLine($"[dim]Project: {projectInfo.ProjectName}[/]"); - AnsiConsole.MarkupLine($"[dim]Namespace: {projectInfo.RootNamespace}[/]"); - } + NativePlatform.MAUI => ".NET MAUI", + NativePlatform.Avalonia => "Avalonia UI", + NativePlatform.WinUI => "WinUI 3", + NativePlatform.WPF => "WPF", + _ => "Unknown" + }; + AnsiConsole.MarkupLine($"[green]โœ“ Detected:[/] {platformName}"); + AnsiConsole.MarkupLine($"[dim]Project: {Markup.Escape(projectInfo.ProjectName)}[/]"); + AnsiConsole.MarkupLine($"[dim]Namespace: {Markup.Escape(projectInfo.RootNamespace)}[/]"); if (projectInfo.Platform == NativePlatform.Unknown && !nonInteractive) { @@ -77,11 +55,10 @@ await AnsiConsole.Status() AnsiConsole.MarkupLine("[yellow]Platform not detected, defaulting to MAUI[/]"); } - await AnsiConsole.Status() - .StartAsync("Setting up ShellUI Native...", async ctx => + await LogoLoader.RunAsync("Setting up ShellUI Native...", async status => { // Create Components/UI folder - ctx.Status("Creating component folders..."); + status("Creating component folders..."); var componentsPath = Path.Combine(Directory.GetCurrentDirectory(), "Components", "UI"); var variantsPath = Path.Combine(componentsPath, "Variants"); Directory.CreateDirectory(componentsPath); @@ -89,11 +66,11 @@ await AnsiConsole.Status() AnsiConsole.MarkupLine($"[green]โœ“ Created:[/] Components/UI/"); // Install Shell utilities - ctx.Status("Installing Shell utilities..."); + status("Installing Shell utilities..."); await InstallShellUtilityAsync(projectInfo, componentsPath); // Create configuration file - ctx.Status("Creating configuration..."); + status("Creating configuration..."); var config = new ShellUINativeConfig { Style = style, @@ -117,7 +94,7 @@ await AnsiConsole.Status() // Create theme resources (MAUI only for now) if (projectInfo.Platform == NativePlatform.MAUI) { - ctx.Status("Creating theme resources..."); + status("Creating theme resources..."); await CreateThemeResourcesAsync(); } }); diff --git a/src/ShellUI.Native.CLI/Services/Loaders.cs b/src/ShellUI.Native.CLI/Services/Loaders.cs new file mode 100644 index 0000000..ea58e8e --- /dev/null +++ b/src/ShellUI.Native.CLI/Services/Loaders.cs @@ -0,0 +1,117 @@ +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace ShellUI.Native.CLI.Services; + +public static class Loaders +{ + public static Status SnakeStatus() => + AnsiConsole.Status().Spinner(SnakeSpinner.Instance).SpinnerStyle(Style.Parse("green")); +} + +// ShellUI's "snake" loader as a two-character Braille spinner; Spectre falls back to ASCII without Unicode. +public sealed class SnakeSpinner : Spinner +{ + private static readonly int[] Path = { 0, 1, 2, 5, 8, 7, 6, 3 }; + private static readonly int[,] Bits = { { 0x01, 0x08 }, { 0x02, 0x10 }, { 0x04, 0x20 } }; + + public static readonly SnakeSpinner Instance = new(); + + private SnakeSpinner() + { + Frames = Enumerable.Range(0, Path.Length) + .Select(step => Braille(i => IsLit(Array.IndexOf(Path, i), step))) + .ToList(); + } + + public override TimeSpan Interval => TimeSpan.FromMilliseconds(110); + public override bool IsUnicode => true; + public override IReadOnlyList Frames { get; } + + private static bool IsLit(int position, int step) => + position >= 0 && Enumerable.Range(0, 3).Any(k => (step - k + Path.Length) % Path.Length == position); + + internal static string Braille(Func lit) + { + var chars = new int[2]; + for (var i = 0; i < 9; i++) + if (lit(i)) chars[i % 3 / 2] |= Bits[i / 3, i % 3 % 2]; + return string.Concat(chars.Select(c => (char)(0x2800 + c))); + } +} + +// The ShellUI Native mark (two screens) drawn beside the current step; the snake spinner is used when there is no terminal. +public static class LogoLoader +{ + internal static readonly string[] Pattern = { "XXX..", "X.X..", "XXXXX", "..X.X", "..XXX" }; + private const int Cycle = 30; + + public static async Task RunAsync(string initialStatus, Func, Task> work) + { + var caps = AnsiConsole.Profile.Capabilities; + if (!caps.Interactive || !caps.Unicode || Console.IsOutputRedirected) + { + await Loaders.SnakeStatus().StartAsync(initialStatus, ctx => work(s => ctx.Status(s))); + return; + } + + var status = initialStatus; + await AnsiConsole.Live(Render(0, status)) + .AutoClear(true) + .StartAsync(async ctx => + { + var task = work(s => status = s); + for (var frame = 1; !task.IsCompleted; frame++) + { + ctx.UpdateTarget(Render(frame, status)); + await Task.WhenAny(task, Task.Delay(70)); + } + await task; + }); + } + + // Lit dots switch on along the diagonal, hold, then switch off the same way. + internal static string[] Frame(int frame) + { + var t = frame % Cycle; + return Enumerable.Range(0, 5).Select(r => string.Concat(Enumerable.Range(0, 5).Select(c => + { + var diagonal = r + c; + var on = Pattern[r][c] == 'X' && (t < Cycle / 2 ? diagonal <= t : diagonal > t - Cycle / 2); + return on ? 'X' : '.'; + }))).ToArray(); + } + + private static IRenderable Render(int frame, string status) + { + var grid = new Grid().AddColumn().AddColumn(); + grid.AddRow(new Markup(Dots(Frame(frame))), new Markup($"\n\n {Markup.Escape(status)}")); + return grid; + } + + private static string Dots(IEnumerable rows) => + string.Join("\n", rows.Select(row => string.Join(" ", row.Select(ch => ch == 'X' ? "[white]โ—[/]" : "[grey19]โ—[/]")))); + + // The static mark with the name, version and a subtitle beside it (used by init and list). + public static void WriteHeader(string subtitle) + { + var version = typeof(LogoLoader).Assembly + .GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false) + .OfType() + .FirstOrDefault()?.InformationalVersion.Split('+')[0] ?? ""; + + if (!AnsiConsole.Profile.Capabilities.Unicode) + { + AnsiConsole.MarkupLine($"[bold]ShellUI Native[/] [dim]{Markup.Escape(version)}[/] {Markup.Escape(subtitle)}\n"); + return; + } + + var grid = new Grid().AddColumn().AddColumn(); + grid.AddRow( + new Markup(Dots(Pattern)), + new Markup($"\n [bold]ShellUI Native[/] [dim]{Markup.Escape(version)}[/]\n [dim]{Markup.Escape(subtitle)}[/]")); + AnsiConsole.WriteLine(); + AnsiConsole.Write(grid); + AnsiConsole.WriteLine(); + } +} diff --git a/tests/ShellUI.Native.Tests/LoadersTests.cs b/tests/ShellUI.Native.Tests/LoadersTests.cs new file mode 100644 index 0000000..eaaa886 --- /dev/null +++ b/tests/ShellUI.Native.Tests/LoadersTests.cs @@ -0,0 +1,43 @@ +using ShellUI.Native.CLI.Services; + +namespace ShellUI.Native.Tests; + +public class LoadersTests +{ + [Fact] + public void SnakeSpinner_walks_the_grid_edge_with_a_three_dot_trail() + { + var frames = SnakeSpinner.Instance.Frames; + + Assert.Equal(8, frames.Count); + Assert.Equal(8, frames.Distinct().Count()); + foreach (var frame in frames) + { + Assert.Equal(2, frame.Length); + Assert.All(frame, ch => Assert.InRange(ch, 'โ €', 'โฃฟ')); + Assert.Equal(3, frame.Sum(ch => System.Numerics.BitOperations.PopCount((uint)(ch - 0x2800)))); + } + + // The centre cell (row 1, column 1 โ†’ first character, dot 0x10) is never part of the snake path. + Assert.DoesNotContain(frames, f => ((f[0] - 0x2800) & 0x10) != 0); + } + + [Fact] + public void Braille_maps_grid_cells_to_dots() + { + Assert.Equal("โ ‰โ ", SnakeSpinner.Braille(i => i < 3)); + Assert.Equal("โ ‡โ €", SnakeSpinner.Braille(i => i % 3 == 0)); + } + + // Same rows as the ShellUI Native mark in md-files BRAND_MARKS.md (two screens). + [Fact] + public void LogoLoader_builds_the_two_screens_mark_along_the_diagonal_then_clears_it() + { + Assert.Equal(new[] { "XXX..", "X.X..", "XXXXX", "..X.X", "..XXX" }, LogoLoader.Pattern); + Assert.Equal(new[] { "X....", ".....", ".....", ".....", "....." }, LogoLoader.Frame(0)); + Assert.Equal(LogoLoader.Pattern, LogoLoader.Frame(8)); + Assert.Equal(LogoLoader.Frame(8), LogoLoader.Frame(14)); + Assert.Equal(new[] { ".....", ".....", ".....", ".....", "....." }, LogoLoader.Frame(29)); + Assert.Equal(LogoLoader.Frame(0), LogoLoader.Frame(30)); + } +} From 6b060f140d022a6daf5615df20e7514cf962cacd Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 10:52:02 +0200 Subject: [PATCH 08/11] docs: show the Shell family marks and make the NuGet README plain Markdown nuget.org renders Markdown only, so the block at the top of the CLI README showed as raw HTML. It now uses a 64 px PNG of the mark. Both READMEs list ShellUI, ShellIcons and ShellDocs with their marks; the PNGs were drawn with md-files brand/generate.py. --- assets/brand/shelldocs.png | Bin 0 -> 850 bytes assets/brand/shellicons.png | Bin 0 -> 1095 bytes assets/brand/shellui.png | Bin 0 -> 913 bytes assets/icon-64.png | Bin 0 -> 1593 bytes docs/README.md | 11 ++++++++--- src/ShellUI.Native.CLI/README.md | 13 +++++++++---- 6 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 assets/brand/shelldocs.png create mode 100644 assets/brand/shellicons.png create mode 100644 assets/brand/shellui.png create mode 100644 assets/icon-64.png diff --git a/assets/brand/shelldocs.png b/assets/brand/shelldocs.png new file mode 100644 index 0000000000000000000000000000000000000000..db7d06bc647c999434e059d9cd9f78d976e3f836 GIT binary patch literal 850 zcmV-Y1FigtP)GTs8cQ7piNRs|2`Z2zB!mQscpsz|RfXyYs06d`ei!C4 zw_JOa()YTM{FBSf{%2=yXJ_{A@VpOxS65dJAJ0Bse7yTG{}}K4x$&Xl9sBbK-~0IU z9~@2sKHVod|L9};3!N9>+v8`>8%OXMmo{660@y~<>6hkw;-1je#yExk~@>B+2CD>ELCBl3JcA8-KITE|#8aJ3xU%mIfvj|kw! zOKMUWYPZ`VTCJAJWHNycCw9ACh{a+N$aA?|gaeSFV~plt135OatvN>o@$Mx>$=$G> zDVNLU?(Qz)hyX{c)#?EU8M-(Qwy*wS+X0Z5dTAdX9!x5g3UssCEaJ#yG6~b6*=&X!t|jdlqdC|>j!kUqRJHfp zNdbsMEHQUUoEHoRgTSeNzaNovx8VS;wT`iH;A%OxnFEdle3k$JdCF3Mv`^yXx3{+e zzQSP+<~r7|qXN*?MwT4(dOh>@_7=!Dn~mvqyPh$dZ zsMYqSlcg8u1+TBKfqXO?9dMALV~mz#LsHS^*nQ%p0PI>Xo0SxZt0*@&H)cMcAIOoR zi{oI+-rUEoLSg`PP1~?+SV;xJdc6+h!{P9NgA5&Gv>Y3Fx_e5glzf(y zz_CuJ6Y8eZX~^MP^NHhay+1$ z+Vz>uNq$R;Xy@SoI|#;t|}{`OQlW~9M;{NYY9T9mM*c_ z)ZH7{r`VGM_~bpGY8^RL&P|G?q)Cno1V@?FGdR_8x-U&LPxF5=G~ cF8o!UbN~PV literal 0 HcmV?d00001 diff --git a/assets/brand/shellicons.png b/assets/brand/shellicons.png new file mode 100644 index 0000000000000000000000000000000000000000..988b743251bfa9dc1c802716ebc353f8fc1cd32f GIT binary patch literal 1095 zcmV-N1i1T&P)kswhjet(EorBZ1nIyA?AHfA$B zd#+t~cYi-Czne_5nK|Ei?3tPK*c${*er;`Siy<9_bQjV~NbWDw%cpI8SPb(1!of2k z{rLwDXAwTlWI4YT(%TPoUI^c|zH;8xgtmNX^H!q(-V*8bLvuc}CUm!PHKoPivrW}C zfTQ5E*M2#0aB$Gg&CR*l*;&`u*O!nT92~gY+grE4zn|a_3=BjVh-WQhEEuqwk8W&$ z!DfyC?t-jE3Pr1{t1+#tthk<@o=Aoh@9*z1U0+{E{Qmy_1O|eKj4>L64*2Lqx5nfM z;w8w6lDA=?HHpm z=zxz-bn8^L_101V;t*5JTN3Amc6N3mr?$7Z6MWt_7>H*rV=Nf3nvZU5fRTVp2>>Eb znd)YB;^U8wjz;ty3^uTp*RNaw=x!rZ4sC93x~He7i2wNb=r%SsV%g2jO^o4L_>5sJ ze6Z*cnNCURPK5)YSKFeKsTb#k9v&Vd{@&hR3Ih)rV>BNfl8P2%*NK(_u)SW2l@#!G zl;PoFcX@f4;=@Dc!=TF+civgZ41iVBDwcwkR1mtqzmNF4ySph2JYt?IOc6R2DkB{BL!a_nuagyJXB0@_`OYZdaG$Nk0jIm&(u!Yc#4Qyev zlmM7=otMwg&u)HxKGiC9s^IYJ>ub#CS%T21rCTgI(XC}VC0h>Q>gp<0T!)8;(E{b` zD5MN;-SXif^I_11PRqyk{{U*ALbWeqp^lCY*WTVpVxRGLMh061J&#ge~eZl}>{?c_Pw zGgwL*~PvtFnu&W><^6 zF`qN6qHG0h0Pv9cFz70Bwb=6Iu3aOf literal 0 HcmV?d00001 diff --git a/assets/brand/shellui.png b/assets/brand/shellui.png new file mode 100644 index 0000000000000000000000000000000000000000..50e5cf4c265249c3381b74eff8afb316b5ebbd68 GIT binary patch literal 913 zcmV;C18)3@P)#vhT!LI^g3B6bQ&+7$PP*jiaxr4_6+ zX~umP&oVo^W`i#GI^hOhSl%;pK6cJIGrJqd`Qn#KrAjW&T|BsWb)o(;UO#Q)L&+KS z=MFBq`121OjsrdwJ0J1^Ct&=De~5ji|KgTNFS)kWRle=VNO^?>5R(l$_5t zWorQE&S$S3I`HuDP<1*Tb$ffO_V)GyvV15%2PbZmLeXF_@bUKcrgnCAJQ+^x@9+D-S@`++`2Y{#kTFK*(E*Q6bnCW8 z2;$X=i;}ycD+Om~XKH3+2yN4q4P3bfMFykVVZw7dlNl*#2<<_6%EOnGjiq{0=znYUyrz81nHXG_rKMp|eHnQZPUa$Mh{`~x`j*gB3L7CkNEG#Uj z-Q8V}uT&~(adFX?-Q3*xDb($DlL>&kVHTY%y`Hn+>FLSiS65d8o&mf1hj@Vi#p2ijtY_P{8=F=8#TNJS|s_9f<{iLXHC5ec6x%6esyDDIy>> zuCA^;NSX*3%N!|e!S?pHy12Md*VotT;NT#c09bN8XW3O{V=@nBk}5d-{{HT#1J@FS zo?3c~rCGeZyaZ}MPswiwU^)rdU5A~8xa(#?!VNnMwcG8`oV&Zb{{h&aLe>}YShP{A zH!|yw64oE)tUpd$|3EVqkCawF)LZ{r#rhXpHoRK2{^hRquh*^rz+v|%ChI@)jm2kH n>pygl#a|7^;?Ev7{@vySrk~DV8^j}K50#m8czk%k5a1{;aSW(tDQ2@DMV2LxjU1G%6en3$+w zBpR3;#7Hy|q3?c!wY0s}yED48*Xzzg!7^3T(^b<|_3N&hR4T-;s;VlTr9W9Z$WP2PrRv zPIFJC55=ZnS!E!vw3ql|5UiwZQrg$;5{g;F~8j+!df0Dn^N z&@uE))6>&te}CWH-QAhv<72b9xR@uiva({%&d$v3?XB6{+cU$%!yow7)zxNeYs*|; zUkAj0uGup6_4TH=x7Uo0j+&vNA=B2@miL^NmKHNGFc8S}_xGE|#>S`yI7z+Z=g`W( zzP{$s^73*{W@BR`kN^1i7)(%P4i69WWVj~$U%!5t@$qq!PNzRmM@L6a27|bKD54Sm zrlM?Yc=MV8Zfm#2CBw4!O}@FgIaumG{^{vyFvj}&x>p7sGJ}JIs&WvnQCnN<<>L-fbc$pEd|sRQ z!^1N=-~mn69p_yysv9Sqa1hEmM@-07CeDF`1^OrhWV5y#4Z-K8H2G3DUXa~hu953p0`t-q&FMnu$;vwS)%Rj z?H^>Orl!pK`FU;({&UTi;T}8CF{TTa>~u+rv~fZ#1_0q{0S=$H{aXu8;tmZB4d(v- zK5q=J!4ucf#<1fAF+r1w8V4d70C=kzfPg2<-j}}m`uYNyrKKfr8ia=ovc7S|9SDpX zAJ`n50l@aY$@A9mMc>2$H;l2iw&ooJ-nRxqY;3fNlj=mq2T&x9&Cbqx4FIpu@@8y# zj-<#!Zc=gs@K823?!@gpPw;XlMUJ*3IjGQw6vdS^tZxTXaDvNc163^Zd^rn{wut;- zdnRooJUjp6$1G*urk2K5S69u^(NRGB*U~8WG6rL5m;%HIY(|ERKzc445c(p$;(k14=Tx2U9s{sHa?qLR-7(4I)Xxuz+ z`?oBAHwL(F;Km8Yw80Y3bT?JZ%>_#(H2_CfTJ&Z!BQiF^(-h=w`?7$n1kFH#809(k zX38e!7@Gk&zw=F==u3d~@d$}Pi{)BwKI9{Zu?+77b? z*T!JW9-9F;Y$#N)DA$1q?%*4PLz~zPK(y8-#qm_RkHnQ>3 zQyME)UJF~ME13a^1Cpazq7*1y8DfH*+w*qZifx$UD0yB|9RbfSz6ep6r9dl98g{2j zEC$d{Vj%y1GWd=Y6y>jhtIR@dGisK&oxR`e5-6_H%nK1yL|G&MDoc@ r^3hcC*#}BX00000NkvXXu0mjfQ=Ikn literal 0 HcmV?d00001 diff --git a/docs/README.md b/docs/README.md index 183776e..67cc026 100644 --- a/docs/README.md +++ b/docs/README.md @@ -132,10 +132,15 @@ YourProject/ โ””โ”€โ”€ shellui-native.json ``` -## Related Projects +## The Shell family -- [ShellUI (Blazor)](https://github.com/shellui-dev/shellui) - Blazor component library -- [shadcn/ui](https://ui.shadcn.com/) - The original inspiration +| | Project | | +|---|---|---| +| | [ShellUI](https://github.com/shellui-dev/shellui) | The Blazor component library this one mirrors | +| | [ShellIcons](https://github.com/shellui-dev/shell-icons) | Lucide icons for Blazor, MAUI and Avalonia; the source of the `icon` component | +| | [ShellDocs](https://github.com/shellui-dev/shelldocs) | The docs framework behind the ShellUI sites | + +Inspired by [shadcn/ui](https://ui.shadcn.com/). ## License diff --git a/src/ShellUI.Native.CLI/README.md b/src/ShellUI.Native.CLI/README.md index 5f3e6fa..d4cc501 100644 --- a/src/ShellUI.Native.CLI/README.md +++ b/src/ShellUI.Native.CLI/README.md @@ -1,7 +1,4 @@ - - - - +![ShellUI Native](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/icon-64.png) # ShellUI Native CLI @@ -45,3 +42,11 @@ Components are written to `Components/UI/` as plain C# files you own. Avalonia s - [Component reference](https://github.com/shellui-dev/shellui-native/blob/main/docs/COMPONENTS.md) - [Release notes](https://github.com/shellui-dev/shellui-native/blob/main/docs/RELEASE_NOTES.md) - Docs site: [native.shellui.dev](https://native.shellui.dev) (coming soon) + +## The Shell family + +| | Project | | +|---|---|---| +| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shellui.png) | [ShellUI](https://www.nuget.org/packages/ShellUI.CLI) | The Blazor component library this one mirrors | +| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shellicons.png) | [ShellIcons](https://www.nuget.org/packages/ShellIcons.Blazor) | Lucide icons for Blazor, MAUI and Avalonia | +| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shelldocs.png) | [ShellDocs](https://www.nuget.org/packages/ShellDocs.CLI) | The docs framework behind the ShellUI sites | From 9c3bc94f36b9e58a278aa7c8029a64c32bf7789b Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 11:16:40 +0200 Subject: [PATCH 09/11] docs: use theme-aware SVG marks in the READMEs nuget.org accepts SVG images from raw.githubusercontent.com, which serves them as image/svg+xml. The SVGs are the brand favicon tile (ink on light, paper on dark via prefers-color-scheme) with their size set in the file, since Markdown can't size images. They replace the 64 px and 48 px PNGs. --- assets/brand/shelldocs.png | Bin 850 -> 0 bytes assets/brand/shelldocs.svg | 1 + assets/brand/shellicons.png | Bin 1095 -> 0 bytes assets/brand/shellicons.svg | 1 + assets/brand/shellui.png | Bin 913 -> 0 bytes assets/brand/shellui.svg | 1 + assets/icon-64.png | Bin 1593 -> 0 bytes assets/icon.svg | 1 + docs/README.md | 6 +++--- src/ShellUI.Native.CLI/README.md | 8 ++++---- 10 files changed, 11 insertions(+), 7 deletions(-) delete mode 100644 assets/brand/shelldocs.png create mode 100644 assets/brand/shelldocs.svg delete mode 100644 assets/brand/shellicons.png create mode 100644 assets/brand/shellicons.svg delete mode 100644 assets/brand/shellui.png create mode 100644 assets/brand/shellui.svg delete mode 100644 assets/icon-64.png create mode 100644 assets/icon.svg diff --git a/assets/brand/shelldocs.png b/assets/brand/shelldocs.png deleted file mode 100644 index db7d06bc647c999434e059d9cd9f78d976e3f836..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 850 zcmV-Y1FigtP)GTs8cQ7piNRs|2`Z2zB!mQscpsz|RfXyYs06d`ei!C4 zw_JOa()YTM{FBSf{%2=yXJ_{A@VpOxS65dJAJ0Bse7yTG{}}K4x$&Xl9sBbK-~0IU z9~@2sKHVod|L9};3!N9>+v8`>8%OXMmo{660@y~<>6hkw;-1je#yExk~@>B+2CD>ELCBl3JcA8-KITE|#8aJ3xU%mIfvj|kw! zOKMUWYPZ`VTCJAJWHNycCw9ACh{a+N$aA?|gaeSFV~plt135OatvN>o@$Mx>$=$G> zDVNLU?(Qz)hyX{c)#?EU8M-(Qwy*wS+X0Z5dTAdX9!x5g3UssCEaJ#yG6~b6*=&X!t|jdlqdC|>j!kUqRJHfp zNdbsMEHQUUoEHoRgTSeNzaNovx8VS;wT`iH;A%OxnFEdle3k$JdCF3Mv`^yXx3{+e zzQSP+<~r7|qXN*?MwT4(dOh>@_7=!Dn~mvqyPh$dZ zsMYqSlcg8u1+TBKfqXO?9dMALV~mz#LsHS^*nQ%p0PI>Xo0SxZt0*@&H)cMcAIOoR zi{oI+-rUEoLSg`PP1~?+SV;xJdc6+h!{P9NgA5&Gv>Y3Fx_e5glzf(y zz_CuJ6Y8eZX~^MP^NHhay+1$ z+Vz>uNq$R;Xy@SoI|#;t|}{`OQlW~9M;{NYY9T9mM*c_ z)ZH7{r`VGM_~bpGY8^RL&P|G?q)Cno1V@?FGdR_8x-U&LPxF5=G~ cF8o!UbN~PV diff --git a/assets/brand/shelldocs.svg b/assets/brand/shelldocs.svg new file mode 100644 index 0000000..9b82bc2 --- /dev/null +++ b/assets/brand/shelldocs.svg @@ -0,0 +1 @@ + diff --git a/assets/brand/shellicons.png b/assets/brand/shellicons.png deleted file mode 100644 index 988b743251bfa9dc1c802716ebc353f8fc1cd32f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1095 zcmV-N1i1T&P)kswhjet(EorBZ1nIyA?AHfA$B zd#+t~cYi-Czne_5nK|Ei?3tPK*c${*er;`Siy<9_bQjV~NbWDw%cpI8SPb(1!of2k z{rLwDXAwTlWI4YT(%TPoUI^c|zH;8xgtmNX^H!q(-V*8bLvuc}CUm!PHKoPivrW}C zfTQ5E*M2#0aB$Gg&CR*l*;&`u*O!nT92~gY+grE4zn|a_3=BjVh-WQhEEuqwk8W&$ z!DfyC?t-jE3Pr1{t1+#tthk<@o=Aoh@9*z1U0+{E{Qmy_1O|eKj4>L64*2Lqx5nfM z;w8w6lDA=?HHpm z=zxz-bn8^L_101V;t*5JTN3Amc6N3mr?$7Z6MWt_7>H*rV=Nf3nvZU5fRTVp2>>Eb znd)YB;^U8wjz;ty3^uTp*RNaw=x!rZ4sC93x~He7i2wNb=r%SsV%g2jO^o4L_>5sJ ze6Z*cnNCURPK5)YSKFeKsTb#k9v&Vd{@&hR3Ih)rV>BNfl8P2%*NK(_u)SW2l@#!G zl;PoFcX@f4;=@Dc!=TF+civgZ41iVBDwcwkR1mtqzmNF4ySph2JYt?IOc6R2DkB{BL!a_nuagyJXB0@_`OYZdaG$Nk0jIm&(u!Yc#4Qyev zlmM7=otMwg&u)HxKGiC9s^IYJ>ub#CS%T21rCTgI(XC}VC0h>Q>gp<0T!)8;(E{b` zD5MN;-SXif^I_11PRqyk{{U*ALbWeqp^lCY*WTVpVxRGLMh061J&#ge~eZl}>{?c_Pw zGgwL*~PvtFnu&W><^6 zF`qN6qHG0h0Pv9cFz70Bwb=6Iu3aOf diff --git a/assets/brand/shellicons.svg b/assets/brand/shellicons.svg new file mode 100644 index 0000000..645c843 --- /dev/null +++ b/assets/brand/shellicons.svg @@ -0,0 +1 @@ + diff --git a/assets/brand/shellui.png b/assets/brand/shellui.png deleted file mode 100644 index 50e5cf4c265249c3381b74eff8afb316b5ebbd68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 913 zcmV;C18)3@P)#vhT!LI^g3B6bQ&+7$PP*jiaxr4_6+ zX~umP&oVo^W`i#GI^hOhSl%;pK6cJIGrJqd`Qn#KrAjW&T|BsWb)o(;UO#Q)L&+KS z=MFBq`121OjsrdwJ0J1^Ct&=De~5ji|KgTNFS)kWRle=VNO^?>5R(l$_5t zWorQE&S$S3I`HuDP<1*Tb$ffO_V)GyvV15%2PbZmLeXF_@bUKcrgnCAJQ+^x@9+D-S@`++`2Y{#kTFK*(E*Q6bnCW8 z2;$X=i;}ycD+Om~XKH3+2yN4q4P3bfMFykVVZw7dlNl*#2<<_6%EOnGjiq{0=znYUyrz81nHXG_rKMp|eHnQZPUa$Mh{`~x`j*gB3L7CkNEG#Uj z-Q8V}uT&~(adFX?-Q3*xDb($DlL>&kVHTY%y`Hn+>FLSiS65d8o&mf1hj@Vi#p2ijtY_P{8=F=8#TNJS|s_9f<{iLXHC5ec6x%6esyDDIy>> zuCA^;NSX*3%N!|e!S?pHy12Md*VotT;NT#c09bN8XW3O{V=@nBk}5d-{{HT#1J@FS zo?3c~rCGeZyaZ}MPswiwU^)rdU5A~8xa(#?!VNnMwcG8`oV&Zb{{h&aLe>}YShP{A zH!|yw64oE)tUpd$|3EVqkCawF)LZ{r#rhXpHoRK2{^hRquh*^rz+v|%ChI@)jm2kH n>pygl#a|7^;?Ev7{@vySrk~D diff --git a/assets/icon-64.png b/assets/icon-64.png deleted file mode 100644 index ca12e76a31da7a919bea58753c1d388f42d3b299..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1593 zcmV-92FCe`P)V8^j}K50#m8czk%k5a1{;aSW(tDQ2@DMV2LxjU1G%6en3$+w zBpR3;#7Hy|q3?c!wY0s}yED48*Xzzg!7^3T(^b<|_3N&hR4T-;s;VlTr9W9Z$WP2PrRv zPIFJC55=ZnS!E!vw3ql|5UiwZQrg$;5{g;F~8j+!df0Dn^N z&@uE))6>&te}CWH-QAhv<72b9xR@uiva({%&d$v3?XB6{+cU$%!yow7)zxNeYs*|; zUkAj0uGup6_4TH=x7Uo0j+&vNA=B2@miL^NmKHNGFc8S}_xGE|#>S`yI7z+Z=g`W( zzP{$s^73*{W@BR`kN^1i7)(%P4i69WWVj~$U%!5t@$qq!PNzRmM@L6a27|bKD54Sm zrlM?Yc=MV8Zfm#2CBw4!O}@FgIaumG{^{vyFvj}&x>p7sGJ}JIs&WvnQCnN<<>L-fbc$pEd|sRQ z!^1N=-~mn69p_yysv9Sqa1hEmM@-07CeDF`1^OrhWV5y#4Z-K8H2G3DUXa~hu953p0`t-q&FMnu$;vwS)%Rj z?H^>Orl!pK`FU;({&UTi;T}8CF{TTa>~u+rv~fZ#1_0q{0S=$H{aXu8;tmZB4d(v- zK5q=J!4ucf#<1fAF+r1w8V4d70C=kzfPg2<-j}}m`uYNyrKKfr8ia=ovc7S|9SDpX zAJ`n50l@aY$@A9mMc>2$H;l2iw&ooJ-nRxqY;3fNlj=mq2T&x9&Cbqx4FIpu@@8y# zj-<#!Zc=gs@K823?!@gpPw;XlMUJ*3IjGQw6vdS^tZxTXaDvNc163^Zd^rn{wut;- zdnRooJUjp6$1G*urk2K5S69u^(NRGB*U~8WG6rL5m;%HIY(|ERKzc445c(p$;(k14=Tx2U9s{sHa?qLR-7(4I)Xxuz+ z`?oBAHwL(F;Km8Yw80Y3bT?JZ%>_#(H2_CfTJ&Z!BQiF^(-h=w`?7$n1kFH#809(k zX38e!7@Gk&zw=F==u3d~@d$}Pi{)BwKI9{Zu?+77b? z*T!JW9-9F;Y$#N)DA$1q?%*4PLz~zPK(y8-#qm_RkHnQ>3 zQyME)UJF~ME13a^1Cpazq7*1y8DfH*+w*qZifx$UD0yB|9RbfSz6ep6r9dl98g{2j zEC$d{Vj%y1GWd=Y6y>jhtIR@dGisK&oxR`e5-6_H%nK1yL|G&MDoc@ r^3hcC*#}BX00000NkvXXu0mjfQ=Ikn diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..2bf104d --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1 @@ + diff --git a/docs/README.md b/docs/README.md index 67cc026..767a03d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -136,9 +136,9 @@ YourProject/ | | Project | | |---|---|---| -| | [ShellUI](https://github.com/shellui-dev/shellui) | The Blazor component library this one mirrors | -| | [ShellIcons](https://github.com/shellui-dev/shell-icons) | Lucide icons for Blazor, MAUI and Avalonia; the source of the `icon` component | -| | [ShellDocs](https://github.com/shellui-dev/shelldocs) | The docs framework behind the ShellUI sites | +| | [ShellUI](https://github.com/shellui-dev/shellui) | The Blazor component library this one mirrors | +| | [ShellIcons](https://github.com/shellui-dev/shell-icons) | Lucide icons for Blazor, MAUI and Avalonia; the source of the `icon` component | +| | [ShellDocs](https://github.com/shellui-dev/shelldocs) | The docs framework behind the ShellUI sites | Inspired by [shadcn/ui](https://ui.shadcn.com/). diff --git a/src/ShellUI.Native.CLI/README.md b/src/ShellUI.Native.CLI/README.md index d4cc501..fea6a1a 100644 --- a/src/ShellUI.Native.CLI/README.md +++ b/src/ShellUI.Native.CLI/README.md @@ -1,4 +1,4 @@ -![ShellUI Native](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/icon-64.png) +![ShellUI Native](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/icon.svg) # ShellUI Native CLI @@ -47,6 +47,6 @@ Components are written to `Components/UI/` as plain C# files you own. Avalonia s | | Project | | |---|---|---| -| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shellui.png) | [ShellUI](https://www.nuget.org/packages/ShellUI.CLI) | The Blazor component library this one mirrors | -| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shellicons.png) | [ShellIcons](https://www.nuget.org/packages/ShellIcons.Blazor) | Lucide icons for Blazor, MAUI and Avalonia | -| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shelldocs.png) | [ShellDocs](https://www.nuget.org/packages/ShellDocs.CLI) | The docs framework behind the ShellUI sites | +| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shellui.svg) | [ShellUI](https://www.nuget.org/packages/ShellUI.CLI) | The Blazor component library this one mirrors | +| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shellicons.svg) | [ShellIcons](https://www.nuget.org/packages/ShellIcons.Blazor) | Lucide icons for Blazor, MAUI and Avalonia | +| ![](https://raw.githubusercontent.com/shellui-dev/shellui-native/main/assets/brand/shelldocs.svg) | [ShellDocs](https://www.nuget.org/packages/ShellDocs.CLI) | The docs framework behind the ShellUI sites | From c2ce727589e66e7f78ac70b09b914eab445b53a9 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 11:23:28 +0200 Subject: [PATCH 10/11] docs: README header like ShellUI's, with logos that load from the repo The mark above the ASCII banner used absolute raw.githubusercontent.com URLs, which return 404 while the repository is private, so no logo showed. The header now matches ShellUI's: a with light and dark bare-dot logos (200 px, same dot size, spacing and off-dot opacity as ShellUI's) by relative path, a centred title, tagline and badges, and no ASCII banner. The NuGet badge shows prereleases. assets/icon-dark.png is no longer used. --- assets/icon-dark.png | Bin 8807 -> 0 bytes assets/logo-dark.png | Bin 0 -> 1236 bytes assets/logo-light.png | Bin 0 -> 1250 bytes docs/README.md | 41 +++++++++++++++++++---------------------- 4 files changed, 19 insertions(+), 22 deletions(-) delete mode 100644 assets/icon-dark.png create mode 100644 assets/logo-dark.png create mode 100644 assets/logo-light.png diff --git a/assets/icon-dark.png b/assets/icon-dark.png deleted file mode 100644 index 2d5ef9c46254195246fbefbe4f5b16551a0672b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8807 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 diff --git a/assets/logo-dark.png b/assets/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..399f3e11f812decf91273dbb0d0534ec736ce032 GIT binary patch literal 1236 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fn~O*i(^Q|t+#g_^A;P3uszt` z;Ho6TcdhpQwBRo~mfn00oozY0x(>+Ad-z)=+ay$c|D6deZ@$Yg0!=^$oIaPonD+m+ zk^5ZX_uppv|NV9URFCUf?y8J=W_aeYS%2Zr@`$>7b2X34i68s^$^HM4*WdR&{h@F9 z>*-PRI)k|V*X1pb$B7@Wje4%P%rvb-(ZZy-!zsn_GX`)?u2A`>(34ud-WZuI|6K z=2z8Z)#LNTPi(%#{YdlpGMVjO`e&lMEx+w?ST!*@xNqaN48wMR0n3`E9qbbi+j>-g zSC}VP+4WR&+E@113fCOhEQ$8qJ}FOs+TMt7NjJ}Gx8L3K%JE46|Dr_!e4?$#S7?NA z%Vk}1_T81Iv~acOi!3HsI2In-8xi?UZf5@m@zd`@KMR(9UR7@(zfjJFb?(oAz={pC zzUY|Vc&2=1$?Hdnc242KwF}O)?~6EF9(LxE=Y^VF^zNCn?l0cG zv*GzHc#ger(WP0!Q_r|tB%ZQf_VWYhBkS$e0zgM6fgN2cfZ=G%#8Y{3yDR4!ER}Py z?GRoTa$#nFK;R0t+chn-N@8u!xFb9I-rk6!6N%>QWr|j?Pjm+Qu;@gf{_&gHm!3{= z4i~n3azWgi&ywqzMRmF>kTW}+&vMo-anCDfWB4rR@GG^;WEWW^dhr26^Ms;%^?ZeW zuTMRcJ91gb(&p$UKBe|j0qvJXK4F*M9-ZXOx4RN(rfr8zkx4xNq9Wfupqsu$_61B{ zSKTJFa5X0^99-wJXPqG%R{?fj~be8hzoiZS<(KV^wXEP?vd6QyS(^PTX zME;#)&GRz;OJ8g|mPddhDpmG~RR<_A!Y-{{a=SYfl)B13-`bmHccr=I7yIXhiwr=q z)C`O>wOHR>z-aO0F5n4$$RJhb7loMRyB z7cwhsj)5vLBg|JQ+QDAP|48l#XD~QTwP=D;)Et9U)w^8Vrj}H1eABo|{4~(!4E5cW zSDRO`&%DwM%mslO?X$iFOx9~(r?zl)rwq)cFnY;H?)?dSL;swVUkDQNboFyt=akR{ E0PycTKmY&$ literal 0 HcmV?d00001 diff --git a/assets/logo-light.png b/assets/logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..865c31c019d61ad96cad6a3ef28f1e2bd9488e03 GIT binary patch literal 1250 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fn}wqi(^Q|t+#j2<~}wMV7p*> zgI&5QwCD5*+r3M>)*juugh79EK!dAa+^iVhm){wb93~uQKW0DagrpnL3}oQIm!DcQ zrBM6v=APf*K5zT`&$sXY{Kr*$!&31zrSt&_uqjXx%K+) z&t;ynmfwl1dCmOz_4_{K=QmCtZ`N;_|Jb>&&T^aoldVZ}(#~w09zRvT?fpmZzHgSN zx?jiqT=Ylo`OKo8Z)Y$5njG9`HvfjN{lU#oZhW5i_+G)D(;sX4-bU5uTJ$MDzj1l( zNA5>z$3I*Cd+7G#;>Mmi$>-g7e&>7a+xJW6w(^y&hb?+$B)3)fr?21p^hNR6|5t1s zrpXw`#$AmIJ8|**re&`_sd@L=)|OQ{)|?8Q|9Jh>m)9N}CH2|fwdS+bDlVxC{3ogW z>;tb-d%nPTg`yqolJDh?TvqnqXmfPazXaJMRvlAIv|ol@Ivl4{z46V_O|!%Kc2_#v zcF4SP3=uiCa8bdeE~l*z7OL4^S@K#j?x79O=Z7^D3Kj*O^>ll7m^0YU@GQ{L*YCV@ywhCK^;C1(@`!7W zYnmh2H%`h^z3WwVqdQf9+TIAi8C<(7-##lnG^I-5tAgK*puh~f5Rp{}&E&H#m3}mk z-!%*7V23?#;&d#ZaO|#pIzjnL^9e=w6&h2V`_@^;ZC8usvn(;4=-gLuXo^hHmUAC> z%@ut*0q8u-ClVjxR9wxAf%=|ni9?>FqbowA<4XZ?Qbb8~=Vai;Q?_hgo{|7Wy6o_S0r@|FA<_dfIEJ3r0;V0+H^ zX@`nv$+P4)`F+dgM?@B-tOSNZ71>jG;CztIvj)IfJtf3{!bJul7fwEG(KLNoYiIcsn50TRPH>K2bAjE9Pu=I`8i`yh&QlA`^Iy1X z1C!Ij*_CHgTV>9%b3RWz@vvnjC|z}W75!lQ2+RxBz`XFzagG5vQTff-#Fr#{gflqo z(%OV*&+X#cZyLFPPJYI_JLB!MQ2s?g+nZNtgam4|&-wz2zjbO0S9i+5TneLAK1v7j VBs=>qt_}wYdAj - - - - - - -``` -โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— -โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• -โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— -โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ•šโ•โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•”โ•โ•โ• -โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— -โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ• -``` - -**One design system, every platform** - -[![NuGet](https://img.shields.io/nuget/v/ShellUI.Native.CLI.svg)](https://www.nuget.org/packages/ShellUI.Native.CLI) -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../LICENSE) - - +

+ + + + ShellUI Native logo + +

+ +

ShellUI Native

+ +

+ One design system, every platform.
+ Copy-and-own native components for .NET MAUI, inspired by shadcn/ui. +

+ +

+ ShellUI.Native.CLI on NuGet + MIT license +

ShellUI Native is the native cross-platform extension of [ShellUI](https://shellui.dev/), bringing the same design system and component philosophy to MAUI, Avalonia, and other native platforms. Inspired by [shadcn/ui](https://ui.shadcn.com/)'s approach, ShellUI Native provides copy-and-own components for native desktop and mobile development. From 51d21accc46343871e4b0abe4685a45c26604429 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 7 Oct 2026 11:35:46 +0200 Subject: [PATCH 11/11] ci(release): publish through NuGet Trusted Publishing Same setup as ShellDocs: the release job runs in the 'release' environment with id-token: write, and NuGet/login exchanges the GitHub OIDC token for a one-hour API key, so no long-lived NUGET_API_KEY secret is stored; only NUGET_USER (the nuget.org profile name). Also from ShellDocs: a manual dry run that packs and validates without publishing, and a check that stops the run when the version is already on nuget.org. docs/RELEASING.md has the one-time setup. --- .github/workflows/release.yml | 75 ++++++++++++++++++++++++++--------- docs/ARCHITECTURE.md | 6 +-- docs/DEVELOPMENT_PLAN.md | 3 +- docs/README.md | 1 + docs/RELEASING.md | 59 +++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 docs/RELEASING.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e22d85a..cd2efcf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,45 +1,60 @@ name: Release +# Fires on v*.*.* tags (with prerelease suffixes). Version comes from Directory.Build.props. +# Publishes via NuGet Trusted Publishing (OIDC, no long-lived API key). Setup in docs/RELEASING.md. on: push: tags: - - 'v*' - -permissions: - contents: write + - 'v[0-9]+.[0-9]+.[0-9]+*' + workflow_dispatch: + inputs: + dry_run: + description: "Pack and validate only โ€” skip nuget push" + type: boolean + default: true concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: - publish: + release: runs-on: ubuntu-latest if: github.event.repository.fork == false + environment: release + permissions: + contents: write # for creating the GitHub Release + id-token: write # required for the Trusted Publishing OIDC exchange + env: + PUBLISH: ${{ github.event_name == 'push' || inputs.dry_run == false }} steps: - uses: actions/checkout@v4 - - name: Extract version from tag - id: version - run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - - - name: Extract release notes for this version - run: bash scripts/extract-release-notes.sh "${{ steps.version.outputs.VERSION }}" > "$RUNNER_TEMP/release-body.md" - - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 10.0.x - - name: Check package version matches tag + - name: Read version + id: version run: | v=$(dotnet msbuild src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj -getProperty:Version) - if [ "$v" != "${{ steps.version.outputs.VERSION }}" ]; then - echo "::error::The CLI builds version $v but the tag is v${{ steps.version.outputs.VERSION }}. Update Directory.Build.props." + echo "Directory.Build.props version: $v" + echo "VERSION=$v" >> "$GITHUB_OUTPUT" + + - name: Check tag matches version + if: github.event_name == 'push' + run: | + tag="${GITHUB_REF#refs/tags/v}" + if [ "$tag" != "${{ steps.version.outputs.VERSION }}" ]; then + echo "::error::The CLI builds version ${{ steps.version.outputs.VERSION }} but the tag is v$tag. Update Directory.Build.props." exit 1 fi + - name: Extract release notes for this version + run: bash scripts/extract-release-notes.sh "${{ steps.version.outputs.VERSION }}" > "$RUNNER_TEMP/release-body.md" + - name: Restore dependencies (src + tests) run: | dotnet restore src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj @@ -57,17 +72,39 @@ jobs: - name: Pack CLI tool run: dotnet pack src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj --no-build --configuration Release -p:ContinuousIntegrationBuild=true -o ./nupkg + # nuget.org locks a version forever after first publish, and --skip-duplicate would skip it silently. + - name: Verify version is not already published + if: env.PUBLISH == 'true' + run: | + v="${{ steps.version.outputs.VERSION }}" + existing=$(curl -sf "https://api.nuget.org/v3-flatcontainer/shellui.native.cli/index.json" | grep -oE '"[^"]*"' | tr -d '"' || true) + if echo "$existing" | grep -Fxiq "$v"; then + echo "::error::ShellUI.Native.CLI $v is already on nuget.org. Bump Directory.Build.props and re-tag." + exit 1 + fi + + # Right before the push: the temporary API key is valid for one hour. + # `user` is the nuget.org profile name (not the email, not the GitHub org), kept as a secret. + - name: Login to NuGet via Trusted Publishing + if: env.PUBLISH == 'true' + id: nuget-login + uses: NuGet/login@v1 + with: + user: ${{ secrets.NUGET_USER }} + - name: Publish CLI to NuGet - run: dotnet nuget push ./nupkg/ShellUI.Native.CLI.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + if: env.PUBLISH == 'true' + env: + NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} + run: dotnet nuget push ./nupkg/ShellUI.Native.CLI.*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate - name: Create GitHub Release + if: github.event_name == 'push' uses: softprops/action-gh-release@v2 with: name: ShellUI Native v${{ steps.version.outputs.VERSION }} body_path: ${{ runner.temp }}/release-body.md draft: false - prerelease: ${{ contains(github.ref, '-') }} + prerelease: ${{ contains(steps.version.outputs.VERSION, '-') }} files: | ./nupkg/*.nupkg - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d10dab4..07dba0c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -210,6 +210,6 @@ so `shellui-native.json` records the CLI version each component was installed wi **Releasing:** set the version in `Directory.Build.props`, add a `# ShellUI Native v` section to [RELEASE_NOTES.md](./RELEASE_NOTES.md), merge to `main`, then push a `v` tag. -`.github/workflows/release.yml` checks that the tag matches the props version, runs the tests, -publishes `ShellUI.Native.CLI` to NuGet and creates the GitHub release (a prerelease when the -version has a `-`) with the notes as its body. +`.github/workflows/release.yml` checks the tag, runs the tests, publishes `ShellUI.Native.CLI` +through NuGet Trusted Publishing and creates the GitHub release. Setup and steps are in +[RELEASING.md](./RELEASING.md). diff --git a/docs/DEVELOPMENT_PLAN.md b/docs/DEVELOPMENT_PLAN.md index e17bea8..4018744 100644 --- a/docs/DEVELOPMENT_PLAN.md +++ b/docs/DEVELOPMENT_PLAN.md @@ -426,7 +426,8 @@ First NuGet prerelease of `ShellUI.Native.CLI`, MAUI only. - [x] Component versions read from the assembly (an installed tool has no `Directory.Build.props`) - [x] `release.yml` builds the CLI and tests (not the bare `src/` folder), checks the tag against the props version, runs the tests and uses [RELEASE_NOTES.md](./RELEASE_NOTES.md) as the - GitHub release body (`scripts/extract-release-notes.sh`) + GitHub release body (`scripts/extract-release-notes.sh`); publishes through NuGet Trusted + Publishing like ShellDocs (setup in [RELEASING.md](./RELEASING.md)) - [x] Install docs use `--prerelease`; roadmap and plan brought up to date - [x] Every CLI target added alone to a fresh MAUI library, plus all together in a fresh MAUI app diff --git a/docs/README.md b/docs/README.md index d03570b..a094598 100644 --- a/docs/README.md +++ b/docs/README.md @@ -86,6 +86,7 @@ They're separate rendering contexts with no conflict. Install what you need base - [Getting Started](./QUICKSTART.md) - [Component List](./COMPONENTS.md) - [Release Notes](./RELEASE_NOTES.md) +- [Releasing](./RELEASING.md) โ€” how a version is published - [Components Roadmap](./COMPONENTS_ROADMAP.md) โ€” prioritized P0โ€“P7 backlog - [Development Plan](./DEVELOPMENT_PLAN.md) โ€” branch strategy & phase breakdown - [Architecture](./ARCHITECTURE.md) diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..cf3fe34 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,59 @@ +# Releasing ShellUI Native + +Runbook for publishing `ShellUI.Native.CLI` to NuGet. Same setup as ShellDocs. + +## One-time setup + +Publishing uses **NuGet Trusted Publishing**: each run of `.github/workflows/release.yml` exchanges a GitHub OIDC token for a NuGet API key that lasts one hour. No long-lived API key is stored. Official docs: . + +### 1. Create the `release` environment on GitHub + +Repo โ†’ Settings โ†’ Environments โ†’ **New environment** โ†’ name it exactly `release`. Nothing else is required (optionally add required reviewers for a manual gate on each publish). + +### 2. Add a Trusted Publishing policy on nuget.org + +Sign in โ†’ your username โ†’ **Trusted Publishing** โ†’ **Add**, choose the owner of the package, then (case-insensitive): + +| Field | Value | +|---|---| +| Repository Owner | `shellui-dev` | +| Repository | `shellui-native` | +| Workflow File | `release.yml` (file name only, no `.github/workflows/`) | +| Environment | `release` (must match `environment: release` in the workflow) | + +A policy for a private repository starts as provisional for 7 days; the first successful publish locks it to the repository. If nothing is published in 7 days it goes inactive and can be restarted. + +### 3. Add the `NUGET_USER` secret + +Repo โ†’ Settings โ†’ Secrets and variables โ†’ Actions โ†’ **New repository secret** (or add it to the `release` environment): + +| Name | Value | +|---|---| +| `NUGET_USER` | Your nuget.org profile name, as in `nuget.org/profiles/`; not the email and not the GitHub org | + +No `NUGET_API_KEY` secret is needed; delete an old one if it exists. + +## Releasing a version + +1. Set the version in `Directory.Build.props` (`ShellUINativeVersion` and `ShellUINativeVersionSuffix`). +2. Add a `# ShellUI Native v` section to [RELEASE_NOTES.md](./RELEASE_NOTES.md). It becomes the GitHub release text. +3. Merge to `main`, then tag and push: + + ```bash + git switch main && git pull --ff-only + git tag -a v -m "ShellUI Native v" + git push origin v + ``` + +The tag push runs the workflow: it checks the tag matches the props version, extracts the release notes, builds, runs the tests, packs the CLI, stops if that version is already on nuget.org, logs in via Trusted Publishing, pushes the package and creates the GitHub release (a prerelease when the version has a `-`). + +If the login step fails, check in this order: `NUGET_USER` missing or wrong, the policy's Workflow File has a path prefix, the policy's Environment doesn't match `release`, the policy expired (provisional, private repo). + +## Dry run + +Actions โ†’ Release โ†’ **Run workflow** with "Pack and validate only" checked: runs the version, release-notes, build, test and pack steps and skips the version check, login, push and GitHub release. + +## After the release + +- The package shows at after indexing (a few minutes). +- Check the install in a scratch folder: `dotnet tool install -g ShellUI.Native.CLI --prerelease`.