diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index d07e7b5..b8fbd83 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -17,18 +17,28 @@ jobs: with: dotnet-version: 10.0.x + # The example projects (examples/MAUI.Demo) need the MAUI workload and are covered + # by the separate build-examples job in ci.yml. This validate job stays lightweight — + # library projects + tests only, no workload install required. + - name: Check formatting - run: dotnet format src/ --verify-no-changes --verbosity diagnostic + run: dotnet format src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj --verify-no-changes --verbosity diagnostic continue-on-error: true - name: Restore - run: dotnet restore src/ + 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 --warnaserror + run: | + dotnet build src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj --no-restore --configuration Release --warnaserror + dotnet build tests/ShellUI.Native.Tests/ShellUI.Native.Tests.csproj --no-restore --configuration Release --warnaserror - name: Check for package vulnerabilities - run: dotnet list src/ package --vulnerable --include-transitive + run: | + dotnet list src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj package --vulnerable --include-transitive + dotnet list tests/ShellUI.Native.Tests/ShellUI.Native.Tests.csproj package --vulnerable --include-transitive continue-on-error: true # Label PR based on files changed diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2a86a6c..6c1108f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -68,17 +68,22 @@ public class ButtonTemplate Dependencies = new List { "button-variants" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; -// ... component code"; + // Template System v2: content is keyed by target NativePlatform. Adding Avalonia + // support to a component means adding one dict entry here — no registry edit needed. + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; +// ... MAUI component code", + // [NativePlatform.Avalonia] = @"..." // added in Phase 2 + }; } ``` -**Gap today (Template System v2 target):** `Content` is a single MAUI-flavored string, and -`ComponentRegistry.GetComponentContent` has no platform parameter — it can't yet return -different code for Avalonia vs. MAUI. Adding Avalonia support requires evolving this to -something like `ButtonTemplate.Content(NativePlatform)` or per-platform static properties -(`ButtonTemplate.MauiContent` / `ButtonTemplate.AvaloniaContent`) selected by -`config.TargetPlatform`. See [PLAN.md](./PLAN.md) and [DEVELOPMENT_PLAN.md](./DEVELOPMENT_PLAN.md). +**Registry lookup (Template System v2):** `ComponentRegistry.GetComponentContent(name, NativePlatform)` +returns the source for the target platform, or `null` if the component doesn't exist OR exists +but has no template for that platform. Callers use `SupportsPlatform(name, platform)` and +`GetSupportedPlatforms(name)` to distinguish those two failure modes. Landed on +`feat/template-system-v2` (2026-07-18). ### 2. Namespace Replacement diff --git a/docs/COMPONENTS_ROADMAP.md b/docs/COMPONENTS_ROADMAP.md index a48a758..110eb68 100644 --- a/docs/COMPONENTS_ROADMAP.md +++ b/docs/COMPONENTS_ROADMAP.md @@ -2,6 +2,51 @@ 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-08-29** (on `feat/template-system-v2`). + +--- + +## Form Sizing Contract + +Every single-line form control renders at **40px** high (matches shadcn `h-10` / default `ButtonSize.Default`). Multi-line controls use `MinimumHeightRequest`. Horizontal padding is 12px; vertical padding is 0 when `HeightRequest` is fixed (the fixed height owns the vertical rhythm), 8px when it isn't. + +| Component | Height | Padding | Notes | +|-----------|--------|---------|-------| +| `Button` (Default) | 40 (from `ButtonStyle.Height`) | `(16, 10)` | Variant-driven: Sm=36, Lg=44, Icon=40×40 | +| `Input` | 40 | `(12, 0)` | Fixed 2026-08-29 — was unset + `(12, 8)`, rendered inconsistent across platforms | +| `Select` | 40 | `(12, 0)` | | +| `DatePicker` | 40 | `(12, 0)` | | +| `TimePicker` | 40 | `(12, 0)` | | +| `Textarea` | `MinimumHeightRequest=80` | `(12, 8)` | Multi-line — grows with content | +| `Checkbox` | 20×20 (box) | — | Icon-shaped, not a field | +| `RadioGroupItem` | 20×20 (dot) | — | Icon-shaped, not a field | + +**Rule for new form controls:** if it visually sits in a form row next to `Input`, it MUST be 40px tall. If it's a compositional container that hosts its own field (e.g. `Combobox`, `InputOTP`), the inner field carries the 40. Don't rely on platform default heights — MAUI's `Entry` / `Picker` defaults vary wildly across Android / iOS / Windows. + +**Menu / list rows** (`DropdownItem`, `SelectItem` when added, `ContextMenuOption`) use `MinimumHeightRequest = 40` plus symmetric `Padding = (12, 12)` — they need a real touch target (44px iOS / 48dp Android guidance), not just enough space to draw the text. + +**Icon-shaped controls** (`Checkbox` box 20×20, `RadioGroupItem` dot 20×20, `Switch` track 44×24) keep their exact pixel dimensions — those numbers are the design, not a fill. Their outer row inherits its hit area from the surrounding `HorizontalStackLayout`, which currently follows the icon height. If a future accessibility pass needs 44px touch targets for these, expand the container's `MinimumHeightRequest`, not the icon size. + +--- + +## Design Token Contract + +Templates hardcode ARGB strings today (no shared token file until Phase 2 lands the Avalonia `ResourceDictionary`). While we're still copy-pasting them, they MUST agree — otherwise "primary" reads as two different blues across the demo. + +| Token | Value | Used by | +|-------|-------|---------| +| `Primary` | `#2563EB` | Button (Default), Checkbox (fill+border when checked), RadioGroupItem (fill+border when checked, fixed 2026-08-29), Switch (track when on), Progress (Default fill), Input (focus border, fixed 2026-08-29) | +| `Destructive` | `#EF4444` | Button (Destructive), Badge (Destructive), Progress (Destructive), Input (error border), Alert title (Destructive) | +| `Border` | `#E5E7EB` | Input (idle), Card (Default/Bordered), Checkbox (unchecked ring), RadioGroupItem (unchecked ring), Separator, Alert (Default) | +| `Muted background` | `#F3F4F6` | Badge (Secondary bg), Alert (Default bg) | +| `Foreground` | `#1F2937` | Input text, Label (Default), Checkbox label, Switch label, Alert title (Default) | +| `Muted foreground` | `#6B7280` | Progress label, Label (Muted variant) | +| `Placeholder` | `#9CA3AF` | Input placeholder | +| `Success` | `#22C55E` | Badge/Alert/Progress Success | +| `Warning` | `#F59E0B` | Badge/Alert/Progress Warning | + +**Rule:** never introduce a new hex for a role that already has a token above. If you need a token that isn't listed, add it here first, then use it — that keeps Phase 2's `ResourceDictionary` extraction mechanical (grep the hex, replace with `{DynamicResource ShellUIPrimary}` in Avalonia XAML). + --- ## Architectural Pattern: Dependencies over ChildComponent @@ -41,7 +86,7 @@ Prioritized list of components to create for ShellUI Native, aligned with [Shell | shell | ✅ Done | — | Shell | | button | ✅ Done | button-variants | Button | | button-variants | ✅ Done | — | ButtonVariants | -| input | ✅ Done | — | Input | +| input | ✅ Done (height fix 2026-08-29) | — | Input | | label | ✅ Done | — | Label | | checkbox | ✅ Done | — | Checkbox | | switch | ✅ Done | — | Switch | diff --git a/docs/DEVELOPMENT_PLAN.md b/docs/DEVELOPMENT_PLAN.md new file mode 100644 index 0000000..f4cf6af --- /dev/null +++ b/docs/DEVELOPMENT_PLAN.md @@ -0,0 +1,302 @@ +# ShellUI Native — Development Plan + +Living document tracking **branches**, **phases**, and **feature implementations** for +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-29**. + +--- + +## Branch Strategy + +We use a phase-anchored branch model. `main` stays shippable; each phase lives on a +long-lived `feat/*` branch that is squash-merged into `main` when its exit criteria are hit. +Short-lived sub-branches (`feat//`) cut off the phase branch and merge back +into it, not directly into `main`. + +``` +main ← Phase 1a merged (2026-07-05) + └─ feat/template-system-v2 ← Phase 1b (active) — per-platform template keying + └─ feat/avalonia-implementation ← Phase 2 (planned) — Avalonia templates + reference impl + └─ feat/winui ← Phase 3 (conditional) +``` + +**Rules:** +- Never merge a phase branch to `main` without a green CI run and the exit criteria table checked. +- Never open a Phase 2 slice while Phase 1b (template system v2) is unmerged — it's the + prerequisite. +- Docs (`docs/*.md`) may change on any branch; prefer conflict-free additions over edits to + shared prose during parallel work. + +--- + +## Current Snapshot + +| Branch | Base | Status | Purpose | +|--------|------|--------|---------| +| `main` | — | Phase 1a merged | 44 components on .NET 10 + xUnit tests + Avalonia-ready platform model | +| `feat/template-system-v2` | `main` | **Active** | Phase 1b — per-platform template keying (this branch) | + +--- + +## Phase 1a — `feat/initial-foundation` (**merged 2026-07-05**) + +The MAUI-only alpha foundation. Merged to `main` via PR #1. + +### Delivered + +**Infrastructure** +- [x] .NET 10 unified across `Core`, `Templates`, `CLI`, and `examples/MAUI.Demo` +- [x] [`global.json`](../global.json) pins SDK `10.0.100` +- [x] Solution migrated from `.sln` to [`.slnx`](../ShellUI.Native.slnx) +- [x] CI workflows on `dotnet-version: 10.0.x` + ([ci.yml](../.github/workflows/ci.yml), [pr-check.yml](../.github/workflows/pr-check.yml), + [release.yml](../.github/workflows/release.yml)) +- [x] MAUI demo builds on Windows: `WindowsPackageType=None`, explicit + `RuntimeIdentifier=win-x64`, explicit `Microsoft.Maui.Controls` + + `Microsoft.Extensions.Logging.Debug` PackageReferences + +**Platform model** +- [x] `NativePlatform.Avalonia` added to the enum + ([NativePlatform.cs](../src/ShellUI.Native.Core/Models/NativePlatform.cs)) +- [x] Avalonia project detection via `PackageReference` prefix or `App.axaml` presence + ([ProjectDetector.cs](../src/ShellUI.Native.CLI/Services/ProjectDetector.cs)) +- [x] Avalonia option in the `init` manual-selection prompt + ([InitService.cs](../src/ShellUI.Native.CLI/Services/InitService.cs)) +- [x] CLI description text reflects MAUI+Avalonia + ([Program.cs](../src/ShellUI.Native.CLI/Program.cs)) +- [x] MAUI-only warning in `ComponentInstaller` for non-MAUI target platforms + ([ComponentInstaller.cs](../src/ShellUI.Native.CLI/Services/ComponentInstaller.cs)) + +**Components (see [COMPONENTS_ROADMAP.md](./COMPONENTS_ROADMAP.md) for full priority list)** +- [x] P0 complete: `shell`, `button`, `input`, `label`, `checkbox`, `switch`, `card` (+ header, content, footer), `separator`, `badge`, `progress`, `alert` +- [x] P1 complete: `dialog` (+ family), `drawer` (+ family), `sheet` (+ family), `dropdown` (+ family), `popover` (+ family) +- [x] P2 partial: `textarea`, `slider`, `select`, `radio-group` (+ item), `date-picker`, `time-picker` + +**Template hygiene** +- [x] All 18 `RoundRectangle`-using templates now include `using Microsoft.Maui.Controls.Shapes;` + in their `Content` string (previously produced non-compiling code on install) +- [x] All 14 compositional-container `Children` properties (Card, CardFooter, Dialog family, + Drawer/Sheet/Popover/DropdownContent, RadioGroup — in both templates and demo copies) now + use `public new IList Children`, silencing CS0108 shadow warnings against + `TemplatedView.Children` +- [x] `Switch.IsEnabledProperty` and `Checkbox.IsEnabledProperty` marked `static new` — they + are intentional shadows of `VisualElement.IsEnabledProperty` so the components can observe + their own enable-state changes via `propertyChanged` handlers +- [x] `Switch` now uses `_ = TranslateToAsync(...)` instead of the obsolete `TranslateTo` +- [x] **Cold multi-TFM build**: `dotnet build ShellUI.Native.slnx -c Release` → **0 warnings, + 0 errors** across net10.0-android / ios / maccatalyst / windows10.0.19041.0 + +**Tests** +- [x] xUnit test project at [`tests/ShellUI.Native.Tests`](../tests/ShellUI.Native.Tests) — + 147 tests covering: + - Every registered component has non-empty `Content` + - Every template using `RoundRectangle` imports `Microsoft.Maui.Controls.Shapes` + (regression lock for the 2026-07-04 bug) + - Every template has a `YourProjectNamespace` placeholder (namespace replacement contract) + - Every metadata `Name` matches its registry key + - Every declared dependency is itself registered + - `ProjectDetector.DetectPlatform` maps `.csproj` XML → `NativePlatform` correctly for + MAUI / Avalonia (both PackageReference + `App.axaml` paths) / WinUI / WPF / Unknown, and + prefers MAUI when a project declares both `UseMaui=true` and an `Avalonia` PackageReference +- [x] `ProjectDetector.DetectPlatform` promoted to `internal` + `InternalsVisibleTo` in + [ShellUI.Native.CLI.csproj](../src/ShellUI.Native.CLI/ShellUI.Native.CLI.csproj) so tests + hit the pure XML→enum core without cwd manipulation +- [x] CI runs `dotnet test` and uploads TRX + coverage results + ([ci.yml](../.github/workflows/ci.yml)) + +### In progress / remaining before merge to `main` + +- [x] `Element.FindParentOfType()` is implemented in + [`ElementExtensionsTemplate`](../src/ShellUI.Native.Templates/Templates/ElementExtensionsTemplate.cs) + and auto-installed as a dependency by every overlay component (Dialog, Drawer, Sheet, + Dropdown, Popover). Verified. +- [x] Windows CI head builds `MAUI.Demo` end-to-end via the `build-examples` job + ([ci.yml](../.github/workflows/ci.yml)) — runs `dotnet workload install maui` then + `dotnet build … --framework net10.0-windows10.0.19041.0`. +- [x] [COMPONENTS.md](./COMPONENTS.md) reconciled with the registry — removed stale + "Coming Soon" entries, added docs for `textarea`, `slider`, `select`, `radio-group`, + `date-picker`, `time-picker`, updated category summary. +- [x] [QUICKSTART.md](./QUICKSTART.md) — prerequisites now say .NET 10 SDK; platform + detection line mentions Avalonia and links the "MAUI-only warning" behavior. +- [ ] Manually run `shellui-native init --yes` + `add button input card dialog` against a + fresh `dotnet new maui` project to confirm the end-to-end install path works — needs a + human on a workstation with the MAUI workload installed. +- [ ] Manually launch `MAUI.Demo` at least once to verify components render as expected + (compilation is verified by CI, rendering is not). + +### Exit criteria to merge → `main` + +1. `dotnet build ShellUI.Native.slnx` green +2. `dotnet build examples/MAUI.Demo/MAUI.Demo.csproj --framework net10.0-windows10.0.19041.0` green in CI +3. `shellui-native init --yes` + `shellui-native add button input card dialog` works end-to-end on a fresh MAUI project +4. `COMPONENTS.md` is honest about what's shipped + +--- + +## Phase 1b — `feat/template-system-v2` (**active — this branch**) + +**Prerequisite for any Phase 2 work.** Focused refactor: the template layer becomes platform-keyed +so adding Avalonia is a per-template dict entry rather than a whole parallel registry. + +### Problem (context) + +Before this branch, [`ComponentRegistry.GetComponentContent`](../src/ShellUI.Native.Templates/ComponentRegistry.cs) +returned a single MAUI-flavored C# string per component name. There was no platform dimension in +the lookup path. Adding Avalonia templates would have meant either duplicating the registry or +string-mangling MAUI templates at install time. + +### Design + +Per-template `Contents` dictionary owned by the template class; registry stores +`(Metadata, Contents)` bundles so lookup is one hop — no per-component switch arms to maintain +when a new platform is added. + +```csharp +public static class ButtonTemplate +{ + public static ComponentMetadata Metadata => new() { ... }; + public static IReadOnlyDictionary Contents { get; } = new() + { + [NativePlatform.MAUI] = @"..." + // [NativePlatform.Avalonia] = @"..." ← added in Phase 2, one line, no registry edit + }; +} +``` + +### Deliverables + +- [x] `ComponentRegistry.GetComponentContent(name, NativePlatform)` signature +- [x] Every template migrated from `public static string Content =>` to + `public static IReadOnlyDictionary Contents { get; }` (all 44 templates) +- [x] Registry stores `(Meta, Contents)` bundles instead of just metadata; adds + `SupportsPlatform(name, platform)` and `GetSupportedPlatforms(name)` +- [x] `ComponentInstaller` uses `config.TargetPlatform`; checks `SupportsPlatform` before + installing and prints a clear error listing supported platforms if the combo isn't ready +- [x] `InitService.InstallShellUtilityAsync` and `ComponentManager.UpdateComponents` updated + to pass the target platform through +- [x] New `UnsupportedPlatformException` typed exception in + [Core.Models](../src/ShellUI.Native.Core/Models/UnsupportedPlatformException.cs) with + `ComponentName`, `RequestedPlatform`, `SupportedPlatforms` for programmatic callers +- [x] Blanket "MAUI-only" warning removed from `ComponentInstaller` — replaced with per-component + platform check (silent when the platform IS supported) + +### Verification + +- [x] `dotnet build src/` → 0 warnings, 0 errors +- [x] `dotnet test` → **199/199 passing** (up from 147 — 52 new/updated tests): + - `TemplateContentTests` parameterized on `NativePlatform.MAUI` + - `Every_component_reports_MAUI_as_a_supported_platform` — regression lock + - `GetComponentContent_returns_null_for_known_component_but_unsupported_platform` — verifies + the null return contract for the Avalonia case that will flip once Phase 2 adds content + - `SupportsPlatform_reports_MAUI_for_all_components_and_no_others_yet` — will fail-loud on + Phase 2 when Avalonia content is added (intended trigger to update the test) + - `UnsupportedPlatformExceptionTests` — message formatting + property exposure + +### Follow-ups landed on this branch + +- [x] **Input height fix (2026-08-29):** [`InputTemplate`](../src/ShellUI.Native.Templates/Templates/InputTemplate.cs) + now sets `HeightRequest = 40` and `Padding = (12, 0)` on its `Border`, matching + `Select` / `DatePicker` / `TimePicker`. Previously the `Border` had no explicit height + and used `Padding = (12, 8)`, so the composite grew to whatever platform-default height + `Entry` picked (Windows ~44, Android ~48+ with material padding) — inputs rendered + visibly taller than the pickers sitting next to them in the same form row. Rule captured + in [COMPONENTS_ROADMAP.md § Form Sizing Contract](./COMPONENTS_ROADMAP.md#form-sizing-contract). + Tests still green (199/199). + +- [x] **P0/P1 sweep (2026-08-29):** full re-read of every P0 + P1 template looking for the + same class of drift Input had. Three real bugs found and fixed: + 1. **Primary-blue token drift.** [`InputTemplate`](../src/ShellUI.Native.Templates/Templates/InputTemplate.cs) + focus border and [`RadioGroupItemTemplate`](../src/ShellUI.Native.Templates/Templates/RadioGroupItemTemplate.cs) + checked indicator used `#3B82F6` (blue-500) while `Button` / `Checkbox` / `Switch` / + `Progress` all use `#2563EB` (blue-600) for the same "primary" role. Standardized on + `#2563EB` and locked the mapping in + [COMPONENTS_ROADMAP.md § Design Token Contract](./COMPONENTS_ROADMAP.md#design-token-contract). + 2. **`DropdownItem` hit target too small.** Was `Padding = (8, 10)` with no minimum + height → ~34px rows, below iOS 44px / Android 48dp touch guidance. Now + `Padding = (12, 12)` + `MinimumHeightRequest = 40`. Rule added to Form Sizing Contract + under "Menu / list rows". + 3. **`RadioGroupItem` checked-border missing.** Only the fill flipped color when a radio + became selected; the ring stayed grey. `Checkbox` had already handled this. Fixed to + match — both stroke and background flip to `#2563EB` when checked. + + Everything else surveyed (Badge, Progress, Alert, Card + variants, Label, Separator, + Dialog/Drawer/Sheet/Popover content boxes, Shell) was sized/tokenized correctly. Build + clean, 199/199 tests still passing. + +### Exit criteria to merge → `main` + +1. All existing MAUI templates install and compile identically (verified: `TemplateContentTests` + parameterized over all 44 components pass) +2. `Registry.GetComponentContent("button", Avalonia)` returns `null` today (documented behavior); + will return non-null once Phase 2 adds Avalonia content +3. `ComponentInstaller` fails-loud on unsupported (name, platform) combos with a clear error + listing supported platforms +4. Form controls that share a row (`Input`, `Select`, `DatePicker`, `TimePicker`) render at + the same 40px height on Windows/Android/iOS heads of `MAUI.Demo` — visual check, not + yet automated + +--- + +## Phase 2 — `feat/avalonia-implementation` (blocked on Phase 1b) + +Cross-desktop (Windows + macOS + Linux) from one XAML codebase. + +### Deliverables + +- [ ] New `src/ShellUI.Native.Avalonia/` reference project (Avalonia 11.x) +- [ ] Avalonia design-token `ResourceDictionary` mirroring [`StyleTemplates.cs`](../src/ShellUI.Native.Templates/StyleTemplates.cs) +- [ ] Add `AvaloniaContent` to each of the ~40 templates, using the + `TemplatedControl`/`StyledProperty` pattern documented in + [ARCHITECTURE.md § Component Pattern (Avalonia)](./ARCHITECTURE.md) +- [ ] `examples/Avalonia.Demo/` reference app mirroring the MAUI demo +- [ ] CI job that builds the Avalonia demo on all three OSes (`ubuntu-latest`, `windows-latest`, `macos-latest`) + +### Order of implementation + +Same P0 → P7 order as MAUI, per [COMPONENTS_ROADMAP.md](./COMPONENTS_ROADMAP.md). Do not start +component `N` until MAUI has landed `N`. + +### Exit criteria + +1. Every P0+P1 MAUI component has a working Avalonia counterpart +2. `shellui-native init` in an Avalonia project installs Avalonia code (verified by grepping + installed files for `TemplatedControl`, not `ContentView`) +3. Avalonia demo builds & runs on Linux (the platform Phase 2 exists to unlock) + +--- + +## Phase 3 — `feat/winui` (conditional) + +Only pursued if a concrete requirement surfaces for Windows 11 Fluent-specific styling that +Avalonia's Fluent theme cannot satisfy. Default: **skip**, redirect that effort into +Avalonia's Windows head. + +If pursued, mirror Phase 2's structure: new project, `WinUIContent` per template, dedicated +demo, CI job. + +--- + +## Cross-cutting workstreams + +These aren't phases but ongoing concerns that touch every branch: + +| Concern | Owner branch | Notes | +|---------|--------------|-------| +| **Docs** | Whatever branch touches the feature | Prefer additions over prose rewrites on active parallel branches | +| **CI** | The branch introducing the new build target | Every new platform gets its own job, not a matrix | +| **Versioning** | `main` after every merge | Bump [Directory.Build.props](../Directory.Build.props) via [prepare-release.ps1](../prepare-release.ps1) | +| **Compositional pattern** | Component-authoring branches | Enforce Dialog+Trigger+Content style (see [COMPONENTS_ROADMAP.md § Architectural Pattern](./COMPONENTS_ROADMAP.md#architectural-pattern-dependencies-over-childcomponent)) | +| **Template hygiene** | Component-authoring branches | Every `RoundRectangle`/`Border`/`Path` usage must ship its `using` in the template `Content` string | + +--- + +## How to add a new branch to this plan + +1. Cut off the current phase branch (not `main`) unless you're starting a new phase. +2. Add a row to the Current Snapshot table above. +3. If it's a new phase, add a Phase section with **Deliverables** and **Exit criteria**. +4. Link the branch to the priority tier it delivers (P0–P7) in + [COMPONENTS_ROADMAP.md](./COMPONENTS_ROADMAP.md). +5. Update this doc's `Last revised` date. diff --git a/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs b/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs index c998458..73b8550 100644 --- a/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.Native.CLI/Services/ComponentInstaller.cs @@ -30,12 +30,6 @@ public static async Task InstallComponents(string[] components, bool force) var projectInfo = ProjectDetector.DetectProject(); - if (config.TargetPlatform != NativePlatform.MAUI) - { - AnsiConsole.MarkupLine($"[yellow]Warning:[/] component templates are currently MAUI-only. " + - $"'{config.TargetPlatform}' support is planned but not yet implemented — installed files will contain MAUI code."); - } - // Parse comma-separated components var componentList = new List(); foreach (var comp in components) @@ -163,7 +157,17 @@ private static async Task InstallComponentInternalAsync( return InstallResult.Skipped; } - var content = ComponentRegistry.GetComponentContent(componentName); + if (!ComponentRegistry.SupportsPlatform(componentName, config.TargetPlatform)) + { + var supported = ComponentRegistry.GetSupportedPlatforms(componentName); + var supportedList = supported.Count == 0 ? "(none)" : string.Join(", ", supported); + AnsiConsole.MarkupLine( + $"[red]Component '{componentName}' has no template for {config.TargetPlatform}.[/] " + + $"[dim]Supported platforms: {supportedList}. See docs/DEVELOPMENT_PLAN.md for the roadmap.[/]"); + return InstallResult.Failed; + } + + var content = ComponentRegistry.GetComponentContent(componentName, config.TargetPlatform); if (content == null) { AnsiConsole.MarkupLine($"[red]Failed to get content for '{componentName}'[/]"); diff --git a/src/ShellUI.Native.CLI/Services/ComponentManager.cs b/src/ShellUI.Native.CLI/Services/ComponentManager.cs index 7ac86eb..f1f2d92 100644 --- a/src/ShellUI.Native.CLI/Services/ComponentManager.cs +++ b/src/ShellUI.Native.CLI/Services/ComponentManager.cs @@ -171,7 +171,14 @@ public static void UpdateComponents(string[] components, bool all) continue; } - var content = ComponentRegistry.GetComponentContent(componentName); + if (!ComponentRegistry.SupportsPlatform(componentName, config.TargetPlatform)) + { + AnsiConsole.MarkupLine( + $"[yellow]Skipped '{componentName}': no template for {config.TargetPlatform}.[/]"); + continue; + } + + var content = ComponentRegistry.GetComponentContent(componentName, config.TargetPlatform); if (content == null) continue; content = content.Replace("YourProjectNamespace", projectInfo.RootNamespace); diff --git a/src/ShellUI.Native.CLI/Services/InitService.cs b/src/ShellUI.Native.CLI/Services/InitService.cs index aa2d265..ab0bcc3 100644 --- a/src/ShellUI.Native.CLI/Services/InitService.cs +++ b/src/ShellUI.Native.CLI/Services/InitService.cs @@ -130,7 +130,15 @@ await AnsiConsole.Status() private static async Task InstallShellUtilityAsync(ProjectInfo projectInfo, string componentsPath) { - var content = ComponentRegistry.GetComponentContent("shell"); + if (!ComponentRegistry.SupportsPlatform("shell", projectInfo.Platform)) + { + AnsiConsole.MarkupLine( + $"[yellow]Shell utility has no template for {projectInfo.Platform} yet — skipping.[/] " + + $"[dim]Add components once per-platform templates land.[/]"); + return; + } + + var content = ComponentRegistry.GetComponentContent("shell", projectInfo.Platform); if (content == null) return; content = content.Replace("YourProjectNamespace", projectInfo.RootNamespace); diff --git a/src/ShellUI.Native.Core/Models/UnsupportedPlatformException.cs b/src/ShellUI.Native.Core/Models/UnsupportedPlatformException.cs new file mode 100644 index 0000000..d28f896 --- /dev/null +++ b/src/ShellUI.Native.Core/Models/UnsupportedPlatformException.cs @@ -0,0 +1,28 @@ +namespace ShellUI.Native.Core.Models; + +// Thrown when a component template is requested for a platform it does not implement yet. +// e.g. `shellui-native add button` in an Avalonia project before Phase 2 templates exist. +public class UnsupportedPlatformException : Exception +{ + public string ComponentName { get; } + public NativePlatform RequestedPlatform { get; } + public IReadOnlySet SupportedPlatforms { get; } + + public UnsupportedPlatformException( + string componentName, + NativePlatform requestedPlatform, + IReadOnlySet supportedPlatforms) + : base(BuildMessage(componentName, requestedPlatform, supportedPlatforms)) + { + ComponentName = componentName; + RequestedPlatform = requestedPlatform; + SupportedPlatforms = supportedPlatforms; + } + + private static string BuildMessage(string name, NativePlatform requested, IReadOnlySet supported) + { + var supportedList = supported.Count == 0 ? "(none)" : string.Join(", ", supported); + return $"Component '{name}' does not have a template for {requested}. " + + $"Supported platforms: {supportedList}."; + } +} diff --git a/src/ShellUI.Native.Templates/ComponentRegistry.cs b/src/ShellUI.Native.Templates/ComponentRegistry.cs index 8dfaa37..1aec936 100644 --- a/src/ShellUI.Native.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Native.Templates/ComponentRegistry.cs @@ -3,139 +3,121 @@ namespace ShellUI.Native.Templates; -// Central registry of all available component templates +// Central registry of all available component templates. +// +// Template System v2: content is keyed by (componentName, NativePlatform). Each template class +// owns an IReadOnlyDictionary Contents, and the registry bundles +// (metadata, contents) so a name lookup is one hop — no per-component switch arms to maintain. +// Adding a platform to a component is one dictionary entry inside that template file. public static class ComponentRegistry { - public static readonly Dictionary Components = new() + private static readonly Dictionary Contents)> _templates = new() { // Core utilities - { "shell", ShellTemplate.Metadata }, - + { "shell", (ShellTemplate.Metadata, ShellTemplate.Contents) }, + // Form components - { "button", ButtonTemplate.Metadata }, - { "button-variants", ButtonVariantsTemplate.Metadata }, - { "input", InputTemplate.Metadata }, - { "label", LabelTemplate.Metadata }, - { "checkbox", CheckboxTemplate.Metadata }, - { "switch", SwitchTemplate.Metadata }, - + { "button", (ButtonTemplate.Metadata, ButtonTemplate.Contents) }, + { "button-variants", (ButtonVariantsTemplate.Metadata, ButtonVariantsTemplate.Contents) }, + { "input", (InputTemplate.Metadata, InputTemplate.Contents) }, + { "label", (LabelTemplate.Metadata, LabelTemplate.Contents) }, + { "checkbox", (CheckboxTemplate.Metadata, CheckboxTemplate.Contents) }, + { "switch", (SwitchTemplate.Metadata, SwitchTemplate.Contents) }, + // Layout components - { "card", CardTemplate.Metadata }, - { "card-header", CardHeaderTemplate.Metadata }, - { "card-content", CardContentTemplate.Metadata }, - { "card-footer", CardFooterTemplate.Metadata }, - { "separator", SeparatorTemplate.Metadata }, - + { "card", (CardTemplate.Metadata, CardTemplate.Contents) }, + { "card-header", (CardHeaderTemplate.Metadata, CardHeaderTemplate.Contents) }, + { "card-content", (CardContentTemplate.Metadata, CardContentTemplate.Contents) }, + { "card-footer", (CardFooterTemplate.Metadata, CardFooterTemplate.Contents) }, + { "separator", (SeparatorTemplate.Metadata, SeparatorTemplate.Contents) }, + // Data display - { "badge", BadgeTemplate.Metadata }, - { "progress", ProgressTemplate.Metadata }, - + { "badge", (BadgeTemplate.Metadata, BadgeTemplate.Contents) }, + { "progress", (ProgressTemplate.Metadata, ProgressTemplate.Contents) }, + // Feedback - { "alert", AlertTemplate.Metadata }, - + { "alert", (AlertTemplate.Metadata, AlertTemplate.Contents) }, + // Overlay (modal/panel) - P1 - { "element-extensions", ElementExtensionsTemplate.Metadata }, - { "dialog", DialogTemplate.Metadata }, - { "dialog-trigger", DialogTriggerTemplate.Metadata }, - { "dialog-content", DialogContentTemplate.Metadata }, - { "dialog-header", DialogHeaderTemplate.Metadata }, - { "dialog-footer", DialogFooterTemplate.Metadata }, - { "dialog-title", DialogTitleTemplate.Metadata }, - { "dialog-description", DialogDescriptionTemplate.Metadata }, - { "dialog-close", DialogCloseTemplate.Metadata }, - { "drawer", DrawerTemplate.Metadata }, - { "drawer-trigger", DrawerTriggerTemplate.Metadata }, - { "drawer-content", DrawerContentTemplate.Metadata }, - { "sheet", SheetTemplate.Metadata }, - { "sheet-trigger", SheetTriggerTemplate.Metadata }, - { "sheet-content", SheetContentTemplate.Metadata }, - { "dropdown", DropdownTemplate.Metadata }, - { "dropdown-trigger", DropdownTriggerTemplate.Metadata }, - { "dropdown-content", DropdownContentTemplate.Metadata }, - { "dropdown-item", DropdownItemTemplate.Metadata }, - { "popover", PopoverTemplate.Metadata }, - { "popover-trigger", PopoverTriggerTemplate.Metadata }, - { "popover-content", PopoverContentTemplate.Metadata }, - + { "element-extensions", (ElementExtensionsTemplate.Metadata, ElementExtensionsTemplate.Contents) }, + { "dialog", (DialogTemplate.Metadata, DialogTemplate.Contents) }, + { "dialog-trigger", (DialogTriggerTemplate.Metadata, DialogTriggerTemplate.Contents) }, + { "dialog-content", (DialogContentTemplate.Metadata, DialogContentTemplate.Contents) }, + { "dialog-header", (DialogHeaderTemplate.Metadata, DialogHeaderTemplate.Contents) }, + { "dialog-footer", (DialogFooterTemplate.Metadata, DialogFooterTemplate.Contents) }, + { "dialog-title", (DialogTitleTemplate.Metadata, DialogTitleTemplate.Contents) }, + { "dialog-description", (DialogDescriptionTemplate.Metadata, DialogDescriptionTemplate.Contents) }, + { "dialog-close", (DialogCloseTemplate.Metadata, DialogCloseTemplate.Contents) }, + { "drawer", (DrawerTemplate.Metadata, DrawerTemplate.Contents) }, + { "drawer-trigger", (DrawerTriggerTemplate.Metadata, DrawerTriggerTemplate.Contents) }, + { "drawer-content", (DrawerContentTemplate.Metadata, DrawerContentTemplate.Contents) }, + { "sheet", (SheetTemplate.Metadata, SheetTemplate.Contents) }, + { "sheet-trigger", (SheetTriggerTemplate.Metadata, SheetTriggerTemplate.Contents) }, + { "sheet-content", (SheetContentTemplate.Metadata, SheetContentTemplate.Contents) }, + { "dropdown", (DropdownTemplate.Metadata, DropdownTemplate.Contents) }, + { "dropdown-trigger", (DropdownTriggerTemplate.Metadata, DropdownTriggerTemplate.Contents) }, + { "dropdown-content", (DropdownContentTemplate.Metadata, DropdownContentTemplate.Contents) }, + { "dropdown-item", (DropdownItemTemplate.Metadata, DropdownItemTemplate.Contents) }, + { "popover", (PopoverTemplate.Metadata, PopoverTemplate.Contents) }, + { "popover-trigger", (PopoverTriggerTemplate.Metadata, PopoverTriggerTemplate.Contents) }, + { "popover-content", (PopoverContentTemplate.Metadata, PopoverContentTemplate.Contents) }, + // Form P2 - { "textarea", TextareaTemplate.Metadata }, - { "slider", SliderTemplate.Metadata }, - { "select", SelectTemplate.Metadata }, - { "radio-group", RadioGroupTemplate.Metadata }, - { "radio-group-item", RadioGroupItemTemplate.Metadata }, - { "date-picker", DatePickerTemplate.Metadata }, - { "time-picker", TimePickerTemplate.Metadata } + { "textarea", (TextareaTemplate.Metadata, TextareaTemplate.Contents) }, + { "slider", (SliderTemplate.Metadata, SliderTemplate.Contents) }, + { "select", (SelectTemplate.Metadata, SelectTemplate.Contents) }, + { "radio-group", (RadioGroupTemplate.Metadata, RadioGroupTemplate.Contents) }, + { "radio-group-item", (RadioGroupItemTemplate.Metadata, RadioGroupItemTemplate.Contents) }, + { "date-picker", (DatePickerTemplate.Metadata, DatePickerTemplate.Contents) }, + { "time-picker", (TimePickerTemplate.Metadata, TimePickerTemplate.Contents) } }; - public static string? GetComponentContent(string componentName) + // Metadata-only view for callers that don't need the per-platform payload. + public static IReadOnlyDictionary Components { get; } = + _templates.ToDictionary(kv => kv.Key, kv => kv.Value.Meta); + + // Returns the C# source for `name` targeted at `platform`, or null if the component + // doesn't exist OR exists but has no template for that platform. Callers that need to + // distinguish those two cases should use `Exists` and `SupportsPlatform` first. + public static string? GetComponentContent(string name, NativePlatform platform) + { + if (!_templates.TryGetValue(name.ToLower(), out var entry)) + return null; + return entry.Contents.TryGetValue(platform, out var content) ? content : null; + } + + public static bool SupportsPlatform(string name, NativePlatform platform) + { + return _templates.TryGetValue(name.ToLower(), out var entry) + && entry.Contents.ContainsKey(platform); + } + + public static IReadOnlySet GetSupportedPlatforms(string name) { - return componentName.ToLower() switch - { - "shell" => ShellTemplate.Content, - "button" => ButtonTemplate.Content, - "button-variants" => ButtonVariantsTemplate.Content, - "input" => InputTemplate.Content, - "label" => LabelTemplate.Content, - "checkbox" => CheckboxTemplate.Content, - "switch" => SwitchTemplate.Content, - "card" => CardTemplate.Content, - "card-header" => CardHeaderTemplate.Content, - "card-content" => CardContentTemplate.Content, - "card-footer" => CardFooterTemplate.Content, - "separator" => SeparatorTemplate.Content, - "badge" => BadgeTemplate.Content, - "progress" => ProgressTemplate.Content, - "alert" => AlertTemplate.Content, - "element-extensions" => ElementExtensionsTemplate.Content, - "dialog" => DialogTemplate.Content, - "dialog-trigger" => DialogTriggerTemplate.Content, - "dialog-content" => DialogContentTemplate.Content, - "dialog-header" => DialogHeaderTemplate.Content, - "dialog-footer" => DialogFooterTemplate.Content, - "dialog-title" => DialogTitleTemplate.Content, - "dialog-description" => DialogDescriptionTemplate.Content, - "dialog-close" => DialogCloseTemplate.Content, - "drawer" => DrawerTemplate.Content, - "drawer-trigger" => DrawerTriggerTemplate.Content, - "drawer-content" => DrawerContentTemplate.Content, - "sheet" => SheetTemplate.Content, - "sheet-trigger" => SheetTriggerTemplate.Content, - "sheet-content" => SheetContentTemplate.Content, - "dropdown" => DropdownTemplate.Content, - "dropdown-trigger" => DropdownTriggerTemplate.Content, - "dropdown-content" => DropdownContentTemplate.Content, - "dropdown-item" => DropdownItemTemplate.Content, - "popover" => PopoverTemplate.Content, - "popover-trigger" => PopoverTriggerTemplate.Content, - "popover-content" => PopoverContentTemplate.Content, - "textarea" => TextareaTemplate.Content, - "slider" => SliderTemplate.Content, - "select" => SelectTemplate.Content, - "radio-group" => RadioGroupTemplate.Content, - "radio-group-item" => RadioGroupItemTemplate.Content, - "date-picker" => DatePickerTemplate.Content, - "time-picker" => TimePickerTemplate.Content, - _ => null - }; + if (!_templates.TryGetValue(name.ToLower(), out var entry)) + return new HashSet(); + return entry.Contents.Keys.ToHashSet(); } public static IEnumerable GetByCategory(ComponentCategory category) { - return Components.Values.Where(c => c.Category == category); + return _templates.Values.Where(t => t.Meta.Category == category).Select(t => t.Meta); } public static IEnumerable SearchByTag(string tag) { - return Components.Values.Where(c => c.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase)); + return _templates.Values + .Where(t => t.Meta.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase)) + .Select(t => t.Meta); } public static ComponentMetadata? GetMetadata(string componentName) { - return Components.TryGetValue(componentName.ToLower(), out var metadata) ? metadata : null; + return _templates.TryGetValue(componentName.ToLower(), out var entry) ? entry.Meta : null; } public static bool Exists(string componentName) { - return Components.ContainsKey(componentName.ToLower()); + return _templates.ContainsKey(componentName.ToLower()); } } diff --git a/src/ShellUI.Native.Templates/Templates/AlertTemplate.cs b/src/ShellUI.Native.Templates/Templates/AlertTemplate.cs index b8ad14f..a948cf6 100644 --- a/src/ShellUI.Native.Templates/Templates/AlertTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/AlertTemplate.cs @@ -17,7 +17,9 @@ public static class AlertTemplate Tags = new List { "feedback", "alert", "notification", "message", "banner" } }; - public static string Content => @"using Microsoft.Maui.Controls.Shapes; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"using Microsoft.Maui.Controls.Shapes; namespace YourProjectNamespace.Components.UI; // Alert component for notifications and feedback @@ -175,6 +177,7 @@ public enum AlertVariant Warning, Info } -"; +" + }; } \ No newline at end of file diff --git a/src/ShellUI.Native.Templates/Templates/BadgeTemplate.cs b/src/ShellUI.Native.Templates/Templates/BadgeTemplate.cs index 92c637a..abee8ad 100644 --- a/src/ShellUI.Native.Templates/Templates/BadgeTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/BadgeTemplate.cs @@ -17,7 +17,9 @@ public static class BadgeTemplate Tags = new List { "status", "indicator", "badge", "tag", "chip" } }; - public static string Content => @"using Microsoft.Maui.Controls.Shapes; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"using Microsoft.Maui.Controls.Shapes; namespace YourProjectNamespace.Components.UI; // Badge status indicator component @@ -130,5 +132,6 @@ private void UpdateVisualState() } public enum BadgeVariant { Default, Secondary, Destructive, Outline, Success, Warning } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs b/src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs index c54ec43..435c7dc 100644 --- a/src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/ButtonTemplate.cs @@ -17,7 +17,9 @@ public static class ButtonTemplate Tags = new List { "form", "input", "interactive", "button", "action" } }; - public static string Content => @"using Microsoft.Maui.Controls.Shapes; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"using Microsoft.Maui.Controls.Shapes; using YourProjectNamespace.Components.UI.Variants; namespace YourProjectNamespace.Components.UI; @@ -153,5 +155,6 @@ private void UpdateVisualState() Opacity = IsEnabled ? 1.0 : 0.5; } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/ButtonVariantsTemplate.cs b/src/ShellUI.Native.Templates/Templates/ButtonVariantsTemplate.cs index 7e0e8c9..6483168 100644 --- a/src/ShellUI.Native.Templates/Templates/ButtonVariantsTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/ButtonVariantsTemplate.cs @@ -15,7 +15,9 @@ public static class ButtonVariantsTemplate IsAvailable = false // Installed as dependency of button }; - public static string Content => @"namespace YourProjectNamespace.Components.UI.Variants; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI.Variants; public enum ButtonVariant { @@ -124,5 +126,6 @@ public static ButtonStyle GetStyle(ButtonVariant variant = ButtonVariant.Default return style; } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/CardContentTemplate.cs b/src/ShellUI.Native.Templates/Templates/CardContentTemplate.cs index 4584c83..b73cbd7 100644 --- a/src/ShellUI.Native.Templates/Templates/CardContentTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/CardContentTemplate.cs @@ -17,7 +17,9 @@ public static class CardContentTemplate Tags = new List { "layout", "card", "content" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; // Card main content section public partial class CardContent : ContentView @@ -68,5 +70,6 @@ private static void OnNoPaddingChanged(BindableObject bindable, object oldValue, } } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/CardFooterTemplate.cs b/src/ShellUI.Native.Templates/Templates/CardFooterTemplate.cs index 6d7689c..2fd0b8f 100644 --- a/src/ShellUI.Native.Templates/Templates/CardFooterTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/CardFooterTemplate.cs @@ -17,7 +17,9 @@ public static class CardFooterTemplate Tags = new List { "layout", "card", "footer", "actions" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; // Card footer section for actions public partial class CardFooter : ContentView @@ -103,5 +105,6 @@ private static void OnJustifyChanged(BindableObject bindable, object oldValue, o } public enum FooterJustify { Start, Center, End, SpaceBetween } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/CardHeaderTemplate.cs b/src/ShellUI.Native.Templates/Templates/CardHeaderTemplate.cs index 1ed523d..b1cd1c4 100644 --- a/src/ShellUI.Native.Templates/Templates/CardHeaderTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/CardHeaderTemplate.cs @@ -17,7 +17,9 @@ public static class CardHeaderTemplate Tags = new List { "layout", "card", "header" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; // Card header section public partial class CardHeader : ContentView @@ -88,5 +90,6 @@ private static void OnDescriptionChanged(BindableObject bindable, object oldValu } } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/CardTemplate.cs b/src/ShellUI.Native.Templates/Templates/CardTemplate.cs index 08405ca..1c046ec 100644 --- a/src/ShellUI.Native.Templates/Templates/CardTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/CardTemplate.cs @@ -17,7 +17,9 @@ public static class CardTemplate Tags = new List { "layout", "container", "card", "panel", "surface" } }; - public static string Content => @"using Microsoft.Maui.Controls.Shapes; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"using Microsoft.Maui.Controls.Shapes; namespace YourProjectNamespace.Components.UI; // Card container - compositional children via Dependencies (CardHeader, CardContent, CardFooter) @@ -108,5 +110,6 @@ private void UpdateVisualState() } public enum CardVariant { Default, Bordered, Elevated } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/CheckboxTemplate.cs b/src/ShellUI.Native.Templates/Templates/CheckboxTemplate.cs index 19e3fc5..cc4e41d 100644 --- a/src/ShellUI.Native.Templates/Templates/CheckboxTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/CheckboxTemplate.cs @@ -17,7 +17,9 @@ public static class CheckboxTemplate Tags = new List { "form", "checkbox", "input", "selection" } }; - public static string Content => @"using Microsoft.Maui.Controls.Shapes; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"using Microsoft.Maui.Controls.Shapes; namespace YourProjectNamespace.Components.UI; // Checkbox component with label support @@ -165,6 +167,7 @@ private void UpdateVisualState() _checkboxBorder.Opacity = IsEnabled ? 1.0 : 0.5; } } -"; +" + }; } \ No newline at end of file diff --git a/src/ShellUI.Native.Templates/Templates/DatePickerTemplate.cs b/src/ShellUI.Native.Templates/Templates/DatePickerTemplate.cs index a938859..6955c94 100644 --- a/src/ShellUI.Native.Templates/Templates/DatePickerTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DatePickerTemplate.cs @@ -15,7 +15,9 @@ public static class DatePickerTemplate Tags = new List { "form", "date", "picker" } }; - public static string Content => @"using Microsoft.Maui.Controls.Shapes; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"using Microsoft.Maui.Controls.Shapes; namespace YourProjectNamespace.Components.UI; public partial class DatePicker : ContentView @@ -86,5 +88,6 @@ private static void OnDateChanged(BindableObject b, object o, object n) dp._nativePicker.Date = dt; } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/DialogCloseTemplate.cs b/src/ShellUI.Native.Templates/Templates/DialogCloseTemplate.cs index 8f8817d..157a755 100644 --- a/src/ShellUI.Native.Templates/Templates/DialogCloseTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DialogCloseTemplate.cs @@ -15,7 +15,9 @@ public static class DialogCloseTemplate Tags = new List { "overlay", "dialog", "close" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; public partial class DialogClose : ContentView { @@ -26,5 +28,6 @@ public DialogClose() GestureRecognizers.Add(tap); } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/DialogContentTemplate.cs b/src/ShellUI.Native.Templates/Templates/DialogContentTemplate.cs index 84352b0..f32390e 100644 --- a/src/ShellUI.Native.Templates/Templates/DialogContentTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DialogContentTemplate.cs @@ -15,7 +15,9 @@ public static class DialogContentTemplate Tags = new List { "overlay", "dialog", "content" } }; - public static string Content => @"using Microsoft.Maui.Controls.Shapes; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"using Microsoft.Maui.Controls.Shapes; namespace YourProjectNamespace.Components.UI; // Modal content overlay - backdrop + centered box. Usage: ... @@ -66,5 +68,6 @@ public DialogContent() } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/DialogDescriptionTemplate.cs b/src/ShellUI.Native.Templates/Templates/DialogDescriptionTemplate.cs index a2c03c0..47f5154 100644 --- a/src/ShellUI.Native.Templates/Templates/DialogDescriptionTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DialogDescriptionTemplate.cs @@ -15,7 +15,9 @@ public static class DialogDescriptionTemplate Tags = new List { "overlay", "dialog", "description" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; public partial class DialogDescription : Label { @@ -25,5 +27,6 @@ public DialogDescription() TextColor = Color.FromArgb(""#6B7280""); } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/DialogFooterTemplate.cs b/src/ShellUI.Native.Templates/Templates/DialogFooterTemplate.cs index fa7e106..099a654 100644 --- a/src/ShellUI.Native.Templates/Templates/DialogFooterTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DialogFooterTemplate.cs @@ -15,7 +15,9 @@ public static class DialogFooterTemplate Tags = new List { "overlay", "dialog", "footer" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; [ContentProperty(nameof(Children))] public partial class DialogFooter : ContentView @@ -34,5 +36,6 @@ public DialogFooter() Content = _stack; } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/DialogHeaderTemplate.cs b/src/ShellUI.Native.Templates/Templates/DialogHeaderTemplate.cs index 98a1286..ed349dd 100644 --- a/src/ShellUI.Native.Templates/Templates/DialogHeaderTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DialogHeaderTemplate.cs @@ -15,7 +15,9 @@ public static class DialogHeaderTemplate Tags = new List { "overlay", "dialog", "header" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; [ContentProperty(nameof(Children))] public partial class DialogHeader : ContentView @@ -30,5 +32,6 @@ public DialogHeader() Content = _stack; } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/DialogTemplate.cs b/src/ShellUI.Native.Templates/Templates/DialogTemplate.cs index a71d642..5ab34f0 100644 --- a/src/ShellUI.Native.Templates/Templates/DialogTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DialogTemplate.cs @@ -15,7 +15,9 @@ public static class DialogTemplate Tags = new List { "overlay", "modal", "dialog" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; // Modal dialog - compositional. Usage: ...... public partial class Dialog : AbsoluteLayout @@ -75,5 +77,6 @@ private void OnOpenChanged() OpenChanged?.Invoke(this, Open); } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/DialogTitleTemplate.cs b/src/ShellUI.Native.Templates/Templates/DialogTitleTemplate.cs index 6ff0eaf..a56d681 100644 --- a/src/ShellUI.Native.Templates/Templates/DialogTitleTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DialogTitleTemplate.cs @@ -15,7 +15,9 @@ public static class DialogTitleTemplate Tags = new List { "overlay", "dialog", "title" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; public partial class DialogTitle : Label { @@ -25,5 +27,6 @@ public DialogTitle() FontAttributes = FontAttributes.Bold; } } -"; +" + }; } diff --git a/src/ShellUI.Native.Templates/Templates/DialogTriggerTemplate.cs b/src/ShellUI.Native.Templates/Templates/DialogTriggerTemplate.cs index b70ca10..b4fad90 100644 --- a/src/ShellUI.Native.Templates/Templates/DialogTriggerTemplate.cs +++ b/src/ShellUI.Native.Templates/Templates/DialogTriggerTemplate.cs @@ -15,7 +15,9 @@ public static class DialogTriggerTemplate Tags = new List { "overlay", "dialog", "trigger" } }; - public static string Content => @"namespace YourProjectNamespace.Components.UI; + public static IReadOnlyDictionary Contents { get; } = new Dictionary + { + [NativePlatform.MAUI] = @"namespace YourProjectNamespace.Components.UI; // Tap to open dialog. Usage: