diff --git a/plugins/reactor/skills/reactor-recipes/SKILL.md b/plugins/reactor/skills/reactor-recipes/SKILL.md index a4f1b7988..29a1e1f81 100644 --- a/plugins/reactor/skills/reactor-recipes/SKILL.md +++ b/plugins/reactor/skills/reactor-recipes/SKILL.md @@ -1,39 +1,38 @@ --- name: reactor-recipes -description: "Paste-ready single-file Reactor recipes indexed by intent — list with add/delete/toggle, sidebar navigation between pages, form with validation + submit gating, async fetch with loading/error/data states, themed Win11 card surface. Use a recipe instead of synthesizing from prose: copy, adjust, ship." +description: "Intent-first map into the Reactor scenario catalogue — list add/delete/toggle, sidebar navigation, validation and submit gating forms, async fetch states, card surfaces, and canvas positioning. Prefer the scenario source under samples/scenarios/ over legacy recipe files." --- ## How to use this skill -Each recipe is a complete, compilable single-file program. **Copy the file's body directly into your project**; don't synthesize the pattern from scratch. The recipes use real Reactor APIs only — no pseudocode. +The primary source is now the **scenario catalogue** under `samples/scenarios/`. Each scenario folder contains a compilable single-file app in `Scenario.cs`; copy the matching scenario and adapt it instead of synthesizing from prose. -The full recipe content is in `references/.cs`. Read just the recipe(s) that match your intent. +The old `references/` folder no longer contains the legacy recipe `.cs` files. It only keeps markdown docs such as `references/index.md` and `references/animated-list.md`. -## Intent → recipe map +## Intent → scenario map -| Intent | Recipe | APIs used | +| Intent | Scenario source | APIs used | |---|---|---| -| Add / remove / toggle items in a list | `references/list-add-delete.cs` | `UseReducer`, `WithKey`, `Command` | -| Animate list insert / move / remove | `references/animated-list.md` | `Animations.Animate`, `AnimationKind`, `IReactorKeyed`, `UseReducedMotion` | -| Sidebar navigation between pages | `references/sidebar-nav.cs` | `UseNavigation`, `NavigationView`, `NavigationHost`, `WithNavigation` | -| Form with validation + submit gating | `references/form-with-validation.cs` | `UseValidationContext`, `FormField`, `Validate.*`, `ShowWhen` | -| Fetch data with loading / error / data states | `references/async-fetch-list.cs` | `UseResource`, `AsyncValue.Match` | -| Themed Win11 card surface | `references/themed-card.cs` | `Theme.*` tokens, `Border`, `FlexColumn`, `.Padding`, `.CornerRadius`, `.WithBorder` | -| Absolute positioning with Canvas | `references/canvas-positioning.cs` | `Canvas`, `.Canvas(left, top)`, `.CenterAt`, shapes | +| Add / remove / toggle items in a list | `samples/scenarios/lists/list-add-delete-toggle/Scenario.cs` | `UseReducer`, `WithKey`, `Command` | +| Animate list insert / move / remove | `samples/apps/animated-list-demo/` | `Animations.Animate`, `AnimationKind`, `IReactorKeyed`, `UseReducedMotion` | +| Sidebar navigation between pages | `samples/scenarios/navigation/sidebar-nav/Scenario.cs` | `UseNavigation`, `NavigationView`, `NavigationHost`, `WithNavigation` | +| Form with validation + submit gating | `samples/scenarios/forms/form-validation-context/Scenario.cs` and `samples/scenarios/forms/form-submit-gating/Scenario.cs` | `UseValidationContext`, `FormField`, `Validate.*`, `ShowWhen` | +| Fetch data with loading / error / data states | `samples/scenarios/hooks/async-fetch-list/Scenario.cs` | `UseResource`, `AsyncValue.Match` | +| Themed Win11 card surface | `samples/scenarios/layout/card-surface/Scenario.cs` | `Theme.*` tokens, `Border`, `FlexColumn`, `.Padding`, `.CornerRadius`, `.WithBorder` | +| Absolute positioning with Canvas | `samples/scenarios/layout/canvas-positioning/Scenario.cs` | `Canvas`, `.Canvas(left, top)`, `.CenterAt`, shapes | -See `references/index.md` for the full index. +See `samples/scenarios/README.md` for the catalogue contract and `samples/scenarios/_generated/scenarios.json` for the generated index. -## Recipe contract +## Scenario contract -A good recipe: +A good scenario: - Compiles standalone (`dotnet run` works against the file). - Targets one intent — no kitchen-sink demos. -- Stays under ~120 lines including imports and `ReactorApp.Run` shell. -- Uses real Reactor APIs only. +- Stays concise, with real Reactor APIs only. - Comments only the *non-obvious*. -## Adapting a recipe +## Adapting a scenario -The recipes use `#:package Microsoft.UI.Reactor@0.0.0-local` (selfhost default). Replace the version with whatever you depend on outside the source clone. +Scenarios use `#:package Microsoft.UI.Reactor@0.0.0-local` (selfhost default). Replace the version with whatever you depend on outside the source clone. -If you need analyzer coverage (`REACTOR_DSL_001` and friends), promote the recipe to a `.csproj` — single-file `.cs` builds don't load analyzers. See `reactor-getting-started` for the `.csproj` template. +If you need analyzer coverage (`REACTOR_DSL_001` and friends), promote the scenario to a `.csproj` — single-file `.cs` builds don't load analyzers. See `reactor-getting-started` for the `.csproj` template. diff --git a/plugins/reactor/skills/reactor-recipes/references/async-fetch-list.cs b/plugins/reactor/skills/reactor-recipes/references/async-fetch-list.cs deleted file mode 100644 index 81ce9dda7..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/async-fetch-list.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Recipe: fetch a remote list, render loading / data / error / reloading. -// -// Pattern: UseResource owns the cancellation token and dedups across siblings. -// AsyncValue is a sealed 4-state record — pattern-match it (or call .Match). -// `deps` controls cache key — pass scalar values or memoized arrays, never -// freshly allocated lambdas / collections. - -// In this clone, run `mur pack-local` once. Bump the version below to match -// whatever `mur pack-local` printed (default: 0.0.0-local). For a real NuGet -// consumer, set Version to a published Microsoft.UI.Reactor release. -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; // InfoBarSeverity, VerticalAlignment -using static Microsoft.UI.Reactor.Factories; - -ReactorApp.Run("Async demo", width: 560, height: 600); - -record Repo(int Id, string Name, string Description); - -static class Api -{ - public static async Task> ListReposAsync(string owner, CancellationToken ct) - { - // Replace with a real HttpClient call — this stub keeps the recipe self-contained. - await Task.Delay(800, ct); - if (owner == "fail") throw new InvalidOperationException("Owner not found"); - return [ - new(1, $"{owner}/alpha", "first repo"), - new(2, $"{owner}/beta", "second repo"), - new(3, $"{owner}/gamma", "third repo"), - ]; - } -} - -class App : Component -{ - public override Element Render() - { - var (owner, setOwner) = UseState("microsoft"); - - var repos = UseResource( - ct => Api.ListReposAsync(owner, ct), - deps: [owner]); - - return VStack(12, - HStack(8, - TextField(owner, setOwner, placeholder: "GitHub owner").Flex(grow: 1), - Caption("(try \"fail\" to see error state)").VAlign(VerticalAlignment.Center)), - - repos.Match( - loading: () => HStack(8, ProgressRing(), TextBlock("Loading…")), - data: list => VStack(8, list.Select(r => - Border(VStack(2, - TextBlock(r.Name).Bold(), - Caption(r.Description))) - .Padding(12) - .CornerRadius(6) - .WithKey(r.Id.ToString())).ToArray()), - error: ex => InfoBar("Error", ex.Message).Severity(InfoBarSeverity.Error), - reloading: prev => VStack(8, - ProgressIndeterminate(), - VStack(8, prev.Select(r => TextBlock(r.Name).Opacity(0.5)).ToArray()))) - ).Padding(24); - } -} diff --git a/plugins/reactor/skills/reactor-recipes/references/calendar-multiselect.cs b/plugins/reactor/skills/reactor-recipes/references/calendar-multiselect.cs deleted file mode 100644 index 9fa08c9ec..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/calendar-multiselect.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Recipe: multi-select calendar with selected-date summary. -// -// Pattern: CalendarView in `Multiple` selection mode (set via `with`), -// state holds an IReadOnlyList, the .SelectedDatesChanged -// fluent fires with the new list on every selection edit. Pass the list -// back through .SelectedDates for two-way binding so programmatic clears -// re-apply. - -// In this clone, run `mur pack-local` once. Bump the version below to match -// whatever `mur pack-local` printed (default: 0.0.0-local). For a real NuGet -// consumer, set Version to a published Microsoft.UI.Reactor release. -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; // CalendarViewSelectionMode -using static Microsoft.UI.Reactor.Factories; - -ReactorApp.Run("Calendar multi-select", width: 520, height: 600); - -class App : Component -{ - public override Element Render() - { - var (dates, setDates) = UseState>( - Array.Empty()); - - return VStack(16, - Subtitle("Pick travel days"), - - (CalendarView() with { SelectionMode = CalendarViewSelectionMode.Multiple }) - .SelectedDates(dates) - .SelectedDatesChanged(setDates), - - Body(dates.Count == 0 - ? "No dates selected." - : $"{dates.Count} selected: " + - string.Join(", ", dates.Select(d => d.ToString("MMM d")))), - - Button("Clear", () => setDates(Array.Empty())) - .SubtleButton() - ).Padding(24); - } -} diff --git a/plugins/reactor/skills/reactor-recipes/references/canvas-positioning.cs b/plugins/reactor/skills/reactor-recipes/references/canvas-positioning.cs deleted file mode 100644 index 77b926bd5..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/canvas-positioning.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Recipe: absolute positioning with Canvas. -// -// Pattern: Canvas() factory + .Canvas(left, top) modifier on children. -// ⚠️ `using static Factories` brings a Canvas() factory that shadows -// Microsoft.UI.Xaml.Controls.Canvas — do NOT call Canvas.SetLeft()/SetTop(), -// use the fluent .Canvas(left, top) modifier instead. - -// In this clone, run `mur pack-local` once. Bump the version below to match -// whatever `mur pack-local` printed (default: 0.0.0-local). For a real NuGet -// consumer, set Version to a published Microsoft.UI.Reactor release. -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using Microsoft.UI.Xaml; -using static Microsoft.UI.Reactor.Factories; - -ReactorApp.Run("Canvas demo", width: 600, height: 500); - -class App : Component -{ - public override Element Render() - { - return Canvas( - // Position children with the .Canvas(left, top) extension - Border(TextBlock("A").Padding(8)) - .Background(Theme.CardBackground) - .WithBorder(Theme.CardStroke, 1) - .CornerRadius(4) - .Canvas(left: 50, top: 30), - - Border(TextBlock("B").Padding(8)) - .Background(Theme.CardBackground) - .WithBorder(Theme.CardStroke, 1) - .CornerRadius(4) - .Canvas(left: 200, top: 100), - - // Center on a point (anchor 0.5, 0.5) - Ellipse().Width(20).Height(20) - .Fill(Theme.Accent) - .CenterAt(x: 150, y: 75), - - // Shapes - Line(50, 55, 200, 125).Stroke(Theme.DividerStroke, 1), - Rectangle().Width(60).Height(40) - .Fill(Theme.SubtleFill) - .Canvas(left: 350, top: 50) - ) with { Width = 500, Height = 400, Background = Theme.SolidBackground }; - } -} diff --git a/plugins/reactor/skills/reactor-recipes/references/form-with-validation.cs b/plugins/reactor/skills/reactor-recipes/references/form-with-validation.cs deleted file mode 100644 index cea5699f7..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/form-with-validation.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Recipe: form with validation, error display, and submit gating. -// -// Pattern: UseValidationContext owns per-field state. Inputs attach validators -// with `.Validate(fieldName, currentValue, ...)` — the value is required for -// auto-validation. FormField wraps each input with label + required marker + -// error display. The validation context is resolved through React-style -// context — no need to thread it explicitly to .Validate(). -// -// Named-input fluents (`.NumericInput()`, `.EmailInput()`) pre-configure the -// TextField with the right InputScope / keyboard hint so on-screen / IME -// keyboards open in the correct mode. They layer on top of validators. -// `.MaxLength(n)` caps input length at the control level. -// `.Changed(handler)` is the fluent alias for the positional onChanged -// callback — it REPLACES (does not append). When you need state-setter + -// side-effects, fold both into one lambda. - -// In this clone, run `mur pack-local` once. Bump the version below to match -// whatever `mur pack-local` printed (default: 0.0.0-local). For a real NuGet -// consumer, set Version to a published Microsoft.UI.Reactor release. -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using Microsoft.UI.Reactor.Controls.Validation; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; // InfoBarSeverity -using static Microsoft.UI.Reactor.Factories; -using static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl; - -ReactorApp.Run("Form demo", width: 520, height: 540); - -class App : Component -{ - public override Element Render() - { - var ctx = this.UseValidationContext(); - var (name, setName) = UseState(""); - var (email, setEmail) = UseState(""); - var (password, setPwd) = UseState(""); - var (confirm, setConfirm) = UseState(""); - var (age, setAge) = UseState(""); - var (submitted, setSubmitted) = UseState(false); - - var sw = submitted ? ShowWhen.Always : ShowWhen.WhenTouched; - - // The positional onChanged is the single callback slot. `.Changed(...)` - // is the fluent alias — equivalent, but it REPLACES the positional - // handler. To layer side effects (state setter + MarkTouched), fold - // them into one lambda as below. - return VStack(12, - Heading("Sign up"), - - FormField( - TextField(name, v => { setName(v); ctx.MarkTouched("name"); }, placeholder: "Your name") - .Validate("name", name, - Validate.Required("Name is required"), - Validate.MinLength(2, "At least 2 characters")), - label: "Name", required: true, showWhen: sw), - - FormField( - TextField(email, v => { setEmail(v); ctx.MarkTouched("email"); }, placeholder: "you@example.com") - .EmailInput() - .Validate("email", email, - Validate.Required("Email is required"), - Validate.Email("Not a valid email")), - label: "Email", required: true, showWhen: sw), - - FormField( - TextField(age, v => { setAge(v); ctx.MarkTouched("age"); }, placeholder: "Age") - .NumericInput() - .MaxLength(3) - .Validate("age", age, - Validate.Required("Age is required")), - label: "Age", required: true, showWhen: sw), - - FormField( - PasswordBox(password, v => { setPwd(v); ctx.MarkTouched("password"); }) - .Validate("password", password, - Validate.Required("Password required"), - Validate.MinLength(8, "At least 8 characters")), - label: "Password", required: true, showWhen: sw), - - FormField( - PasswordBox(confirm, v => { setConfirm(v); ctx.MarkTouched("confirm"); }) - .Validate("confirm", confirm, - Validate.Must(c => c == password, "Passwords don't match")), - label: "Confirm password", required: true, showWhen: sw), - - Button("Create account", () => - { - setSubmitted(true); - ctx.MarkAllTouched(); - }), - - submitted && ctx.IsValid() - ? InfoBar("Created!", $"Welcome, {name}.").Severity(InfoBarSeverity.Success) - : null - ).Padding(24); - } -} diff --git a/plugins/reactor/skills/reactor-recipes/references/list-add-delete.cs b/plugins/reactor/skills/reactor-recipes/references/list-add-delete.cs deleted file mode 100644 index 615cc97f8..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/list-add-delete.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Recipe: editable list with add / toggle / delete. -// -// Pattern: UseReducer for the whole list (UseState + List.Add won't trigger -// a re-render because the reference is unchanged). One reducer, immutable -// updates with `with` and collection expressions. The templated -// `ListView(items, key, builder)` factory carries the keySelector through -// to the element record, but the current reconciler reconciles realized -// containers by index (ItemsSource is a 0..N range, RefreshRealizedContainers -// matches by container position), so on reorder/delete-from-middle, focus -// and per-row state can stick to the wrong row unless each row carries an -// explicit `.WithKey(item.Id)` on its outer element. -// `.ItemClick(handler)` is the fluent for ListView's primary item-tap event. - -// In this clone, run `mur pack-local` once. Bump the version below to match -// whatever `mur pack-local` printed (default: 0.0.0-local). For a real NuGet -// consumer, set Version to a published Microsoft.UI.Reactor release. -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using Microsoft.UI.Reactor.Layout; // FlexAlign -using Microsoft.UI.Xaml; -using static Microsoft.UI.Reactor.Factories; - -ReactorApp.Run("List demo", width: 480, height: 600); - -record Item(string Id, string Text, bool Done); - -abstract record Action; -record Add(string Text) : Action; -record Toggle(string Id) : Action; -record Delete(string Id) : Action; - -static class Reducer -{ - public static IReadOnlyList Apply(IReadOnlyList s, Action a) => a switch - { - Add x when !string.IsNullOrWhiteSpace(x.Text) - => [.. s, new(Guid.NewGuid().ToString(), x.Text.Trim(), false)], - Toggle t => [.. s.Select(i => i.Id == t.Id ? i with { Done = !i.Done } : i)], - Delete d => [.. s.Where(i => i.Id != d.Id)], - _ => s, - }; -} - -class App : Component -{ - public override Element Render() - { - var (items, dispatch) = UseReducer, Action>( - Reducer.Apply, []); - var (draft, setDraft) = UseState(""); - - var add = new Command - { - Label = "Add", - Execute = () => { dispatch(new Add(draft)); setDraft(""); }, - CanExecute = !string.IsNullOrWhiteSpace(draft), - }; - - return CommandHost([add], - VStack(12, - HStack(8, - TextField(draft, setDraft, placeholder: "What needs doing?") - .Flex(grow: 1), - Button(add).Flex(shrink: 0)), - - ListView( - items, - keySelector: item => item.Id, - viewBuilder: (item, _) => HStack(8, - CheckBox(item.Done, _ => dispatch(new Toggle(item.Id))), - TextBlock(item.Text).Flex(grow: 1, alignSelf: FlexAlign.Center), - Button("✕", () => dispatch(new Delete(item.Id)))) - .WithKey(item.Id) - ).ItemClick(item => dispatch(new Toggle(item.Id))) - .Flex(grow: 1)) - .Padding(16)); - } -} diff --git a/plugins/reactor/skills/reactor-recipes/references/named-styles.cs b/plugins/reactor/skills/reactor-recipes/references/named-styles.cs deleted file mode 100644 index bffa691b7..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/named-styles.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Recipe: named-style fluents for buttons, hyperlinks, and InfoBars. -// -// Pattern: named-style fluents are zero-arg modifiers that apply the -// matching WinUI theme resource (AccentButtonStyle, SubtleButtonStyle, -// TextBlockButtonStyle for .TextLink() on Button / HyperlinkButton, -// InfoBarSeverity.*). They re-theme on light / dark / contrast switches -// and stack with other fluents (.Padding, .Click, etc.). - -// In this clone, run `mur pack-local` once. Bump the version below to match -// whatever `mur pack-local` printed (default: 0.0.0-local). For a real NuGet -// consumer, set Version to a published Microsoft.UI.Reactor release. -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using Microsoft.UI.Xaml; -using static Microsoft.UI.Reactor.Factories; - -ReactorApp.Run("Named styles", width: 560, height: 520); - -class App : Component -{ - public override Element Render() => - ScrollView( - VStack(20, - Subtitle("Buttons"), - HStack(8, - Button("Default", () => { }), - // Accent — primary call-to-action color. - Button("Accent", () => { }).AccentButton(), - // Subtle — transparent until hover; for toolbar / chrome. - Button("Subtle", () => { }).SubtleButton(), - // TextLink — borderless, hyperlink-style; flips between - // Button and HyperlinkButton with the same fluent name. - Button("Text link", () => { }).TextLink()), - - Subtitle("Hyperlinks"), - HyperlinkButton("Open docs").TextLink(), - - Subtitle("InfoBars"), - // Severity fluents — short aliases for .Severity(Info|...). - InfoBar("Tip", "You can drag the divider.").Informational(), - InfoBar("Saved", "Changes written to disk.").Success(), - InfoBar("Heads up", "Unsaved changes will be discarded.").Warning(), - InfoBar("Failed", "Couldn't reach the server.").Error() - ).Padding(24)); -} diff --git a/plugins/reactor/skills/reactor-recipes/references/sidebar-nav.cs b/plugins/reactor/skills/reactor-recipes/references/sidebar-nav.cs deleted file mode 100644 index adf6c4882..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/sidebar-nav.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Recipe: sidebar navigation between pages. -// -// Pattern: route enum → UseNavigation handle at the root → NavigationView -// supplies the sidebar UI, items keyed by string tag → NavigationHost renders -// the matched page → .WithNavigation(nav, routeToTag, tagToRoute) wires it up -// so clicking a sidebar item calls nav.Navigate(route). Pages can pull the -// shared handle via UseNavigation(). -// -// `.SelectedTagChanged(handler)` is the low-level NavigationView fluent — it -// fires with the new tag string on every selection change. `.WithNavigation` -// is the typed wrapper that maps tag↔route and dispatches into your -// `UseNavigation` handle for you; prefer it unless you need raw tag access. - -// In this clone, run `mur pack-local` once. Bump the version below to match -// whatever `mur pack-local` printed (default: 0.0.0-local). For a real NuGet -// consumer, set Version to a published Microsoft.UI.Reactor release. -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using Microsoft.UI.Xaml; -using static Microsoft.UI.Reactor.Factories; - -ReactorApp.Run("Sidebar demo", width: 1000, height: 700); - -enum Route { Home, Library, Settings } - -class Shell : Component -{ - static string ToTag(Route r) => r.ToString().ToLowerInvariant(); - static Route ToRoute(string t) => Enum.Parse(t, ignoreCase: true); - - public override Element Render() - { - var nav = UseNavigation(Route.Home); - - return NavigationView( - [ - NavItem("Home", icon: "", tag: ToTag(Route.Home)), - NavItem("Library", icon: "", tag: ToTag(Route.Library)), - NavItem("Settings", icon: "", tag: ToTag(Route.Settings)), - ], - NavigationHost(nav, route => route switch - { - Route.Home => Component(), - Route.Library => Component(), - Route.Settings => Component(), - _ => TextBlock("Not found") - }) - ) - // .WithNavigation internally sets OnSelectedTagChanged to bridge - // tag→route. For a hand-rolled equivalent (or to add a diagnostic - // hook), use the .SelectedTagChanged(tag => ...) fluent directly — - // but note it REPLACES the slot, so pick one or the other. - .WithNavigation(nav, ToTag, ToRoute); - } -} - -class HomePage : Component -{ - public override Element Render() => - VStack(12, Heading("Home"), TextBlock("Welcome.")).Padding(24); -} - -class LibraryPage : Component -{ - public override Element Render() => - VStack(12, Heading("Library"), TextBlock("Your stuff.")).Padding(24); -} - -class SettingsPage : Component -{ - public override Element Render() - { - var nav = this.UseNavigation(); - return VStack(12, - Heading("Settings"), - Button("Back to Home", () => nav.Navigate(Route.Home)) - ).Padding(24); - } -} diff --git a/plugins/reactor/skills/reactor-recipes/references/themed-card.cs b/plugins/reactor/skills/reactor-recipes/references/themed-card.cs deleted file mode 100644 index b639d1ab6..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/themed-card.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Recipe: themed card surface following Win11 design rules. -// -// Pattern: the `Card(child)` factory bakes in Theme.CardBackground, -// Theme.CardStroke (1px), 8px corner radius, and 16px padding — re-themes -// on light/dark/contrast switches. Headings via Subtitle()/Body()/Caption() -// from the WinUI 3 type ramp, not raw FontSize. Override any preset by -// chaining a fluent on the returned border (e.g. .Padding(24)). -// Never hardcode hex on themed surfaces — agents/reviewers will reject it. - -// In this clone, run `mur pack-local` once. Bump the version below to match -// whatever `mur pack-local` printed (default: 0.0.0-local). For a real NuGet -// consumer, set Version to a published Microsoft.UI.Reactor release. -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using Microsoft.UI.Xaml; -using static Microsoft.UI.Reactor.Factories; -using static Microsoft.UI.Reactor.Core.Theme; - -ReactorApp.Run("Card demo", width: 560, height: 480); - -class App : Component -{ - public override Element Render() => - FlexColumn( - (TitleBar("Cards") with { Subtitle = "Win11 design tokens" }).Flex(shrink: 0), - ScrollView( - FlexColumn( - Tile("Storage", "12% used", "View details"), - Tile("Updates", "Up to date", "Check now"), - Tile("Bluetooth", "2 devices paired", "Manage") - ).FlexPadding(16, 16) - ).Flex(grow: 1) - ).Backdrop(BackdropKind.Mica); - - // Card(...) factory bakes in background, stroke, corner radius, padding. - // Bottom margin separates stacked cards — chain any fluent to override. - static Element Tile(string title, string status, string action) => - Card( - FlexColumn( - Subtitle(title), - Caption(status).Foreground(SecondaryText), - HyperlinkButton(action).Margin(0, 8, 0, 0))) - .Margin(0, 0, 0, 12); -} diff --git a/plugins/reactor/skills/reactor-recipes/references/use-custom-hook.cs b/plugins/reactor/skills/reactor-recipes/references/use-custom-hook.cs deleted file mode 100644 index 3ffae74bf..000000000 --- a/plugins/reactor/skills/reactor-recipes/references/use-custom-hook.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Recipe: extract stateful logic into a custom hook. -// -// Rule: hooks (UseState, UseEffect, UseMemo, …) can only be called from a -// Render() override or from another method whose name starts with `Use`. If -// you want to share stateful behavior across components — or just hide it -// behind a clean name — write a custom hook as an *extension method on -// RenderContext*. The analyzer (REACTOR_HOOKS_005) enforces the naming -// convention; a method called `DebouncedValue(...)` that calls UseState -// internally will fail to compile. -// -// Wrong shapes that the analyzer rejects: -// * UseState in a field initializer: `int _x = UseState(0).Item1;` -// * UseState in a regular helper: `int GetCount() { return UseState(0).Item1; }` -// * UseState inside an event handler: `Button("X", () => UseState(0))` -// -// Right shape: a `Use*` extension on RenderContext that bundles the hooks. -// Render() calls it like any other hook — order-stable, deps-driven. - -#:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.0.1 -#:property OutputType=WinExe -#:property TargetFramework=net10.0-windows10.0.22621.0 -#:property UseWinUI=true -#:property WindowsPackageType=None - -using Microsoft.UI.Reactor; -using Microsoft.UI.Reactor.Core; -using static Microsoft.UI.Reactor.Factories; - -ReactorApp.Run("Custom hook demo", width: 520, height: 320); - -// ── The custom hook ──────────────────────────────────────────────────── -// Extension on RenderContext; name starts with `Use`. Inside we are free -// to call other hooks (UseState, UseEffect). Callers see one return value -// and never know the hook ran two underlying hooks. - -static class DebounceHooks -{ - public static T UseDebouncedValue(this RenderContext ctx, T value, int delayMs) - { - var (debounced, setDebounced) = ctx.UseState(value); - - // Deps array drives re-arming. Pass scalars (value, delayMs) — never - // a freshly-allocated array/lambda, or REACTOR_HOOKS_004 fires. - ctx.UseEffect(() => - { - var cts = new CancellationTokenSource(); - _ = Task.Run(async () => - { - try - { - await Task.Delay(delayMs, cts.Token); - setDebounced(value); - } - catch (TaskCanceledException) { /* superseded by next render */ } - }); - return () => cts.Cancel(); - }, value!, delayMs); - - return debounced; - } - - // Optional Component-receiver overload mirrors the built-in hook pattern - // (see UseAnnounce / UseFocus in src/Reactor/Hooks). Lets callers write - // `this.UseDebouncedValue(...)` from inside a Component subclass without - // touching ctx directly. - public static T UseDebouncedValue(this Component component, T value, int delayMs) - => component.Context.UseDebouncedValue(value, delayMs); -} - -// ── The component that uses it ───────────────────────────────────────── - -class App : Component -{ - public override Element Render() - { - var (query, setQuery) = UseState(""); - - // One call site, two underlying hooks. Order is stable because the - // custom hook always calls the same hooks in the same sequence. - var debouncedQuery = this.UseDebouncedValue(query, delayMs: 300); - - return VStack(12, - TextField(query, setQuery, placeholder: "Type to search…"), - TextBlock($"Live: {query}").Opacity(0.7), - TextBlock($"Debounced (300ms): {debouncedQuery}").Bold() - ).Padding(24); - } -} diff --git a/samples/scenarios/README.md b/samples/scenarios/README.md new file mode 100644 index 000000000..4db7dd68d --- /dev/null +++ b/samples/scenarios/README.md @@ -0,0 +1,47 @@ +# Reactor Sample Catalogue + +Curated, compilable single-file Reactor scenarios indexed by `mur find`. + +## Authoring contract + +Every scenario folder contains exactly two files: + +- **`Scenario.cs`** — a complete single-file Reactor app +- **`scenario.json`** — sidecar metadata for the search index + +### `Scenario.cs` format + +```csharp +// id: +// intent: +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Title", width: 400, height: 200); + +class App : Component +{ + public override Element Render() { /* ... */ } +} +``` + +### `scenario.json` schema + +```json +{ + "id": "kebab-case-name", + "category": "hooks|layout|inputs|...", + "title": "Human-readable title", + "intent": "search-friendly description of what this demonstrates", + "tags": ["keyword1", "keyword2"], + "factoryAnchors": ["FactoryName1", "FactoryName2"], + "notesKey": "FactoryOrHookName", + "relatedIds": ["other-scenario-id"], + "priority": "P0" +} +``` + +The folder name IS the scenario id. diff --git a/samples/scenarios/_generated/scenarios.json b/samples/scenarios/_generated/scenarios.json new file mode 100644 index 000000000..a1b02b222 --- /dev/null +++ b/samples/scenarios/_generated/scenarios.json @@ -0,0 +1,1475 @@ +{ + "scenarios": [ + { + "id": "appbarbutton-in-commandbar", + "category": "buttons", + "title": "CommandBar with AppBarButtons", + "intent": "command bar with app bar buttons for a toolbar-style action surface", + "tags": [ + "commandbar", + "appbarbutton", + "toolbar", + "actions" + ], + "factoryAnchors": [ + "CommandBar", + "AppBarButton" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022AppBarButtonInCommandBar\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (lastAction, setLastAction) = UseState(\u0022(none)\u0022);\r\n return VStack(12,\r\n CommandBar(\r\n primaryCommands: new[]\r\n {\r\n AppBarButton(\u0022Add\u0022, () =\u003E setLastAction(\u0022Add\u0022), icon: \u0022Add\u0022),\r\n AppBarButton(\u0022Share\u0022, () =\u003E setLastAction(\u0022Share\u0022), icon: \u0022Share\u0022)\r\n },\r\n secondaryCommands: new[]\r\n {\r\n AppBarButton(\u0022Delete\u0022, () =\u003E setLastAction(\u0022Delete\u0022), icon: \u0022Delete\u0022)\r\n }),\r\n TextBlock($\u0022Last action: {lastAction}\u0022).Padding(24));\r\n }\r\n}", + "rawCode": "// id: appbarbutton-in-commandbar\n// intent: command bar with app bar buttons\n#:package Microsoft.UI.Reactor@0.0.0-local\n#:property Platform=ARM64\n\nusing Microsoft.UI.Reactor;\nusing Microsoft.UI.Reactor.Core;\nusing static Microsoft.UI.Reactor.Factories;\n\nReactorApp.Run\u003CApp\u003E(\u0022AppBarButtonInCommandBar\u0022, width: 400, height: 300);\n\nclass App : Component\n{\n public override Element Render()\n {\n var (lastAction, setLastAction) = UseState(\u0022(none)\u0022);\n return VStack(12,\n CommandBar(\n primaryCommands: new[]\n {\n AppBarButton(\u0022Add\u0022, () =\u003E setLastAction(\u0022Add\u0022), icon: \u0022Add\u0022),\n AppBarButton(\u0022Share\u0022, () =\u003E setLastAction(\u0022Share\u0022), icon: \u0022Share\u0022)\n },\n secondaryCommands: new[]\n {\n AppBarButton(\u0022Delete\u0022, () =\u003E setLastAction(\u0022Delete\u0022), icon: \u0022Delete\u0022)\n }),\n TextBlock($\u0022Last action: {lastAction}\u0022).Padding(24));\n }\n}" + }, + { + "id": "button-label-onclick", + "category": "buttons", + "title": "Button click counter", + "intent": "basic button with click handler that increments a counter", + "tags": [ + "button", + "click", + "counter", + "onclick" + ], + "factoryAnchors": [ + "Button", + "UseState" + ], + "notesKey": "Button", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ButtonLabelOnClick\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (count, setCount) = UseState(0);\r\n return VStack(12,\r\n Heading(\u0022Click counter\u0022),\r\n TextBlock($\u0022Clicked {count} time{(count == 1 ? \u0022\u0022 : \u0022s\u0022)}.\u0022),\r\n Button(\u0022Increment\u0022, () =\u003E setCount(count \u002B 1)).AccentButton())\r\n .Padding(24);\r\n }\r\n}", + "rawCode": "// id: button-label-onclick\n// intent: basic button with click handler\n#:package Microsoft.UI.Reactor@0.0.0-local\n#:property Platform=ARM64\n\nusing Microsoft.UI.Reactor;\nusing Microsoft.UI.Reactor.Core;\nusing static Microsoft.UI.Reactor.Factories;\n\nReactorApp.Run\u003CApp\u003E(\u0022ButtonLabelOnClick\u0022, width: 400, height: 300);\n\nclass App : Component\n{\n public override Element Render()\n {\n var (count, setCount) = UseState(0);\n return VStack(12,\n Heading(\u0022Click counter\u0022),\n TextBlock($\u0022Clicked {count} time{(count == 1 ? \u0022\u0022 : \u0022s\u0022)}.\u0022),\n Button(\u0022Increment\u0022, () =\u003E setCount(count \u002B 1)).AccentButton())\n .Padding(24);\n }\n}" + }, + { + "id": "button-with-command", + "category": "buttons", + "title": "Button with Command", + "intent": "command-driven button where label action and enabled state come from a Command", + "tags": [ + "button", + "command", + "mvvm", + "canexecute" + ], + "factoryAnchors": [ + "Button" + ], + "notesKey": "Button", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ButtonWithCommand\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (runs, setRuns) = UseState(0);\r\n var save = new Command\r\n {\r\n Label = runs \u003C 3 ? $\u0022Save ({3 - runs} left)\u0022 : \u0022Done\u0022,\r\n Execute = () =\u003E setRuns(runs \u002B 1),\r\n CanExecute = runs \u003C 3\r\n };\r\n\r\n return VStack(12, Button(save), TextBlock($\u0022Executed: {runs}\u0022))\r\n .Padding(24);\r\n }\r\n}", + "rawCode": "// id: button-with-command\n// intent: button driven by a Command object (label, execute, canExecute)\n#:package Microsoft.UI.Reactor@0.0.0-local\n#:property Platform=ARM64\n\nusing Microsoft.UI.Reactor;\nusing Microsoft.UI.Reactor.Core;\nusing static Microsoft.UI.Reactor.Factories;\n\nReactorApp.Run\u003CApp\u003E(\u0022ButtonWithCommand\u0022, width: 400, height: 300);\n\nclass App : Component\n{\n public override Element Render()\n {\n var (runs, setRuns) = UseState(0);\n var save = new Command\n {\n Label = runs \u003C 3 ? $\u0022Save ({3 - runs} left)\u0022 : \u0022Done\u0022,\n Execute = () =\u003E setRuns(runs \u002B 1),\n CanExecute = runs \u003C 3\n };\n\n return VStack(12, Button(save), TextBlock($\u0022Executed: {runs}\u0022))\n .Padding(24);\n }\n}" + }, + { + "id": "button-with-icon", + "category": "buttons", + "title": "Button with icon", + "intent": "button with an icon and text using a symbol icon", + "tags": [ + "button", + "icon", + "symbol" + ], + "factoryAnchors": [ + "Button" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Xaml.Controls;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ButtonWithIcon\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (status, setStatus) = UseState(\u0022Ready\u0022);\r\n return VStack(12,\r\n Button(HStack(8, Icon(Symbol.Save), TextBlock(\u0022Save draft\u0022)),\r\n () =\u003E setStatus(\u0022Draft saved\u0022))\r\n .AutomationName(\u0022Save draft\u0022)\r\n .AccentButton(),\r\n TextBlock(status).Foreground(Theme.SecondaryText))\r\n .Padding(24);\r\n }\r\n}", + "rawCode": "// id: button-with-icon\n// intent: button with a symbol icon\n#:package Microsoft.UI.Reactor@0.0.0-local\n#:property Platform=ARM64\n\nusing Microsoft.UI.Reactor;\nusing Microsoft.UI.Reactor.Core;\nusing Microsoft.UI.Xaml.Controls;\nusing static Microsoft.UI.Reactor.Factories;\n\nReactorApp.Run\u003CApp\u003E(\u0022ButtonWithIcon\u0022, width: 400, height: 300);\n\nclass App : Component\n{\n public override Element Render()\n {\n var (status, setStatus) = UseState(\u0022Ready\u0022);\n return VStack(12,\n Button(HStack(8, Icon(Symbol.Save), TextBlock(\u0022Save draft\u0022)),\n () =\u003E setStatus(\u0022Draft saved\u0022))\n .AutomationName(\u0022Save draft\u0022)\n .AccentButton(),\n TextBlock(status).Foreground(Theme.SecondaryText))\n .Padding(24);\n }\n}" + }, + { + "id": "hyperlink-button", + "category": "buttons", + "title": "Hyperlink button", + "intent": "hyperlink-style button for link-like navigation or actions", + "tags": [ + "hyperlink", + "link", + "button" + ], + "factoryAnchors": [ + "HyperlinkButton" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022HyperlinkButton\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (status, setStatus) = UseState(\u0022Open docs\u0022);\r\n return VStack(12,\r\n TextBlock(\u0022Need more details?\u0022),\r\n HyperlinkButton(\u0022Open docs\u0022, onClick: () =\u003E setStatus(\u0022Navigation requested\u0022))\r\n .TextLink(),\r\n TextBlock(status).Foreground(Theme.SecondaryText))\r\n .Padding(24);\r\n }\r\n}", + "rawCode": "// id: hyperlink-button\n// intent: inline hyperlink-style button\n#:package Microsoft.UI.Reactor@0.0.0-local\n#:property Platform=ARM64\n\nusing Microsoft.UI.Reactor;\nusing Microsoft.UI.Reactor.Core;\nusing static Microsoft.UI.Reactor.Factories;\n\nReactorApp.Run\u003CApp\u003E(\u0022HyperlinkButton\u0022, width: 400, height: 300);\n\nclass App : Component\n{\n public override Element Render()\n {\n var (status, setStatus) = UseState(\u0022Open docs\u0022);\n return VStack(12,\n TextBlock(\u0022Need more details?\u0022),\n HyperlinkButton(\u0022Open docs\u0022, onClick: () =\u003E setStatus(\u0022Navigation requested\u0022))\n .TextLink(),\n TextBlock(status).Foreground(Theme.SecondaryText))\n .Padding(24);\n }\n}" + }, + { + "id": "togglebutton-basic", + "category": "buttons", + "title": "Basic toggle button", + "intent": "toggle button that tracks checked state with UseState", + "tags": [ + "toggle", + "togglebutton", + "checked", + "state" + ], + "factoryAnchors": [ + "ToggleButton", + "UseState" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ToggleButtonBasic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (isOn, setIsOn) = UseState(false);\r\n return VStack(12,\r\n ToggleButton(\u0022Notifications\u0022, isOn, setIsOn),\r\n TextBlock(isOn ? \u0022Notifications are on\u0022 : \u0022Notifications are off\u0022)\r\n .Foreground(Theme.SecondaryText))\r\n .Padding(24);\r\n }\r\n}", + "rawCode": "// id: togglebutton-basic\n// intent: toggle button with checked state\n#:package Microsoft.UI.Reactor@0.0.0-local\n#:property Platform=ARM64\n\nusing Microsoft.UI.Reactor;\nusing Microsoft.UI.Reactor.Core;\nusing static Microsoft.UI.Reactor.Factories;\n\nReactorApp.Run\u003CApp\u003E(\u0022ToggleButtonBasic\u0022, width: 400, height: 300);\n\nclass App : Component\n{\n public override Element Render()\n {\n var (isOn, setIsOn) = UseState(false);\n return VStack(12,\n ToggleButton(\u0022Notifications\u0022, isOn, setIsOn),\n TextBlock(isOn ? \u0022Notifications are on\u0022 : \u0022Notifications are off\u0022)\n .Foreground(Theme.SecondaryText))\n .Padding(24);\n }\n}" + }, + { + "id": "form-async-submit", + "category": "forms", + "title": "Async submit form", + "intent": "form submission with loading state", + "tags": [ + "form", + "submit", + "async", + "loading", + "state" + ], + "factoryAnchors": [ + "Button", + "UseState", + "ProgressRing" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using System.Threading.Tasks;\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form async submit\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (name, setName) = UseState(\u0022\u0022);\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (isSubmitting, setIsSubmitting) = UseState(false, threadSafe: true);\r\n var (status, setStatus) = UseState(\u0022Fill in the form.\u0022, threadSafe: true);\r\n\r\n return VStack(12,\r\n Heading(\u0022Newsletter signup\u0022),\r\n TextField(name, setName, placeholder: \u0022Full name\u0022, header: \u0022Name\u0022),\r\n TextField(email, setEmail, placeholder: \u0022you@example.com\u0022, header: \u0022Email\u0022),\r\n HStack(8,\r\n Button(isSubmitting ? \u0022Submitting...\u0022 : \u0022Submit\u0022, () =\u003E\r\n {\r\n if (isSubmitting) return;\r\n setIsSubmitting(true);\r\n setStatus(\u0022Submitting...\u0022);\r\n _ = Task.Run(async () =\u003E\r\n {\r\n await Task.Delay(1200);\r\n setStatus($\u0022Saved {name} ({email}).\u0022);\r\n setIsSubmitting(false);\r\n });\r\n })\r\n .AccentButton()\r\n .Set(b =\u003E b.IsEnabled = !isSubmitting \u0026\u0026 !string.IsNullOrWhiteSpace(name) \u0026\u0026 !string.IsNullOrWhiteSpace(email)),\r\n isSubmitting ? ProgressRing().IsActive(true).Width(20).Height(20) : Empty()),\r\n TextBlock(status))\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: form-async-submit\r\n// intent: form submission with loading state\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System.Threading.Tasks;\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form async submit\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (name, setName) = UseState(\u0022\u0022);\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (isSubmitting, setIsSubmitting) = UseState(false, threadSafe: true);\r\n var (status, setStatus) = UseState(\u0022Fill in the form.\u0022, threadSafe: true);\r\n\r\n return VStack(12,\r\n Heading(\u0022Newsletter signup\u0022),\r\n TextField(name, setName, placeholder: \u0022Full name\u0022, header: \u0022Name\u0022),\r\n TextField(email, setEmail, placeholder: \u0022you@example.com\u0022, header: \u0022Email\u0022),\r\n HStack(8,\r\n Button(isSubmitting ? \u0022Submitting...\u0022 : \u0022Submit\u0022, () =\u003E\r\n {\r\n if (isSubmitting) return;\r\n setIsSubmitting(true);\r\n setStatus(\u0022Submitting...\u0022);\r\n _ = Task.Run(async () =\u003E\r\n {\r\n await Task.Delay(1200);\r\n setStatus($\u0022Saved {name} ({email}).\u0022);\r\n setIsSubmitting(false);\r\n });\r\n })\r\n .AccentButton()\r\n .Set(b =\u003E b.IsEnabled = !isSubmitting \u0026\u0026 !string.IsNullOrWhiteSpace(name) \u0026\u0026 !string.IsNullOrWhiteSpace(email)),\r\n isSubmitting ? ProgressRing().IsActive(true).Width(20).Height(20) : Empty()),\r\n TextBlock(status))\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "form-field-wrapper", + "category": "forms", + "title": "FormField wrapper", + "intent": "FormField wrapper with label and required marker", + "tags": [ + "form", + "formfield", + "label", + "required", + "wrapper" + ], + "factoryAnchors": [ + "FormField", + "TextField" + ], + "notesKey": "FormField", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls.Validation;\r\nusing static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form field wrapper\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var ctx = this.UseValidationContext();\r\n var (fullName, setFullName) = UseState(\u0022\u0022);\r\n var (company, setCompany) = UseState(\u0022\u0022);\r\n var (role, setRole) = UseState(\u0022\u0022);\r\n\r\n return VStack(12,\r\n Heading(\u0022Profile details\u0022),\r\n FormField(TextField(fullName, v =\u003E { setFullName(v); ctx.MarkTouched(\u0022fullName\u0022); }, placeholder: \u0022Ada Lovelace\u0022)\r\n .Validate(\u0022fullName\u0022, fullName, Validate.Required(\u0022Full name is required\u0022)),\r\n label: \u0022Full name\u0022, required: true, description: \u0022Shown on receipts\u0022),\r\n FormField(TextField(company, v =\u003E { setCompany(v); ctx.MarkTouched(\u0022company\u0022); }, placeholder: \u0022Contoso\u0022)\r\n .Validate(\u0022company\u0022, company, Validate.MinLength(2, \u0022Company name looks too short\u0022)),\r\n label: \u0022Company\u0022, description: \u0022Optional but useful\u0022),\r\n FormField(TextField(role, v =\u003E { setRole(v); ctx.MarkTouched(\u0022role\u0022); }, placeholder: \u0022Product manager\u0022),\r\n label: \u0022Role\u0022, description: \u0022No validation on this field\u0022),\r\n Button(\u0022Show field state\u0022, () =\u003E ctx.MarkAllTouched()))\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: form-field-wrapper\r\n// intent: FormField wrapper with label and required marker\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls.Validation;\r\nusing static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form field wrapper\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var ctx = this.UseValidationContext();\r\n var (fullName, setFullName) = UseState(\u0022\u0022);\r\n var (company, setCompany) = UseState(\u0022\u0022);\r\n var (role, setRole) = UseState(\u0022\u0022);\r\n\r\n return VStack(12,\r\n Heading(\u0022Profile details\u0022),\r\n FormField(TextField(fullName, v =\u003E { setFullName(v); ctx.MarkTouched(\u0022fullName\u0022); }, placeholder: \u0022Ada Lovelace\u0022)\r\n .Validate(\u0022fullName\u0022, fullName, Validate.Required(\u0022Full name is required\u0022)),\r\n label: \u0022Full name\u0022, required: true, description: \u0022Shown on receipts\u0022),\r\n FormField(TextField(company, v =\u003E { setCompany(v); ctx.MarkTouched(\u0022company\u0022); }, placeholder: \u0022Contoso\u0022)\r\n .Validate(\u0022company\u0022, company, Validate.MinLength(2, \u0022Company name looks too short\u0022)),\r\n label: \u0022Company\u0022, description: \u0022Optional but useful\u0022),\r\n FormField(TextField(role, v =\u003E { setRole(v); ctx.MarkTouched(\u0022role\u0022); }, placeholder: \u0022Product manager\u0022),\r\n label: \u0022Role\u0022, description: \u0022No validation on this field\u0022),\r\n Button(\u0022Show field state\u0022, () =\u003E ctx.MarkAllTouched()))\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "form-submit-gating", + "category": "forms", + "title": "Submit gating", + "intent": "disable submit button until form is valid", + "tags": [ + "form", + "submit", + "gating", + "validation", + "enabled" + ], + "factoryAnchors": [ + "UseValidationContext", + "Button", + "FormField" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls.Validation;\r\nusing static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form submit gating\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var ctx = this.UseValidationContext();\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (accessCode, setAccessCode) = UseState(\u0022\u0022);\r\n var (status, setStatus) = UseState(\u0022Waiting for valid input.\u0022);\r\n\r\n return VStack(12,\r\n Heading(\u0022Submit gating\u0022),\r\n FormField(TextField(email, v =\u003E { setEmail(v); ctx.MarkTouched(\u0022email\u0022); }, placeholder: \u0022you@example.com\u0022)\r\n .Validate(\u0022email\u0022, email, Validate.Required(\u0022Email is required\u0022), Validate.Email(\u0022Enter a valid email\u0022)),\r\n label: \u0022Email\u0022, required: true),\r\n FormField(TextField(accessCode, v =\u003E { setAccessCode(v); ctx.MarkTouched(\u0022accessCode\u0022); }, placeholder: \u0022ACCESS-123\u0022)\r\n .Validate(\u0022accessCode\u0022, accessCode, Validate.Required(\u0022Code is required\u0022), Validate.MinLength(8, \u0022Use at least 8 characters\u0022)),\r\n label: \u0022Access code\u0022, required: true),\r\n Button(\u0022Submit\u0022, () =\u003E setStatus($\u0022Submitted for {email}.\u0022))\r\n .AccentButton()\r\n .Set(b =\u003E b.IsEnabled = ctx.IsValid()),\r\n TextBlock(status))\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: form-submit-gating\r\n// intent: disable submit button until form is valid\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls.Validation;\r\nusing static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form submit gating\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var ctx = this.UseValidationContext();\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (accessCode, setAccessCode) = UseState(\u0022\u0022);\r\n var (status, setStatus) = UseState(\u0022Waiting for valid input.\u0022);\r\n\r\n return VStack(12,\r\n Heading(\u0022Submit gating\u0022),\r\n FormField(TextField(email, v =\u003E { setEmail(v); ctx.MarkTouched(\u0022email\u0022); }, placeholder: \u0022you@example.com\u0022)\r\n .Validate(\u0022email\u0022, email, Validate.Required(\u0022Email is required\u0022), Validate.Email(\u0022Enter a valid email\u0022)),\r\n label: \u0022Email\u0022, required: true),\r\n FormField(TextField(accessCode, v =\u003E { setAccessCode(v); ctx.MarkTouched(\u0022accessCode\u0022); }, placeholder: \u0022ACCESS-123\u0022)\r\n .Validate(\u0022accessCode\u0022, accessCode, Validate.Required(\u0022Code is required\u0022), Validate.MinLength(8, \u0022Use at least 8 characters\u0022)),\r\n label: \u0022Access code\u0022, required: true),\r\n Button(\u0022Submit\u0022, () =\u003E setStatus($\u0022Submitted for {email}.\u0022))\r\n .AccentButton()\r\n .Set(b =\u003E b.IsEnabled = ctx.IsValid()),\r\n TextBlock(status))\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "form-text-fields", + "category": "forms", + "title": "Basic text fields form", + "intent": "basic form with multiple text inputs and labels", + "tags": [ + "form", + "textfield", + "input", + "layout" + ], + "factoryAnchors": [ + "TextField", + "VStack", + "UseState" + ], + "notesKey": "TextField", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form text fields\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (name, setName) = UseState(\u0022\u0022);\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (message, setMessage) = UseState(\u0022\u0022);\r\n\r\n return VStack(12,\r\n Heading(\u0022Contact form\u0022),\r\n TextField(name, setName, placeholder: \u0022Full name\u0022, header: \u0022Name\u0022),\r\n TextField(email, setEmail, placeholder: \u0022you@example.com\u0022, header: \u0022Email\u0022),\r\n TextField(message, setMessage, placeholder: \u0022How can we help?\u0022, header: \u0022Message\u0022),\r\n TextBlock($\u0022Draft: {name} / {email}\u0022))\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: form-text-fields\r\n// intent: basic form with multiple text inputs and labels\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form text fields\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (name, setName) = UseState(\u0022\u0022);\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (message, setMessage) = UseState(\u0022\u0022);\r\n\r\n return VStack(12,\r\n Heading(\u0022Contact form\u0022),\r\n TextField(name, setName, placeholder: \u0022Full name\u0022, header: \u0022Name\u0022),\r\n TextField(email, setEmail, placeholder: \u0022you@example.com\u0022, header: \u0022Email\u0022),\r\n TextField(message, setMessage, placeholder: \u0022How can we help?\u0022, header: \u0022Message\u0022),\r\n TextBlock($\u0022Draft: {name} / {email}\u0022))\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "form-validation-context", + "category": "forms", + "title": "Validation context form", + "intent": "form validation with UseValidationContext", + "tags": [ + "form", + "validation", + "required", + "email", + "error" + ], + "factoryAnchors": [ + "UseValidationContext", + "FormField", + "TextField" + ], + "notesKey": "UseValidationContext", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls.Validation;\r\nusing static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form validation context\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var ctx = this.UseValidationContext();\r\n var (name, setName) = UseState(\u0022\u0022);\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (password, setPassword) = UseState(\u0022\u0022);\r\n var (submitted, setSubmitted) = UseState(false);\r\n var showWhen = submitted ? ShowWhen.Always : ShowWhen.WhenTouched;\r\n\r\n return VStack(12,\r\n Heading(\u0022Create account\u0022),\r\n FormField(TextField(name, v =\u003E { setName(v); ctx.MarkTouched(\u0022name\u0022); }, placeholder: \u0022Full name\u0022)\r\n .Validate(\u0022name\u0022, name, Validate.Required(\u0022Name is required\u0022), Validate.MinLength(2, \u0022Use at least 2 characters\u0022)),\r\n label: \u0022Name\u0022, required: true, showWhen: showWhen),\r\n FormField(TextField(email, v =\u003E { setEmail(v); ctx.MarkTouched(\u0022email\u0022); }, placeholder: \u0022you@example.com\u0022)\r\n .Validate(\u0022email\u0022, email, Validate.Required(\u0022Email is required\u0022), Validate.Email(\u0022Enter a valid email\u0022)),\r\n label: \u0022Email\u0022, required: true, showWhen: showWhen),\r\n FormField(TextField(password, v =\u003E { setPassword(v); ctx.MarkTouched(\u0022password\u0022); }, placeholder: \u0022Minimum 8 characters\u0022)\r\n .Validate(\u0022password\u0022, password, Validate.Required(\u0022Password is required\u0022), Validate.MinLength(8, \u0022Use at least 8 characters\u0022)),\r\n label: \u0022Password\u0022, required: true, showWhen: showWhen),\r\n Button(\u0022Validate\u0022, () =\u003E { setSubmitted(true); ctx.MarkAllTouched(); }).AccentButton(),\r\n TextBlock(ctx.IsValid() ? \u0022All fields pass validation.\u0022 : \u0022Fix the errors above.\u0022))\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: form-validation-context\r\n// intent: form validation with UseValidationContext\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls.Validation;\r\nusing static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form validation context\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var ctx = this.UseValidationContext();\r\n var (name, setName) = UseState(\u0022\u0022);\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (password, setPassword) = UseState(\u0022\u0022);\r\n var (submitted, setSubmitted) = UseState(false);\r\n var showWhen = submitted ? ShowWhen.Always : ShowWhen.WhenTouched;\r\n\r\n return VStack(12,\r\n Heading(\u0022Create account\u0022),\r\n FormField(TextField(name, v =\u003E { setName(v); ctx.MarkTouched(\u0022name\u0022); }, placeholder: \u0022Full name\u0022)\r\n .Validate(\u0022name\u0022, name, Validate.Required(\u0022Name is required\u0022), Validate.MinLength(2, \u0022Use at least 2 characters\u0022)),\r\n label: \u0022Name\u0022, required: true, showWhen: showWhen),\r\n FormField(TextField(email, v =\u003E { setEmail(v); ctx.MarkTouched(\u0022email\u0022); }, placeholder: \u0022you@example.com\u0022)\r\n .Validate(\u0022email\u0022, email, Validate.Required(\u0022Email is required\u0022), Validate.Email(\u0022Enter a valid email\u0022)),\r\n label: \u0022Email\u0022, required: true, showWhen: showWhen),\r\n FormField(TextField(password, v =\u003E { setPassword(v); ctx.MarkTouched(\u0022password\u0022); }, placeholder: \u0022Minimum 8 characters\u0022)\r\n .Validate(\u0022password\u0022, password, Validate.Required(\u0022Password is required\u0022), Validate.MinLength(8, \u0022Use at least 8 characters\u0022)),\r\n label: \u0022Password\u0022, required: true, showWhen: showWhen),\r\n Button(\u0022Validate\u0022, () =\u003E { setSubmitted(true); ctx.MarkAllTouched(); }).AccentButton(),\r\n TextBlock(ctx.IsValid() ? \u0022All fields pass validation.\u0022 : \u0022Fix the errors above.\u0022))\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "form-with-server-errors", + "category": "forms", + "title": "Server-side field errors", + "intent": "display server-side validation errors on form fields", + "tags": [ + "form", + "server", + "errors", + "validation", + "api" + ], + "factoryAnchors": [ + "UseState", + "FormField", + "TextField" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls.Validation;\r\nusing static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form with server errors\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var ctx = this.UseValidationContext();\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (inviteCode, setInviteCode) = UseState(\u0022\u0022);\r\n var (status, setStatus) = UseState(\u0022Submit to simulate API validation.\u0022);\r\n\r\n return VStack(12,\r\n Heading(\u0022Invite signup\u0022),\r\n FormField(TextField(email, v =\u003E { setEmail(v); ctx.MarkTouched(\u0022email\u0022); }, placeholder: \u0022user@contoso.com\u0022)\r\n .Validate(\u0022email\u0022, email, Validate.Required(\u0022Email is required\u0022), Validate.Email(\u0022Enter a valid email\u0022)),\r\n label: \u0022Email\u0022, required: true, showWhen: ShowWhen.Always),\r\n FormField(TextField(inviteCode, v =\u003E { setInviteCode(v); ctx.MarkTouched(\u0022inviteCode\u0022); }, placeholder: \u0022INVITE-2025\u0022)\r\n .Validate(\u0022inviteCode\u0022, inviteCode, Validate.Required(\u0022Invite code is required\u0022), Validate.MinLength(6, \u0022Use at least 6 characters\u0022)),\r\n label: \u0022Invite code\u0022, required: true, showWhen: ShowWhen.Always),\r\n Button(\u0022Submit\u0022, () =\u003E\r\n {\r\n ctx.ClearExternal(\u0022email\u0022);\r\n ctx.ClearExternal(\u0022inviteCode\u0022);\r\n ctx.MarkAllTouched();\r\n if (!ctx.IsValid())\r\n {\r\n setStatus(\u0022Fix the client-side errors first.\u0022);\r\n return;\r\n }\r\n if (!email.EndsWith(\u0022@contoso.com\u0022, System.StringComparison.OrdinalIgnoreCase))\r\n ctx.AddExternal(\u0022email\u0022, \u0022This email is not allowed by the server.\u0022);\r\n if (inviteCode != \u0022INVITE-2025\u0022)\r\n ctx.AddExternal(\u0022inviteCode\u0022, \u0022That invite code has expired.\u0022);\r\n setStatus(ctx.IsValid() ? \u0022Server accepted the form.\u0022 : \u0022Server returned field errors.\u0022);\r\n }).AccentButton(),\r\n TextBlock(status))\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: form-with-server-errors\r\n// intent: display server-side validation errors on form fields\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls.Validation;\r\nusing static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Form with server errors\u0022, width: 500, height: 400);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var ctx = this.UseValidationContext();\r\n var (email, setEmail) = UseState(\u0022\u0022);\r\n var (inviteCode, setInviteCode) = UseState(\u0022\u0022);\r\n var (status, setStatus) = UseState(\u0022Submit to simulate API validation.\u0022);\r\n\r\n return VStack(12,\r\n Heading(\u0022Invite signup\u0022),\r\n FormField(TextField(email, v =\u003E { setEmail(v); ctx.MarkTouched(\u0022email\u0022); }, placeholder: \u0022user@contoso.com\u0022)\r\n .Validate(\u0022email\u0022, email, Validate.Required(\u0022Email is required\u0022), Validate.Email(\u0022Enter a valid email\u0022)),\r\n label: \u0022Email\u0022, required: true, showWhen: ShowWhen.Always),\r\n FormField(TextField(inviteCode, v =\u003E { setInviteCode(v); ctx.MarkTouched(\u0022inviteCode\u0022); }, placeholder: \u0022INVITE-2025\u0022)\r\n .Validate(\u0022inviteCode\u0022, inviteCode, Validate.Required(\u0022Invite code is required\u0022), Validate.MinLength(6, \u0022Use at least 6 characters\u0022)),\r\n label: \u0022Invite code\u0022, required: true, showWhen: ShowWhen.Always),\r\n Button(\u0022Submit\u0022, () =\u003E\r\n {\r\n ctx.ClearExternal(\u0022email\u0022);\r\n ctx.ClearExternal(\u0022inviteCode\u0022);\r\n ctx.MarkAllTouched();\r\n if (!ctx.IsValid())\r\n {\r\n setStatus(\u0022Fix the client-side errors first.\u0022);\r\n return;\r\n }\r\n if (!email.EndsWith(\u0022@contoso.com\u0022, System.StringComparison.OrdinalIgnoreCase))\r\n ctx.AddExternal(\u0022email\u0022, \u0022This email is not allowed by the server.\u0022);\r\n if (inviteCode != \u0022INVITE-2025\u0022)\r\n ctx.AddExternal(\u0022inviteCode\u0022, \u0022That invite code has expired.\u0022);\r\n setStatus(ctx.IsValid() ? \u0022Server accepted the form.\u0022 : \u0022Server returned field errors.\u0022);\r\n }).AccentButton(),\r\n TextBlock(status))\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "async-fetch-list", + "category": "hooks", + "title": "Async Fetch List", + "intent": "fetch async data with UseResource and render loading, error, and reloading states", + "tags": [ + "async", + "fetch", + "resource", + "loading", + "error", + "reloading" + ], + "factoryAnchors": [ + "UseResource", + "InfoBar", + "ProgressRing" + ], + "notesKey": "UseResource", + "relatedIds": [ + "list-with-loading" + ], + "priority": "P0", + "code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Xaml.Controls;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Async Fetch List\u0022, width: 560, height: 520);\r\n\r\nrecord Repo(int Id, string Name, string Description);\r\n\r\nstatic class Api\r\n{\r\n public static async Task\u003CIReadOnlyList\u003CRepo\u003E\u003E ListReposAsync(string owner, CancellationToken cancellationToken)\r\n {\r\n await Task.Delay(800, cancellationToken);\r\n if (owner == \u0022fail\u0022)\r\n {\r\n throw new InvalidOperationException(\u0022Owner not found\u0022);\r\n }\r\n\r\n return\r\n [\r\n new(1, $\u0022{owner}/alpha\u0022, \u0022first repo\u0022),\r\n new(2, $\u0022{owner}/beta\u0022, \u0022second repo\u0022),\r\n new(3, $\u0022{owner}/gamma\u0022, \u0022third repo\u0022)\r\n ];\r\n }\r\n}\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (owner, setOwner) = UseState(\u0022microsoft\u0022);\r\n var repos = UseResource(cancellationToken =\u003E Api.ListReposAsync(owner, cancellationToken), deps: [owner]);\r\n\r\n return VStack(12,\r\n TextField(owner, setOwner, placeholder: \u0022GitHub owner\u0022),\r\n Caption(\u0022Try \\\u0022fail\\\u0022 to see the error state.\u0022),\r\n repos.Match\u003CElement\u003E(\r\n loading: () =\u003E HStack(8,\r\n ProgressRing().IsActive(true).Width(20).Height(20),\r\n TextBlock(\u0022Loading\u2026\u0022)),\r\n data: list =\u003E VStack(8,\r\n list.Select(repo =\u003E\r\n Border(\r\n VStack(2,\r\n TextBlock(repo.Name).Bold(),\r\n Caption(repo.Description)))\r\n .Padding(12)\r\n .CornerRadius(6)\r\n .WithKey(repo.Id.ToString())\r\n ).ToArray\u003CElement?\u003E()),\r\n error: ex =\u003E InfoBar(\u0022Error\u0022, ex.Message).Severity(InfoBarSeverity.Error),\r\n reloading: previous =\u003E VStack(8,\r\n HStack(8,\r\n ProgressRing().IsActive(true).Width(20).Height(20),\r\n TextBlock(\u0022Refreshing\u2026\u0022)),\r\n VStack(8,\r\n previous.Select(repo =\u003E\r\n TextBlock(repo.Name)\r\n .Opacity(0.5)\r\n .WithKey(repo.Id.ToString()))\r\n .ToArray\u003CElement?\u003E()))))\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: async-fetch-list\r\n// intent: fetch async data with UseResource and render loading, error, and reloading states\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Xaml.Controls;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Async Fetch List\u0022, width: 560, height: 520);\r\n\r\nrecord Repo(int Id, string Name, string Description);\r\n\r\nstatic class Api\r\n{\r\n public static async Task\u003CIReadOnlyList\u003CRepo\u003E\u003E ListReposAsync(string owner, CancellationToken cancellationToken)\r\n {\r\n await Task.Delay(800, cancellationToken);\r\n if (owner == \u0022fail\u0022)\r\n {\r\n throw new InvalidOperationException(\u0022Owner not found\u0022);\r\n }\r\n\r\n return\r\n [\r\n new(1, $\u0022{owner}/alpha\u0022, \u0022first repo\u0022),\r\n new(2, $\u0022{owner}/beta\u0022, \u0022second repo\u0022),\r\n new(3, $\u0022{owner}/gamma\u0022, \u0022third repo\u0022)\r\n ];\r\n }\r\n}\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (owner, setOwner) = UseState(\u0022microsoft\u0022);\r\n var repos = UseResource(cancellationToken =\u003E Api.ListReposAsync(owner, cancellationToken), deps: [owner]);\r\n\r\n return VStack(12,\r\n TextField(owner, setOwner, placeholder: \u0022GitHub owner\u0022),\r\n Caption(\u0022Try \\\u0022fail\\\u0022 to see the error state.\u0022),\r\n repos.Match\u003CElement\u003E(\r\n loading: () =\u003E HStack(8,\r\n ProgressRing().IsActive(true).Width(20).Height(20),\r\n TextBlock(\u0022Loading\u2026\u0022)),\r\n data: list =\u003E VStack(8,\r\n list.Select(repo =\u003E\r\n Border(\r\n VStack(2,\r\n TextBlock(repo.Name).Bold(),\r\n Caption(repo.Description)))\r\n .Padding(12)\r\n .CornerRadius(6)\r\n .WithKey(repo.Id.ToString())\r\n ).ToArray\u003CElement?\u003E()),\r\n error: ex =\u003E InfoBar(\u0022Error\u0022, ex.Message).Severity(InfoBarSeverity.Error),\r\n reloading: previous =\u003E VStack(8,\r\n HStack(8,\r\n ProgressRing().IsActive(true).Width(20).Height(20),\r\n TextBlock(\u0022Refreshing\u2026\u0022)),\r\n VStack(8,\r\n previous.Select(repo =\u003E\r\n TextBlock(repo.Name)\r\n .Opacity(0.5)\r\n .WithKey(repo.Id.ToString()))\r\n .ToArray\u003CElement?\u003E()))))\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "custom-hook-pattern", + "category": "hooks", + "title": "Reusable custom hook", + "intent": "compose hooks into a reusable custom Use extension that debounces input", + "tags": [ + "custom", + "hook", + "extension", + "reuse", + "pattern", + "naming" + ], + "factoryAnchors": [ + "UseState", + "UseEffect" + ], + "notesKey": "UseState", + "relatedIds": [ + "use-effect-deps" + ], + "priority": "P0", + "code": "using System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Custom hooks should keep the Use* naming convention and compose other hooks internally.\r\nReactorApp.Run\u003CApp\u003E(\u0022CustomHookPattern\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render() =\u003E RenderEachTime(UseDebouncedView);\r\n\r\n static Element UseDebouncedView(RenderContext ctx)\r\n {\r\n var (query, setQuery) = ctx.UseState(\u0022\u0022);\r\n var debounced = ctx.UseDebouncedValue(query, 400);\r\n\r\n return VStack(12,\r\n TextBox(query, setQuery, \u0022Type quickly\u0022, header: \u0022Live query\u0022),\r\n TextBlock($\u0022Immediate: {query}\u0022),\r\n TextBlock($\u0022Debounced: {debounced}\u0022));\r\n }\r\n}\r\n\r\nstatic class DebounceHooks\r\n{\r\n public static T UseDebouncedValue\u003CT\u003E(this RenderContext ctx, T value, int delayMs)\r\n {\r\n var (debounced, setDebounced) = ctx.UseState(value);\r\n ctx.UseEffect(() =\u003E\r\n {\r\n var cts = new CancellationTokenSource();\r\n _ = Task.Run(async () =\u003E\r\n {\r\n try\r\n {\r\n await Task.Delay(delayMs, cts.Token);\r\n setDebounced(value);\r\n }\r\n catch (TaskCanceledException)\r\n {\r\n }\r\n });\r\n return () =\u003E cts.Cancel();\r\n }, value!, delayMs);\r\n return debounced;\r\n }\r\n}\r\n", + "rawCode": "// id: custom-hook-pattern\r\n// intent: compose hooks into a reusable custom Use* extension that debounces input\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Custom hooks should keep the Use* naming convention and compose other hooks internally.\r\nReactorApp.Run\u003CApp\u003E(\u0022CustomHookPattern\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render() =\u003E RenderEachTime(UseDebouncedView);\r\n\r\n static Element UseDebouncedView(RenderContext ctx)\r\n {\r\n var (query, setQuery) = ctx.UseState(\u0022\u0022);\r\n var debounced = ctx.UseDebouncedValue(query, 400);\r\n\r\n return VStack(12,\r\n TextBox(query, setQuery, \u0022Type quickly\u0022, header: \u0022Live query\u0022),\r\n TextBlock($\u0022Immediate: {query}\u0022),\r\n TextBlock($\u0022Debounced: {debounced}\u0022));\r\n }\r\n}\r\n\r\nstatic class DebounceHooks\r\n{\r\n public static T UseDebouncedValue\u003CT\u003E(this RenderContext ctx, T value, int delayMs)\r\n {\r\n var (debounced, setDebounced) = ctx.UseState(value);\r\n ctx.UseEffect(() =\u003E\r\n {\r\n var cts = new CancellationTokenSource();\r\n _ = Task.Run(async () =\u003E\r\n {\r\n try\r\n {\r\n await Task.Delay(delayMs, cts.Token);\r\n setDebounced(value);\r\n }\r\n catch (TaskCanceledException)\r\n {\r\n }\r\n });\r\n return () =\u003E cts.Cancel();\r\n }, value!, delayMs);\r\n return debounced;\r\n }\r\n}\r\n" + }, + { + "id": "use-callback", + "category": "hooks", + "title": "Stable callback identity", + "intent": "memoize a callback so child props stay stable across unrelated parent re-renders", + "tags": [ + "callback", + "memoize", + "stable", + "child", + "performance" + ], + "factoryAnchors": [ + "UseCallback", + "UseState", + "Button" + ], + "notesKey": "UseCallback", + "relatedIds": [ + "use-memo" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Stable callback identity helps memoized children skip unnecessary updates.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseCallback\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n record ChildProps(Action OnPress);\r\n\r\n public override Element Render()\r\n {\r\n var (parentRenders, setParentRenders) = UseState(0);\r\n var (clicks, setClicks) = UseState(0);\r\n var increment = UseCallback(() =\u003E setClicks(clicks \u002B 1), clicks);\r\n\r\n return VStack(12,\r\n Heading($\u0022Clicks: {clicks}\u0022),\r\n Button(\u0022Re-render parent\u0022, () =\u003E setParentRenders(parentRenders \u002B 1)),\r\n Caption($\u0022Unrelated parent renders: {parentRenders}\u0022),\r\n Component\u003CChildButton, ChildProps\u003E(new(increment)));\r\n }\r\n\r\n class ChildButton : Component\u003CChildProps\u003E\r\n {\r\n int _renders;\r\n\r\n public override Element Render()\r\n {\r\n _renders\u002B\u002B;\r\n return VStack(8,\r\n Caption($\u0022Child renders: {_renders}\u0022),\r\n Button(\u0022Increment from child\u0022, Props.OnPress));\r\n }\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-callback\r\n// intent: memoize a callback so child props stay stable across unrelated parent renders\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Stable callback identity helps memoized children skip unnecessary updates.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseCallback\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n record ChildProps(Action OnPress);\r\n\r\n public override Element Render()\r\n {\r\n var (parentRenders, setParentRenders) = UseState(0);\r\n var (clicks, setClicks) = UseState(0);\r\n var increment = UseCallback(() =\u003E setClicks(clicks \u002B 1), clicks);\r\n\r\n return VStack(12,\r\n Heading($\u0022Clicks: {clicks}\u0022),\r\n Button(\u0022Re-render parent\u0022, () =\u003E setParentRenders(parentRenders \u002B 1)),\r\n Caption($\u0022Unrelated parent renders: {parentRenders}\u0022),\r\n Component\u003CChildButton, ChildProps\u003E(new(increment)));\r\n }\r\n\r\n class ChildButton : Component\u003CChildProps\u003E\r\n {\r\n int _renders;\r\n\r\n public override Element Render()\r\n {\r\n _renders\u002B\u002B;\r\n return VStack(8,\r\n Caption($\u0022Child renders: {_renders}\u0022),\r\n Button(\u0022Increment from child\u0022, Props.OnPress));\r\n }\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-context-basic", + "category": "hooks", + "title": "Basic context provider", + "intent": "provide a context value at the root and consume it in a nested child", + "tags": [ + "context", + "provider", + "consume", + "share" + ], + "factoryAnchors": [ + "UseContext", + "Provider", + "VStack" + ], + "notesKey": "UseContext", + "relatedIds": [ + "use-context-multi", + "use-reducer-with-context" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Context removes prop-drilling when several descendants need the same value.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseContextBasic\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n internal static readonly Context\u003Cstring\u003E ThemeContext = new(\u0022Light\u0022);\r\n\r\n public override Element Render()\r\n {\r\n var (theme, setTheme) = UseState(\u0022Light\u0022);\r\n\r\n return VStack(12,\r\n Button(theme == \u0022Light\u0022 ? \u0022Switch to dark\u0022 : \u0022Switch to light\u0022,\r\n () =\u003E setTheme(theme == \u0022Light\u0022 ? \u0022Dark\u0022 : \u0022Light\u0022)).AutomationName(\u0022Toggle theme\u0022),\r\n Component\u003CThemeBadge\u003E())\r\n .Provide(ThemeContext, theme);\r\n }\r\n\r\n class ThemeBadge : Component\r\n {\r\n public override Element Render()\r\n {\r\n var theme = UseContext(ThemeContext);\r\n return TextBlock($\u0022Current theme from context: {theme}\u0022);\r\n }\r\n }\r\n}\r\n", + "rawCode": "// id: use-context-basic\r\n// intent: provide a value at the root and consume it from a descendant\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Context removes prop-drilling when several descendants need the same value.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseContextBasic\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n internal static readonly Context\u003Cstring\u003E ThemeContext = new(\u0022Light\u0022);\r\n\r\n public override Element Render()\r\n {\r\n var (theme, setTheme) = UseState(\u0022Light\u0022);\r\n\r\n return VStack(12,\r\n Button(theme == \u0022Light\u0022 ? \u0022Switch to dark\u0022 : \u0022Switch to light\u0022,\r\n () =\u003E setTheme(theme == \u0022Light\u0022 ? \u0022Dark\u0022 : \u0022Light\u0022)).AutomationName(\u0022Toggle theme\u0022),\r\n Component\u003CThemeBadge\u003E())\r\n .Provide(ThemeContext, theme);\r\n }\r\n\r\n class ThemeBadge : Component\r\n {\r\n public override Element Render()\r\n {\r\n var theme = UseContext(ThemeContext);\r\n return TextBlock($\u0022Current theme from context: {theme}\u0022);\r\n }\r\n }\r\n}\r\n" + }, + { + "id": "use-context-multi", + "category": "hooks", + "title": "Composed contexts", + "intent": "compose nested providers and consume multiple context values in one child", + "tags": [ + "context", + "multi", + "compose", + "nested", + "provider" + ], + "factoryAnchors": [ + "UseContext", + "Provider", + "VStack" + ], + "notesKey": "UseContext", + "relatedIds": [ + "use-context-basic" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Multiple contexts can be layered so consumers read each concern independently.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseContextMulti\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n internal static readonly Context\u003Cstring\u003E ThemeContext = new(\u0022Light\u0022);\r\n internal static readonly Context\u003Cstring\u003E UserContext = new(\u0022Guest\u0022);\r\n\r\n public override Element Render()\r\n {\r\n return VStack(12,\r\n Component\u003CProfileCard\u003E())\r\n .Provide(UserContext, \u0022Ada\u0022)\r\n .Provide(ThemeContext, \u0022Dark\u0022);\r\n }\r\n\r\n class ProfileCard : Component\r\n {\r\n public override Element Render()\r\n {\r\n var user = UseContext(UserContext);\r\n var theme = UseContext(ThemeContext);\r\n return VStack(8,\r\n Heading($\u0022Hello, {user}\u0022),\r\n Caption($\u0022Theme from context: {theme}\u0022));\r\n }\r\n }\r\n}\r\n", + "rawCode": "// id: use-context-multi\r\n// intent: compose multiple nested contexts and read them from the same child\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Multiple contexts can be layered so consumers read each concern independently.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseContextMulti\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n internal static readonly Context\u003Cstring\u003E ThemeContext = new(\u0022Light\u0022);\r\n internal static readonly Context\u003Cstring\u003E UserContext = new(\u0022Guest\u0022);\r\n\r\n public override Element Render()\r\n {\r\n return VStack(12,\r\n Component\u003CProfileCard\u003E())\r\n .Provide(UserContext, \u0022Ada\u0022)\r\n .Provide(ThemeContext, \u0022Dark\u0022);\r\n }\r\n\r\n class ProfileCard : Component\r\n {\r\n public override Element Render()\r\n {\r\n var user = UseContext(UserContext);\r\n var theme = UseContext(ThemeContext);\r\n return VStack(8,\r\n Heading($\u0022Hello, {user}\u0022),\r\n Caption($\u0022Theme from context: {theme}\u0022));\r\n }\r\n }\r\n}\r\n" + }, + { + "id": "use-effect-cleanup", + "category": "hooks", + "title": "Effect cleanup with timer", + "intent": "set up a timer in an effect and dispose it from the cleanup callback", + "tags": [ + "effect", + "cleanup", + "dispose", + "timer", + "subscription" + ], + "factoryAnchors": [ + "UseEffect", + "UseState", + "TextBlock" + ], + "notesKey": "UseEffect", + "relatedIds": [ + "use-effect-mount" + ], + "priority": "P0", + "code": "using System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Xaml;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Cleanup returns the timer to a stopped, unsubscribed state when the component leaves.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseEffectCleanup\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (seconds, updateSeconds) = UseReducer(0);\r\n\r\n UseEffect(() =\u003E\r\n {\r\n var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };\r\n EventHandler\u003Cobject\u003E onTick = (_, _) =\u003E updateSeconds(value =\u003E value \u002B 1);\r\n timer.Tick \u002B= onTick;\r\n timer.Start();\r\n\r\n return () =\u003E\r\n {\r\n timer.Tick -= onTick;\r\n timer.Stop();\r\n };\r\n }, Array.Empty\u003Cobject\u003E());\r\n\r\n return VStack(12,\r\n Heading($\u0022Elapsed: {seconds}s\u0022),\r\n Caption(\u0022The cleanup lambda stops the timer when this scenario unmounts.\u0022));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-effect-cleanup\r\n// intent: set up a timer in an effect and clean it up on unmount\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Xaml;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Cleanup returns the timer to a stopped, unsubscribed state when the component leaves.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseEffectCleanup\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (seconds, updateSeconds) = UseReducer(0);\r\n\r\n UseEffect(() =\u003E\r\n {\r\n var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };\r\n EventHandler\u003Cobject\u003E onTick = (_, _) =\u003E updateSeconds(value =\u003E value \u002B 1);\r\n timer.Tick \u002B= onTick;\r\n timer.Start();\r\n\r\n return () =\u003E\r\n {\r\n timer.Tick -= onTick;\r\n timer.Stop();\r\n };\r\n }, Array.Empty\u003Cobject\u003E());\r\n\r\n return VStack(12,\r\n Heading($\u0022Elapsed: {seconds}s\u0022),\r\n Caption(\u0022The cleanup lambda stops the timer when this scenario unmounts.\u0022));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-effect-deps", + "category": "hooks", + "title": "Effect dependencies", + "intent": "rerun an effect when a dependency changes by watching the current search query", + "tags": [ + "effect", + "deps", + "dependency", + "rerun" + ], + "factoryAnchors": [ + "UseEffect", + "UseState", + "TextBox", + "VStack" + ], + "notesKey": "UseEffect", + "relatedIds": [ + "use-effect-mount" + ], + "priority": "P0", + "code": "using System;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// The effect reruns for each query change and writes the latest derived results.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseEffectDeps\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n static readonly string[] Data = [\u0022Hooks\u0022, \u0022Reducer\u0022, \u0022Memo\u0022, \u0022Context\u0022, \u0022Ref\u0022, \u0022Effect\u0022];\r\n\r\n public override Element Render()\r\n {\r\n var (query, setQuery) = UseState(\u0022\u0022);\r\n var (runs, bumpRuns) = UseReducer(0);\r\n var (results, setResults) = UseState(Array.Empty\u003Cstring\u003E());\r\n\r\n UseEffect(() =\u003E\r\n {\r\n bumpRuns(value =\u003E value \u002B 1);\r\n setResults(query.Length == 0\r\n ? Array.Empty\u003Cstring\u003E()\r\n : Data.Where(item =\u003E item.Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray());\r\n }, query);\r\n\r\n return VStack(12,\r\n TextBox(query, setQuery, \u0022Search hooks\u0022, header: \u0022Search query\u0022),\r\n Caption($\u0022Effect runs: {runs}\u0022),\r\n ForEach(results, item =\u003E TextBlock(item)));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-effect-deps\r\n// intent: re-run an effect whenever the search query dependency changes\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// The effect reruns for each query change and writes the latest derived results.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseEffectDeps\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n static readonly string[] Data = [\u0022Hooks\u0022, \u0022Reducer\u0022, \u0022Memo\u0022, \u0022Context\u0022, \u0022Ref\u0022, \u0022Effect\u0022];\r\n\r\n public override Element Render()\r\n {\r\n var (query, setQuery) = UseState(\u0022\u0022);\r\n var (runs, bumpRuns) = UseReducer(0);\r\n var (results, setResults) = UseState(Array.Empty\u003Cstring\u003E());\r\n\r\n UseEffect(() =\u003E\r\n {\r\n bumpRuns(value =\u003E value \u002B 1);\r\n setResults(query.Length == 0\r\n ? Array.Empty\u003Cstring\u003E()\r\n : Data.Where(item =\u003E item.Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray());\r\n }, query);\r\n\r\n return VStack(12,\r\n TextBox(query, setQuery, \u0022Search hooks\u0022, header: \u0022Search query\u0022),\r\n Caption($\u0022Effect runs: {runs}\u0022),\r\n ForEach(results, item =\u003E TextBlock(item)));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-effect-mount", + "category": "hooks", + "title": "Mount-only effect", + "intent": "run an effect once after mount by passing an empty dependency array", + "tags": [ + "effect", + "mount", + "once", + "lifecycle" + ], + "factoryAnchors": [ + "UseEffect", + "VStack", + "TextBlock" + ], + "notesKey": "UseEffect", + "relatedIds": [ + "use-effect-cleanup", + "use-effect-deps" + ], + "priority": "P0", + "code": "using System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// An empty dependency array gives this effect mount-only semantics.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseEffectMount\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (status, setStatus) = UseState(\u0022Mounting...\u0022);\r\n\r\n UseEffect(() =\u003E\r\n {\r\n setStatus(\u0022Loaded initial data on mount.\u0022);\r\n }, Array.Empty\u003Cobject\u003E());\r\n\r\n return VStack(12,\r\n Heading(\u0022Mount effect\u0022),\r\n TextBlock(status),\r\n Caption(\u0022This effect runs once after the first render.\u0022));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-effect-mount\r\n// intent: run a fire-once effect after the component mounts\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// An empty dependency array gives this effect mount-only semantics.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseEffectMount\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (status, setStatus) = UseState(\u0022Mounting...\u0022);\r\n\r\n UseEffect(() =\u003E\r\n {\r\n setStatus(\u0022Loaded initial data on mount.\u0022);\r\n }, Array.Empty\u003Cobject\u003E());\r\n\r\n return VStack(12,\r\n Heading(\u0022Mount effect\u0022),\r\n TextBlock(status),\r\n Caption(\u0022This effect runs once after the first render.\u0022));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-memo", + "category": "hooks", + "title": "Derived state with UseMemo", + "intent": "memoize a filtered view of data so derived state only recomputes when inputs change", + "tags": [ + "memo", + "derived", + "computed", + "filter", + "performance" + ], + "factoryAnchors": [ + "UseMemo", + "UseState", + "TextBox", + "ForEach" + ], + "notesKey": "UseMemo", + "relatedIds": [ + "use-effect-deps" + ], + "priority": "P0", + "code": "using System;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// UseMemo is a good fit for derived values that are expensive or noisy to rebuild.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseMemo\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n static readonly string[] Items = [\u0022Apple\u0022, \u0022Apricot\u0022, \u0022Banana\u0022, \u0022Blueberry\u0022, \u0022Cherry\u0022, \u0022Date\u0022];\r\n\r\n public override Element Render()\r\n {\r\n var (filter, setFilter) = UseState(\u0022\u0022);\r\n var filtered = UseMemo(() =\u003E Items\r\n .Where(item =\u003E item.Contains(filter, StringComparison.OrdinalIgnoreCase))\r\n .ToArray(), filter);\r\n\r\n return VStack(12,\r\n TextBox(filter, setFilter, \u0022Filter fruit\u0022, header: \u0022Filter\u0022),\r\n Caption($\u0022Visible items: {filtered.Length}\u0022),\r\n ForEach(filtered, item =\u003E TextBlock(item)));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-memo\r\n// intent: memoize derived filtered data so it recomputes only when inputs change\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// UseMemo is a good fit for derived values that are expensive or noisy to rebuild.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseMemo\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n static readonly string[] Items = [\u0022Apple\u0022, \u0022Apricot\u0022, \u0022Banana\u0022, \u0022Blueberry\u0022, \u0022Cherry\u0022, \u0022Date\u0022];\r\n\r\n public override Element Render()\r\n {\r\n var (filter, setFilter) = UseState(\u0022\u0022);\r\n var filtered = UseMemo(() =\u003E Items\r\n .Where(item =\u003E item.Contains(filter, StringComparison.OrdinalIgnoreCase))\r\n .ToArray(), filter);\r\n\r\n return VStack(12,\r\n TextBox(filter, setFilter, \u0022Filter fruit\u0022, header: \u0022Filter\u0022),\r\n Caption($\u0022Visible items: {filtered.Length}\u0022),\r\n ForEach(filtered, item =\u003E TextBlock(item)));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-reducer-list", + "category": "hooks", + "title": "Todo list with UseReducer", + "intent": "manage list add delete and toggle operations through an immutable reducer", + "tags": [ + "reducer", + "list", + "add", + "delete", + "toggle", + "immutable" + ], + "factoryAnchors": [ + "UseReducer", + "ListView", + "Button", + "TextBox" + ], + "notesKey": "UseReducer", + "relatedIds": [ + "use-state-list-pitfall" + ], + "priority": "P0", + "code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// UseReducer keeps list updates centralized and immutable.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseReducerList\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n record Item(string Id, string Text, bool Done);\r\n abstract record TodoAction;\r\n record AddItem(string Text) : TodoAction;\r\n record ToggleItem(string Id) : TodoAction;\r\n record DeleteItem(string Id) : TodoAction;\r\n\r\n static IReadOnlyList\u003CItem\u003E Reduce(IReadOnlyList\u003CItem\u003E state, TodoAction action) =\u003E action switch\r\n {\r\n AddItem { Text: var text } when !string.IsNullOrWhiteSpace(text)\r\n =\u003E [.. state, new Item(Guid.NewGuid().ToString(\u0022N\u0022), text.Trim(), false)],\r\n ToggleItem { Id: var id }\r\n =\u003E state.Select(item =\u003E item.Id == id ? item with { Done = !item.Done } : item).ToArray(),\r\n DeleteItem { Id: var id }\r\n =\u003E state.Where(item =\u003E item.Id != id).ToArray(),\r\n _ =\u003E state\r\n };\r\n\r\n public override Element Render()\r\n {\r\n var (draft, setDraft) = UseState(\u0022\u0022);\r\n var (items, dispatch) = UseReducer\u003CIReadOnlyList\u003CItem\u003E, TodoAction\u003E(Reduce, Array.Empty\u003CItem\u003E());\r\n\r\n return VStack(12,\r\n Heading(\u0022Todo reducer\u0022),\r\n HStack(8,\r\n TextBox(draft, setDraft, \u0022Add a todo\u0022, header: \u0022Todo\u0022),\r\n Button(\u0022Add\u0022, () =\u003E { dispatch(new AddItem(draft)); setDraft(\u0022\u0022); })),\r\n ListView(items, item =\u003E item.Id, (item, _) =\u003E HStack(8,\r\n CheckBox(item.Done, isChecked =\u003E dispatch(new ToggleItem(item.Id)), item.Text),\r\n Button(\u0022Delete\u0022, () =\u003E dispatch(new DeleteItem(item.Id))))));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-reducer-list\r\n// intent: manage todo items with immutable add, toggle, and delete reducer actions\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// UseReducer keeps list updates centralized and immutable.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseReducerList\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n record Item(string Id, string Text, bool Done);\r\n abstract record TodoAction;\r\n record AddItem(string Text) : TodoAction;\r\n record ToggleItem(string Id) : TodoAction;\r\n record DeleteItem(string Id) : TodoAction;\r\n\r\n static IReadOnlyList\u003CItem\u003E Reduce(IReadOnlyList\u003CItem\u003E state, TodoAction action) =\u003E action switch\r\n {\r\n AddItem { Text: var text } when !string.IsNullOrWhiteSpace(text)\r\n =\u003E [.. state, new Item(Guid.NewGuid().ToString(\u0022N\u0022), text.Trim(), false)],\r\n ToggleItem { Id: var id }\r\n =\u003E state.Select(item =\u003E item.Id == id ? item with { Done = !item.Done } : item).ToArray(),\r\n DeleteItem { Id: var id }\r\n =\u003E state.Where(item =\u003E item.Id != id).ToArray(),\r\n _ =\u003E state\r\n };\r\n\r\n public override Element Render()\r\n {\r\n var (draft, setDraft) = UseState(\u0022\u0022);\r\n var (items, dispatch) = UseReducer\u003CIReadOnlyList\u003CItem\u003E, TodoAction\u003E(Reduce, Array.Empty\u003CItem\u003E());\r\n\r\n return VStack(12,\r\n Heading(\u0022Todo reducer\u0022),\r\n HStack(8,\r\n TextBox(draft, setDraft, \u0022Add a todo\u0022, header: \u0022Todo\u0022),\r\n Button(\u0022Add\u0022, () =\u003E { dispatch(new AddItem(draft)); setDraft(\u0022\u0022); })),\r\n ListView(items, item =\u003E item.Id, (item, _) =\u003E HStack(8,\r\n CheckBox(item.Done, isChecked =\u003E dispatch(new ToggleItem(item.Id)), item.Text),\r\n Button(\u0022Delete\u0022, () =\u003E dispatch(new DeleteItem(item.Id))))));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-reducer-typed", + "category": "hooks", + "title": "Typed reducer actions", + "intent": "use strongly typed action records and pattern matching in a reducer", + "tags": [ + "reducer", + "typed-actions", + "dispatch", + "pattern-matching" + ], + "factoryAnchors": [ + "UseReducer", + "Button", + "VStack" + ], + "notesKey": "UseReducer", + "relatedIds": [ + "use-state-basic" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Typed action records make reducer flows explicit and easy to extend.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseReducerTyped\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n abstract record CounterAction;\r\n record Increment(int Amount) : CounterAction;\r\n record Decrement(int Amount) : CounterAction;\r\n record Reset() : CounterAction;\r\n record SetValue(int Value) : CounterAction;\r\n\r\n static int Reduce(int state, CounterAction action) =\u003E action switch\r\n {\r\n Increment(var amount) =\u003E state \u002B amount,\r\n Decrement(var amount) =\u003E state - amount,\r\n Reset =\u003E 0,\r\n SetValue(var value) =\u003E value,\r\n _ =\u003E state\r\n };\r\n\r\n public override Element Render()\r\n {\r\n var (count, dispatch) = UseReducer\u003Cint, CounterAction\u003E(Reduce, 0);\r\n\r\n return VStack(12,\r\n Heading($\u0022Count: {count}\u0022),\r\n HStack(8,\r\n Button(\u0022\u002B1\u0022, () =\u003E dispatch(new Increment(1))),\r\n Button(\u0022-1\u0022, () =\u003E dispatch(new Decrement(1))),\r\n Button(\u0022Set 10\u0022, () =\u003E dispatch(new SetValue(10))),\r\n Button(\u0022Reset\u0022, () =\u003E dispatch(new Reset()))));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-reducer-typed\r\n// intent: strongly-typed reducer actions with pattern matching\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Typed action records make reducer flows explicit and easy to extend.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseReducerTyped\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n abstract record CounterAction;\r\n record Increment(int Amount) : CounterAction;\r\n record Decrement(int Amount) : CounterAction;\r\n record Reset() : CounterAction;\r\n record SetValue(int Value) : CounterAction;\r\n\r\n static int Reduce(int state, CounterAction action) =\u003E action switch\r\n {\r\n Increment(var amount) =\u003E state \u002B amount,\r\n Decrement(var amount) =\u003E state - amount,\r\n Reset =\u003E 0,\r\n SetValue(var value) =\u003E value,\r\n _ =\u003E state\r\n };\r\n\r\n public override Element Render()\r\n {\r\n var (count, dispatch) = UseReducer\u003Cint, CounterAction\u003E(Reduce, 0);\r\n\r\n return VStack(12,\r\n Heading($\u0022Count: {count}\u0022),\r\n HStack(8,\r\n Button(\u0022\u002B1\u0022, () =\u003E dispatch(new Increment(1))),\r\n Button(\u0022-1\u0022, () =\u003E dispatch(new Decrement(1))),\r\n Button(\u0022Set 10\u0022, () =\u003E dispatch(new SetValue(10))),\r\n Button(\u0022Reset\u0022, () =\u003E dispatch(new Reset()))));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-reducer-with-context", + "category": "hooks", + "title": "Reducer with shared context", + "intent": "combine reducer state and context so nested components can dispatch global actions", + "tags": [ + "reducer", + "context", + "global", + "dispatch", + "pattern" + ], + "factoryAnchors": [ + "UseReducer", + "UseContext", + "Provider" + ], + "notesKey": "UseContext", + "relatedIds": [ + "use-context-basic", + "use-reducer-list" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Provide both state and dispatch so nested components can read and update shared state.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseReducerWithContext\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n record CounterState(int Count);\r\n abstract record CounterAction;\r\n record Increment() : CounterAction;\r\n record Decrement() : CounterAction;\r\n\r\n static readonly Context\u003CCounterState\u003E StateContext = new(new CounterState(0));\r\n static readonly Context\u003CAction\u003CCounterAction\u003E\u003E DispatchContext = new(_ =\u003E { });\r\n\r\n static CounterState Reduce(CounterState state, CounterAction action) =\u003E action switch\r\n {\r\n Increment =\u003E state with { Count = state.Count \u002B 1 },\r\n Decrement =\u003E state with { Count = state.Count - 1 },\r\n _ =\u003E state\r\n };\r\n\r\n public override Element Render()\r\n {\r\n var (state, dispatch) = UseReducer\u003CCounterState, CounterAction\u003E(Reduce, new CounterState(0));\r\n\r\n return VStack(12,\r\n Component\u003CCounterValue\u003E(),\r\n Component\u003CCounterButtons\u003E())\r\n .Provide(StateContext, state)\r\n .Provide(DispatchContext, dispatch);\r\n }\r\n\r\n class CounterValue : Component\r\n {\r\n public override Element Render()\r\n {\r\n var state = UseContext(StateContext);\r\n return Heading($\u0022Global count: {state.Count}\u0022);\r\n }\r\n }\r\n\r\n class CounterButtons : Component\r\n {\r\n public override Element Render()\r\n {\r\n var dispatch = UseContext(DispatchContext);\r\n return HStack(8,\r\n Button(\u0022-1\u0022, () =\u003E dispatch(new Decrement())),\r\n Button(\u0022\u002B1\u0022, () =\u003E dispatch(new Increment())));\r\n }\r\n }\r\n}\r\n", + "rawCode": "// id: use-reducer-with-context\r\n// intent: combine reducer state and context to model global app state\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Provide both state and dispatch so nested components can read and update shared state.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseReducerWithContext\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n record CounterState(int Count);\r\n abstract record CounterAction;\r\n record Increment() : CounterAction;\r\n record Decrement() : CounterAction;\r\n\r\n static readonly Context\u003CCounterState\u003E StateContext = new(new CounterState(0));\r\n static readonly Context\u003CAction\u003CCounterAction\u003E\u003E DispatchContext = new(_ =\u003E { });\r\n\r\n static CounterState Reduce(CounterState state, CounterAction action) =\u003E action switch\r\n {\r\n Increment =\u003E state with { Count = state.Count \u002B 1 },\r\n Decrement =\u003E state with { Count = state.Count - 1 },\r\n _ =\u003E state\r\n };\r\n\r\n public override Element Render()\r\n {\r\n var (state, dispatch) = UseReducer\u003CCounterState, CounterAction\u003E(Reduce, new CounterState(0));\r\n\r\n return VStack(12,\r\n Component\u003CCounterValue\u003E(),\r\n Component\u003CCounterButtons\u003E())\r\n .Provide(StateContext, state)\r\n .Provide(DispatchContext, dispatch);\r\n }\r\n\r\n class CounterValue : Component\r\n {\r\n public override Element Render()\r\n {\r\n var state = UseContext(StateContext);\r\n return Heading($\u0022Global count: {state.Count}\u0022);\r\n }\r\n }\r\n\r\n class CounterButtons : Component\r\n {\r\n public override Element Render()\r\n {\r\n var dispatch = UseContext(DispatchContext);\r\n return HStack(8,\r\n Button(\u0022-1\u0022, () =\u003E dispatch(new Decrement())),\r\n Button(\u0022\u002B1\u0022, () =\u003E dispatch(new Increment())));\r\n }\r\n }\r\n}\r\n" + }, + { + "id": "use-ref-dom", + "category": "hooks", + "title": "Native element ref", + "intent": "hand a mounted element reference to native APIs for focus and measurement", + "tags": [ + "ref", + "dom", + "element", + "focus", + "native" + ], + "factoryAnchors": [ + "UseRef", + "UseEffect", + "TextBox" + ], + "notesKey": "UseRef", + "relatedIds": [ + "use-ref-mutable" + ], + "priority": "P0", + "code": "using System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Reactor.Hooks;\r\nusing Microsoft.UI.Xaml;\r\nusing Microsoft.UI.Xaml.Controls;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Pair a stable ref with a mount effect when native APIs need the real control instance.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseRefDom\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (text, setText) = UseState(\u0022\u0022);\r\n var (message, setMessage) = UseState(\u0022Waiting for mount...\u0022);\r\n var focusAttempts = UseRef(0);\r\n var inputRef = this.UseElementRef\u003CTextBox\u003E();\r\n\r\n UseEffect(() =\u003E\r\n {\r\n focusAttempts.Current\u002B\u002B;\r\n if (inputRef.Current is { } box)\r\n {\r\n box.Focus(FocusState.Programmatic);\r\n setMessage($\u0022Focus requested. ActualWidth = {box.ActualWidth:F0}px\u0022);\r\n }\r\n }, Array.Empty\u003Cobject\u003E());\r\n\r\n return VStack(12,\r\n TextBox(text, setText, \u0022Focused on mount\u0022, header: \u0022Focusable input\u0022).Width(240).Ref(inputRef),\r\n Caption(message),\r\n Caption($\u0022Native ref uses: {focusAttempts.Current}\u0022));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-ref-dom\r\n// intent: hand a mounted element reference to native APIs for focus and measurement\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Controls;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Reactor.Hooks;\r\nusing Microsoft.UI.Xaml;\r\nusing Microsoft.UI.Xaml.Controls;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Pair a stable ref with a mount effect when native APIs need the real control instance.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseRefDom\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (text, setText) = UseState(\u0022\u0022);\r\n var (message, setMessage) = UseState(\u0022Waiting for mount...\u0022);\r\n var focusAttempts = UseRef(0);\r\n var inputRef = this.UseElementRef\u003CTextBox\u003E();\r\n\r\n UseEffect(() =\u003E\r\n {\r\n focusAttempts.Current\u002B\u002B;\r\n if (inputRef.Current is { } box)\r\n {\r\n box.Focus(FocusState.Programmatic);\r\n setMessage($\u0022Focus requested. ActualWidth = {box.ActualWidth:F0}px\u0022);\r\n }\r\n }, Array.Empty\u003Cobject\u003E());\r\n\r\n return VStack(12,\r\n TextBox(text, setText, \u0022Focused on mount\u0022, header: \u0022Focusable input\u0022).Width(240).Ref(inputRef),\r\n Caption(message),\r\n Caption($\u0022Native ref uses: {focusAttempts.Current}\u0022));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-ref-mutable", + "category": "hooks", + "title": "Mutable ref storage", + "intent": "store a previous value in a ref so it persists without triggering re-renders", + "tags": [ + "ref", + "mutable", + "previous", + "persist" + ], + "factoryAnchors": [ + "UseRef", + "UseState", + "UseEffect", + "VStack" + ], + "notesKey": "UseRef", + "relatedIds": [ + "use-ref-dom" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Refs keep mutable values across renders without participating in diffing.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseRefMutable\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (count, setCount) = UseState(0);\r\n var previous = UseRef\u003Cint?\u003E(null);\r\n\r\n UseEffect(() =\u003E\r\n {\r\n previous.Current = count;\r\n }, count);\r\n\r\n return VStack(12,\r\n Heading($\u0022Current: {count}\u0022),\r\n TextBlock($\u0022Previous: {(previous.Current is int value ? value : -1)}\u0022),\r\n Button(\u0022\u002B1\u0022, () =\u003E setCount(count \u002B 1)));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-ref-mutable\r\n// intent: store previous state in a mutable ref without causing re-renders\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Refs keep mutable values across renders without participating in diffing.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseRefMutable\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (count, setCount) = UseState(0);\r\n var previous = UseRef\u003Cint?\u003E(null);\r\n\r\n UseEffect(() =\u003E\r\n {\r\n previous.Current = count;\r\n }, count);\r\n\r\n return VStack(12,\r\n Heading($\u0022Current: {count}\u0022),\r\n TextBlock($\u0022Previous: {(previous.Current is int value ? value : -1)}\u0022),\r\n Button(\u0022\u002B1\u0022, () =\u003E setCount(count \u002B 1)));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-state-basic", + "category": "hooks", + "title": "Counter with UseState", + "intent": "increment a primitive value on click", + "tags": [ + "state", + "counter", + "hook", + "useState", + "primitive" + ], + "factoryAnchors": [ + "UseState", + "Button", + "VStack" + ], + "notesKey": "UseState", + "relatedIds": [ + "use-state-list-pitfall", + "use-reducer-list" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022UseStateBasic\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (count, setCount) = UseState(0);\r\n return VStack(\r\n Heading($\u0022Count: {count}\u0022),\r\n Button(\u0022\u002B1\u0022, () =\u003E setCount(count \u002B 1)));\r\n }\r\n}\r\n", + "rawCode": "// id: use-state-basic\r\n// intent: count clicks; demonstrate UseState with primitive value\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022UseStateBasic\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (count, setCount) = UseState(0);\r\n return VStack(\r\n Heading($\u0022Count: {count}\u0022),\r\n Button(\u0022\u002B1\u0022, () =\u003E setCount(count \u002B 1)));\r\n }\r\n}\r\n" + }, + { + "id": "use-state-list-pitfall", + "category": "hooks", + "title": "List mutation anti-pattern", + "intent": "demonstrate why mutating a List in place does not trigger a UseState re-render", + "tags": [ + "state", + "list", + "anti-pattern", + "pitfall" + ], + "factoryAnchors": [ + "UseState", + "Button", + "VStack" + ], + "notesKey": "UseState", + "relatedIds": [ + "use-reducer-list" + ], + "priority": "P0", + "code": "using System.Collections.Generic;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Mutating the same List\u003CT\u003E instance changes data but not state identity, so no re-render happens.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseStateListPitfall\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (items, _) = UseState(new List\u003Cstring\u003E { \u0022Alpha\u0022 });\r\n var (rerenders, setRerenders) = UseState(0);\r\n\r\n return VStack(12,\r\n Heading($\u0022Visible count: {items.Count}\u0022),\r\n Caption($\u0022Unrelated renders: {rerenders}\u0022),\r\n Button(\u0022Mutate list in place (wrong)\u0022, () =\u003E\r\n {\r\n // Wrong: UseState still holds the same List\u003CT\u003E reference, so Reactor\r\n // does not detect a new state value and the UI stays stale.\r\n items.Add($\u0022Item {items.Count \u002B 1}\u0022);\r\n }),\r\n Button(\u0022Force unrelated re-render\u0022, () =\u003E setRerenders(rerenders \u002B 1)),\r\n ForEach(items, item =\u003E TextBlock(item)));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-state-list-pitfall\r\n// intent: show why mutating List\u003CT\u003E in place is a UseState anti-pattern\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System.Collections.Generic;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Mutating the same List\u003CT\u003E instance changes data but not state identity, so no re-render happens.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseStateListPitfall\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (items, _) = UseState(new List\u003Cstring\u003E { \u0022Alpha\u0022 });\r\n var (rerenders, setRerenders) = UseState(0);\r\n\r\n return VStack(12,\r\n Heading($\u0022Visible count: {items.Count}\u0022),\r\n Caption($\u0022Unrelated renders: {rerenders}\u0022),\r\n Button(\u0022Mutate list in place (wrong)\u0022, () =\u003E\r\n {\r\n // Wrong: UseState still holds the same List\u003CT\u003E reference, so Reactor\r\n // does not detect a new state value and the UI stays stale.\r\n items.Add($\u0022Item {items.Count \u002B 1}\u0022);\r\n }),\r\n Button(\u0022Force unrelated re-render\u0022, () =\u003E setRerenders(rerenders \u002B 1)),\r\n ForEach(items, item =\u003E TextBlock(item)));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "use-state-record", + "category": "hooks", + "title": "Record state with UseState", + "intent": "update immutable record state with structural equality and with expressions", + "tags": [ + "state", + "record", + "immutable", + "with-expression" + ], + "factoryAnchors": [ + "UseState", + "ToggleSwitch", + "Slider" + ], + "notesKey": "UseState", + "relatedIds": [ + "use-state-basic" + ], + "priority": "P0", + "code": "using System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Immutable record state stays easy to compare and update with \u0060with\u0060 expressions.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseStateRecord\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n record Settings(bool DarkMode, int FontSize);\r\n\r\n public override Element Render()\r\n {\r\n var (settings, setSettings) = UseState(new Settings(false, 14));\r\n\r\n return VStack(12,\r\n Heading(settings.DarkMode ? \u0022Dark mode\u0022 : \u0022Light mode\u0022),\r\n ToggleSwitch(\r\n settings.DarkMode,\r\n isOn =\u003E setSettings(settings with { DarkMode = isOn }),\r\n onContent: \u0022On\u0022,\r\n offContent: \u0022Off\u0022,\r\n header: \u0022Theme\u0022),\r\n TextBlock($\u0022Font size: {settings.FontSize}\u0022),\r\n Slider(\r\n settings.FontSize,\r\n min: 12,\r\n max: 24,\r\n onValueChanged: value =\u003E setSettings(settings with { FontSize = (int)Math.Round(value) })),\r\n TextBlock(\u0022Preview text\u0022).FontSize(settings.FontSize));\r\n }\r\n}\r\n\r\n", + "rawCode": "// id: use-state-record\r\n// intent: record-shaped state with structural equality and with-expression updates\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\n// Immutable record state stays easy to compare and update with \u0060with\u0060 expressions.\r\nReactorApp.Run\u003CApp\u003E(\u0022UseStateRecord\u0022, width: 400, height: 200);\r\n\r\nclass App : Component\r\n{\r\n record Settings(bool DarkMode, int FontSize);\r\n\r\n public override Element Render()\r\n {\r\n var (settings, setSettings) = UseState(new Settings(false, 14));\r\n\r\n return VStack(12,\r\n Heading(settings.DarkMode ? \u0022Dark mode\u0022 : \u0022Light mode\u0022),\r\n ToggleSwitch(\r\n settings.DarkMode,\r\n isOn =\u003E setSettings(settings with { DarkMode = isOn }),\r\n onContent: \u0022On\u0022,\r\n offContent: \u0022Off\u0022,\r\n header: \u0022Theme\u0022),\r\n TextBlock($\u0022Font size: {settings.FontSize}\u0022),\r\n Slider(\r\n settings.FontSize,\r\n min: 12,\r\n max: 24,\r\n onValueChanged: value =\u003E setSettings(settings with { FontSize = (int)Math.Round(value) })),\r\n TextBlock(\u0022Preview text\u0022).FontSize(settings.FontSize));\r\n }\r\n}\r\n\r\n" + }, + { + "id": "autosuggestbox-typeahead", + "category": "inputs", + "title": "AutoSuggestBox Typeahead", + "intent": "search box with typeahead suggestions filtered from user input", + "tags": [ + "autosuggest", + "typeahead", + "search", + "filter" + ], + "factoryAnchors": [ + "AutoSuggestBox", + "UseState" + ], + "notesKey": "AutoSuggestBox", + "relatedIds": [], + "priority": "P0", + "code": "using System;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022AutoSuggestBox Typeahead\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var allItems = new[] { \u0022Apple\u0022, \u0022Apricot\u0022, \u0022Banana\u0022, \u0022Blueberry\u0022, \u0022Cherry\u0022, \u0022Grape\u0022 };\r\n var (query, setQuery) = UseState(\u0022\u0022);\r\n var matches = allItems.Where(x =\u003E x.Contains(query, StringComparison.OrdinalIgnoreCase)).Take(5).ToArray();\r\n return VStack(12,\r\n Heading(\u0022AutoSuggestBox\u0022),\r\n (AutoSuggestBox(query, setQuery) with { Header = \u0022Fruit search\u0022, PlaceholderText = \u0022Start typing\u0022, Suggestions = matches, IsSuggestionListOpen = query.Length \u003E 0 \u0026\u0026 matches.Length \u003E 0 })\r\n .AutomationName(\u0022Fruit search\u0022),\r\n TextBlock($\u0022Current value: {query}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: autosuggestbox-typeahead\r\n// intent: search box with typeahead suggestions\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022AutoSuggestBox Typeahead\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var allItems = new[] { \u0022Apple\u0022, \u0022Apricot\u0022, \u0022Banana\u0022, \u0022Blueberry\u0022, \u0022Cherry\u0022, \u0022Grape\u0022 };\r\n var (query, setQuery) = UseState(\u0022\u0022);\r\n var matches = allItems.Where(x =\u003E x.Contains(query, StringComparison.OrdinalIgnoreCase)).Take(5).ToArray();\r\n return VStack(12,\r\n Heading(\u0022AutoSuggestBox\u0022),\r\n (AutoSuggestBox(query, setQuery) with { Header = \u0022Fruit search\u0022, PlaceholderText = \u0022Start typing\u0022, Suggestions = matches, IsSuggestionListOpen = query.Length \u003E 0 \u0026\u0026 matches.Length \u003E 0 })\r\n .AutomationName(\u0022Fruit search\u0022),\r\n TextBlock($\u0022Current value: {query}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "calendar-multiselect", + "category": "inputs", + "title": "Calendar Multi-select", + "intent": "select multiple dates in a CalendarView and summarize the chosen days", + "tags": [ + "calendar", + "multi-select", + "dates", + "selection" + ], + "factoryAnchors": [ + "CalendarView", + "UseState" + ], + "notesKey": "CalendarView", + "relatedIds": [ + "calendardatepicker" + ], + "priority": "P0", + "code": "using System;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Xaml.Controls;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Calendar Multi-select\u0022, width: 520, height: 420);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (dates, setDates) = UseState\u003CIReadOnlyList\u003CDateTimeOffset\u003E\u003E(Array.Empty\u003CDateTimeOffset\u003E());\r\n\r\n return VStack(16,\r\n Subtitle(\u0022Pick travel days\u0022),\r\n (CalendarView() with { SelectionMode = CalendarViewSelectionMode.Multiple })\r\n .SelectedDates(dates)\r\n .SelectedDatesChanged(setDates),\r\n Body(dates.Count == 0\r\n ? \u0022No dates selected.\u0022\r\n : $\u0022{dates.Count} selected: {string.Join(\u0022, \u0022, dates.Select(d =\u003E d.ToString(\u0022MMM d\u0022)))}\u0022),\r\n Button(\u0022Clear\u0022, () =\u003E setDates(Array.Empty\u003CDateTimeOffset\u003E()))\r\n .SubtleButton())\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: calendar-multiselect\r\n// intent: select multiple dates in a calendar and summarize the chosen days\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing Microsoft.UI.Xaml.Controls;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Calendar Multi-select\u0022, width: 520, height: 420);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (dates, setDates) = UseState\u003CIReadOnlyList\u003CDateTimeOffset\u003E\u003E(Array.Empty\u003CDateTimeOffset\u003E());\r\n\r\n return VStack(16,\r\n Subtitle(\u0022Pick travel days\u0022),\r\n (CalendarView() with { SelectionMode = CalendarViewSelectionMode.Multiple })\r\n .SelectedDates(dates)\r\n .SelectedDatesChanged(setDates),\r\n Body(dates.Count == 0\r\n ? \u0022No dates selected.\u0022\r\n : $\u0022{dates.Count} selected: {string.Join(\u0022, \u0022, dates.Select(d =\u003E d.ToString(\u0022MMM d\u0022)))}\u0022),\r\n Button(\u0022Clear\u0022, () =\u003E setDates(Array.Empty\u003CDateTimeOffset\u003E()))\r\n .SubtleButton())\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "calendardatepicker", + "category": "inputs", + "title": "CalendarDatePicker Selection", + "intent": "date selection via calendar popup with current date display", + "tags": [ + "calendar", + "date", + "picker", + "datepicker" + ], + "factoryAnchors": [ + "CalendarDatePicker", + "UseState" + ], + "notesKey": "CalendarDatePicker", + "relatedIds": [], + "priority": "P0", + "code": "using System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022CalendarDatePicker\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (date, setDate) = UseState\u003CDateTimeOffset?\u003E(DateTimeOffset.Now);\r\n return VStack(12,\r\n Heading(\u0022CalendarDatePicker\u0022),\r\n (CalendarDatePicker(date, setDate) with { Header = \u0022Start date\u0022, DateFormat = \u0022{month.full} {day.integer}, {year.full}\u0022 }),\r\n TextBlock($\u0022Selected: {(date is null ? \u0022None\u0022 : date.Value.ToString(\u0022D\u0022))}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: calendardatepicker\r\n// intent: date selection via calendar popup\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022CalendarDatePicker\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (date, setDate) = UseState\u003CDateTimeOffset?\u003E(DateTimeOffset.Now);\r\n return VStack(12,\r\n Heading(\u0022CalendarDatePicker\u0022),\r\n (CalendarDatePicker(date, setDate) with { Header = \u0022Start date\u0022, DateFormat = \u0022{month.full} {day.integer}, {year.full}\u0022 }),\r\n TextBlock($\u0022Selected: {(date is null ? \u0022None\u0022 : date.Value.ToString(\u0022D\u0022))}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "checkbox-bool", + "category": "inputs", + "title": "Checkbox Boolean State", + "intent": "checkbox bound to boolean state with text feedback", + "tags": [ + "checkbox", + "boolean", + "toggle" + ], + "factoryAnchors": [ + "CheckBox", + "UseState" + ], + "notesKey": "CheckBox", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Checkbox Boolean\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (accepted, setAccepted) = UseState(false);\r\n return VStack(12,\r\n Heading(\u0022CheckBox\u0022),\r\n CheckBox(accepted, setAccepted, \u0022Accept terms\u0022),\r\n TextBlock($\u0022Accepted: {accepted}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: checkbox-bool\r\n// intent: checkbox bound to boolean state\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Checkbox Boolean\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (accepted, setAccepted) = UseState(false);\r\n return VStack(12,\r\n Heading(\u0022CheckBox\u0022),\r\n CheckBox(accepted, setAccepted, \u0022Accept terms\u0022),\r\n TextBlock($\u0022Accepted: {accepted}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "combobox-from-list", + "category": "inputs", + "title": "ComboBox from String List", + "intent": "dropdown picker from a string array with selected item display", + "tags": [ + "combobox", + "dropdown", + "select", + "picker" + ], + "factoryAnchors": [ + "ComboBox", + "UseState" + ], + "notesKey": "ComboBox", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ComboBox List\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var cities = new[] { \u0022Seattle\u0022, \u0022London\u0022, \u0022Tokyo\u0022, \u0022Sydney\u0022 };\r\n var (selectedIndex, setSelectedIndex) = UseState(1);\r\n return VStack(12,\r\n Heading(\u0022ComboBox\u0022),\r\n (ComboBox(cities, selectedIndex, setSelectedIndex) with { Header = \u0022Office\u0022, PlaceholderText = \u0022Choose a city\u0022 }),\r\n TextBlock($\u0022Selected: {cities[selectedIndex]}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: combobox-from-list\r\n// intent: dropdown picker from a string array\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ComboBox List\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var cities = new[] { \u0022Seattle\u0022, \u0022London\u0022, \u0022Tokyo\u0022, \u0022Sydney\u0022 };\r\n var (selectedIndex, setSelectedIndex) = UseState(1);\r\n return VStack(12,\r\n Heading(\u0022ComboBox\u0022),\r\n (ComboBox(cities, selectedIndex, setSelectedIndex) with { Header = \u0022Office\u0022, PlaceholderText = \u0022Choose a city\u0022 }),\r\n TextBlock($\u0022Selected: {cities[selectedIndex]}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "combobox-of-elements", + "category": "inputs", + "title": "ComboBox of Elements", + "intent": "dropdown with custom element items and selected value display", + "tags": [ + "combobox", + "dropdown", + "elements", + "custom" + ], + "factoryAnchors": [ + "ComboBox", + "UseState" + ], + "notesKey": "ComboBox", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ComboBox Elements\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var labels = new[] { \u0022Low\u0022, \u0022Medium\u0022, \u0022High\u0022 };\r\n var items = new Element[] { TextBlock(\u0022\uD83D\uDFE2 Low\u0022), TextBlock(\u0022\uD83D\uDFE1 Medium\u0022), TextBlock(\u0022\uD83D\uDD34 High\u0022) };\r\n var (selectedIndex, setSelectedIndex) = UseState(0);\r\n return VStack(12,\r\n Heading(\u0022ComboBox with Elements\u0022),\r\n (ComboBox(items, selectedIndex, setSelectedIndex) with { Header = \u0022Priority\u0022 }),\r\n TextBlock($\u0022Selected: {labels[selectedIndex]}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: combobox-of-elements\r\n// intent: dropdown with custom element items\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ComboBox Elements\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var labels = new[] { \u0022Low\u0022, \u0022Medium\u0022, \u0022High\u0022 };\r\n var items = new Element[] { TextBlock(\u0022\uD83D\uDFE2 Low\u0022), TextBlock(\u0022\uD83D\uDFE1 Medium\u0022), TextBlock(\u0022\uD83D\uDD34 High\u0022) };\r\n var (selectedIndex, setSelectedIndex) = UseState(0);\r\n return VStack(12,\r\n Heading(\u0022ComboBox with Elements\u0022),\r\n (ComboBox(items, selectedIndex, setSelectedIndex) with { Header = \u0022Priority\u0022 }),\r\n TextBlock($\u0022Selected: {labels[selectedIndex]}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "numberbox-validated", + "category": "inputs", + "title": "NumberBox Validation", + "intent": "number input with validation constraints and current value display", + "tags": [ + "numberbox", + "number", + "input", + "validation" + ], + "factoryAnchors": [ + "NumberBox", + "UseState" + ], + "notesKey": "NumberBox", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022NumberBox Validation\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (age, setAge) = UseState(21.0);\r\n return VStack(12,\r\n Heading(\u0022NumberBox\u0022),\r\n (NumberBox(age, setAge) with { Header = \u0022Age\u0022, Minimum = 0, Maximum = 120, Description = \u0022Allowed range: 0 to 120\u0022 })\r\n .AutomationName(\u0022Age\u0022),\r\n TextBlock($\u0022Current value: {age:0}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: numberbox-validated\r\n// intent: number input with validation constraints\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022NumberBox Validation\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (age, setAge) = UseState(21.0);\r\n return VStack(12,\r\n Heading(\u0022NumberBox\u0022),\r\n (NumberBox(age, setAge) with { Header = \u0022Age\u0022, Minimum = 0, Maximum = 120, Description = \u0022Allowed range: 0 to 120\u0022 })\r\n .AutomationName(\u0022Age\u0022),\r\n TextBlock($\u0022Current value: {age:0}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "radiobuttons-group", + "category": "inputs", + "title": "RadioButtons Group Selection", + "intent": "radio button group for single selection with selected index tracking", + "tags": [ + "radio", + "radiobuttons", + "selection", + "group" + ], + "factoryAnchors": [ + "RadioButtons", + "UseState" + ], + "notesKey": "RadioButtons", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022RadioButtons Group\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var items = new[] { \u0022System\u0022, \u0022Light\u0022, \u0022Dark\u0022 };\r\n var (selectedIndex, setSelectedIndex) = UseState(0);\r\n return VStack(12,\r\n Heading(\u0022RadioButtons\u0022),\r\n RadioButtons(items, selectedIndex, setSelectedIndex) with { Header = \u0022Theme\u0022 },\r\n TextBlock($\u0022Selected: {items[selectedIndex]}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: radiobuttons-group\r\n// intent: radio button group for single selection\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022RadioButtons Group\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var items = new[] { \u0022System\u0022, \u0022Light\u0022, \u0022Dark\u0022 };\r\n var (selectedIndex, setSelectedIndex) = UseState(0);\r\n return VStack(12,\r\n Heading(\u0022RadioButtons\u0022),\r\n RadioButtons(items, selectedIndex, setSelectedIndex) with { Header = \u0022Theme\u0022 },\r\n TextBlock($\u0022Selected: {items[selectedIndex]}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "slider-range", + "category": "inputs", + "title": "Slider Range Selection", + "intent": "slider for numeric range selection with live value display", + "tags": [ + "slider", + "range", + "numeric" + ], + "factoryAnchors": [ + "Slider", + "UseState" + ], + "notesKey": "Slider", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Slider Range\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (volume, setVolume) = UseState(35.0);\r\n return VStack(12,\r\n Heading(\u0022Slider\u0022),\r\n (Slider(volume, 0, 100, setVolume) with { Header = \u0022Volume\u0022, StepFrequency = 5, TickFrequency = 10 }),\r\n TextBlock($\u0022Current value: {volume:0}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: slider-range\r\n// intent: slider for numeric range selection\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Slider Range\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (volume, setVolume) = UseState(35.0);\r\n return VStack(12,\r\n Heading(\u0022Slider\u0022),\r\n (Slider(volume, 0, 100, setVolume) with { Header = \u0022Volume\u0022, StepFrequency = 5, TickFrequency = 10 }),\r\n TextBlock($\u0022Current value: {volume:0}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "textfield-twoway", + "category": "inputs", + "title": "TextField Two-Way Binding", + "intent": "two-way text input binding with UseState and immediate display updates", + "tags": [ + "textfield", + "input", + "twoway", + "binding", + "text" + ], + "factoryAnchors": [ + "TextField", + "UseState" + ], + "notesKey": "TextField", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022TextField Two-Way\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (name, setName) = UseState(\u0022Reactor\u0022);\r\n return VStack(12,\r\n Heading(\u0022TextField\u0022),\r\n (TextField(name, setName, placeholder: \u0022Type a name\u0022) with { Header = \u0022Display name\u0022, MaxLength = 24 }),\r\n TextBlock($\u0022Current value: {name}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: textfield-twoway\r\n// intent: two-way text input binding\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022TextField Two-Way\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (name, setName) = UseState(\u0022Reactor\u0022);\r\n return VStack(12,\r\n Heading(\u0022TextField\u0022),\r\n (TextField(name, setName, placeholder: \u0022Type a name\u0022) with { Header = \u0022Display name\u0022, MaxLength = 24 }),\r\n TextBlock($\u0022Current value: {name}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "toggleswitch", + "category": "inputs", + "title": "ToggleSwitch Settings Toggle", + "intent": "toggle switch for on off settings state", + "tags": [ + "toggle", + "switch", + "setting", + "onoff" + ], + "factoryAnchors": [ + "ToggleSwitch", + "UseState" + ], + "notesKey": "ToggleSwitch", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Toggle Switch\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (wifiEnabled, setWifiEnabled) = UseState(true);\r\n return VStack(12,\r\n Heading(\u0022ToggleSwitch\u0022),\r\n (ToggleSwitch(wifiEnabled, setWifiEnabled, \u0022On\u0022, \u0022Off\u0022) with { Header = \u0022Wi-Fi\u0022 }),\r\n TextBlock($\u0022Wi-Fi is {(wifiEnabled ? \u0022On\u0022 : \u0022Off\u0022)}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n", + "rawCode": "// id: toggleswitch\r\n// intent: toggle switch for on/off settings\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Toggle Switch\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (wifiEnabled, setWifiEnabled) = UseState(true);\r\n return VStack(12,\r\n Heading(\u0022ToggleSwitch\u0022),\r\n (ToggleSwitch(wifiEnabled, setWifiEnabled, \u0022On\u0022, \u0022Off\u0022) with { Header = \u0022Wi-Fi\u0022 }),\r\n TextBlock($\u0022Wi-Fi is {(wifiEnabled ? \u0022On\u0022 : \u0022Off\u0022)}\u0022))\r\n .Margin(16);\r\n }\r\n}\r\n" + }, + { + "id": "border-with-corner", + "category": "layout", + "title": "Border With Corner Radius", + "intent": "bordered content area with padding, rounded corners, and theme-aware surface colors", + "tags": [ + "border", + "padding", + "corner-radius", + "themed" + ], + "factoryAnchors": [ + "Border" + ], + "notesKey": "Border", + "relatedIds": [ + "card-surface" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Border With Corner\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n Border(\r\n VStack(8,\r\n Subtitle(\u0022Pinned note\u0022),\r\n TextBlock(\u0022Border, padding, and corner radius create a clear content container.\u0022)\r\n .Foreground(Theme.SecondaryText),\r\n Caption(\u0022Updated just now\u0022).Foreground(Theme.TertiaryText))\r\n .Padding(16))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(14)\r\n .Padding(4)\r\n )\r\n .Padding(24)\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: border-with-corner\r\n// intent: padded border with corner radius\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Border With Corner\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n Border(\r\n VStack(8,\r\n Subtitle(\u0022Pinned note\u0022),\r\n TextBlock(\u0022Border, padding, and corner radius create a clear content container.\u0022)\r\n .Foreground(Theme.SecondaryText),\r\n Caption(\u0022Updated just now\u0022).Foreground(Theme.TertiaryText))\r\n .Padding(16))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(14)\r\n .Padding(4)\r\n )\r\n .Padding(24)\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "canvas-positioning", + "category": "layout", + "title": "Canvas Positioning", + "intent": "absolute layout with positioned elements, connector lines, and centered shapes", + "tags": [ + "canvas", + "absolute", + "position", + "shapes" + ], + "factoryAnchors": [ + "Canvas" + ], + "notesKey": "Canvas", + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Canvas Positioning\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var accent = ThemeResource.Brush(\u0022AccentFillColorDefaultBrush\u0022);\r\n var stroke = ThemeResource.Brush(\u0022CardStrokeColorDefaultBrush\u0022);\r\n\r\n return Border(\r\n Canvas(\r\n Line(72, 48, 180, 108).Stroke(stroke).StrokeThickness(2),\r\n Border(TextBlock(\u0022A\u0022).Padding(8))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8)\r\n .Canvas(left: 48, top: 32),\r\n Ellipse().Width(24).Height(24).Fill(accent).CenterAt(x: 180, y: 108),\r\n Line(192, 120, 280, 160).Stroke(accent).StrokeThickness(2),\r\n Border(TextBlock(\u0022Focus\u0022).Padding(8))\r\n .Background(Theme.LayerFill)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8)\r\n .Canvas(left: 280, top: 160))\r\n .Width(340)\r\n .Height(220)\r\n )\r\n .Padding(20)\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: canvas-positioning\r\n// intent: absolute positioning with Canvas and .Canvas(left, top)\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Canvas Positioning\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var accent = ThemeResource.Brush(\u0022AccentFillColorDefaultBrush\u0022);\r\n var stroke = ThemeResource.Brush(\u0022CardStrokeColorDefaultBrush\u0022);\r\n\r\n return Border(\r\n Canvas(\r\n Line(72, 48, 180, 108).Stroke(stroke).StrokeThickness(2),\r\n Border(TextBlock(\u0022A\u0022).Padding(8))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8)\r\n .Canvas(left: 48, top: 32),\r\n Ellipse().Width(24).Height(24).Fill(accent).CenterAt(x: 180, y: 108),\r\n Line(192, 120, 280, 160).Stroke(accent).StrokeThickness(2),\r\n Border(TextBlock(\u0022Focus\u0022).Padding(8))\r\n .Background(Theme.LayerFill)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8)\r\n .Canvas(left: 280, top: 160))\r\n .Width(340)\r\n .Height(220)\r\n )\r\n .Padding(20)\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "card-surface", + "category": "layout", + "title": "Card Surface", + "intent": "theme-aware card surface with title, body copy, and an action button", + "tags": [ + "card", + "theme", + "surface", + "win11", + "design" + ], + "factoryAnchors": [ + "Card", + "Border" + ], + "notesKey": "Card", + "relatedIds": [ + "border-with-corner" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Core.Theme;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Card Surface\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n Card(\r\n VStack(12,\r\n Subtitle(\u0022Sprint planning\u0022),\r\n TextBlock(\u0022Card() applies the canonical WinUI surface, stroke, radius, and padding.\u0022)\r\n .Foreground(SecondaryText),\r\n Button(\u0022Open board\u0022)))\r\n .Width(280)\r\n )\r\n .Padding(24)\r\n .Background(SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: card-surface\r\n// intent: themed card surface following Win11 design\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Core.Theme;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Card Surface\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n Card(\r\n VStack(12,\r\n Subtitle(\u0022Sprint planning\u0022),\r\n TextBlock(\u0022Card() applies the canonical WinUI surface, stroke, radius, and padding.\u0022)\r\n .Foreground(SecondaryText),\r\n Button(\u0022Open board\u0022)))\r\n .Width(280)\r\n )\r\n .Padding(24)\r\n .Background(SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "flexcolumn-with-justify", + "category": "layout", + "title": "Flex Column With Justify", + "intent": "full-height column with aligned items spaced across the available height", + "tags": [ + "flex", + "flexcolumn", + "justify", + "align", + "page-layout" + ], + "factoryAnchors": [ + "FlexColumn" + ], + "notesKey": "FlexColumn", + "relatedIds": [ + "flexrow-with-grow" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Layout;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022FlexColumn Justify\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n (FlexColumn(\r\n VStack(4,\r\n Subtitle(\u0022Welcome\u0022),\r\n TextBlock(\u0022Header content stays at the top.\u0022).Foreground(Theme.SecondaryText)),\r\n Border(TextBlock(\u0022Centered content\u0022).Padding(12))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8),\r\n Caption(\u0022Footer status or actions can sit at the bottom.\u0022)\r\n .Foreground(Theme.SecondaryText)) with\r\n {\r\n JustifyContent = FlexJustify.SpaceBetween,\r\n AlignItems = FlexAlign.Center,\r\n })\r\n .Height(220)\r\n .FlexPadding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: flexcolumn-with-justify\r\n// intent: flexbox column with alignment and justification\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Layout;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022FlexColumn Justify\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n (FlexColumn(\r\n VStack(4,\r\n Subtitle(\u0022Welcome\u0022),\r\n TextBlock(\u0022Header content stays at the top.\u0022).Foreground(Theme.SecondaryText)),\r\n Border(TextBlock(\u0022Centered content\u0022).Padding(12))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8),\r\n Caption(\u0022Footer status or actions can sit at the bottom.\u0022)\r\n .Foreground(Theme.SecondaryText)) with\r\n {\r\n JustifyContent = FlexJustify.SpaceBetween,\r\n AlignItems = FlexAlign.Center,\r\n })\r\n .Height(220)\r\n .FlexPadding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "flexrow-with-grow", + "category": "layout", + "title": "Flex Row With Grow", + "intent": "toolbar layout with a growing spacer that pushes actions to the end", + "tags": [ + "flex", + "flexrow", + "grow", + "toolbar", + "spacer" + ], + "factoryAnchors": [ + "FlexRow" + ], + "notesKey": "FlexRow", + "relatedIds": [ + "flexcolumn-with-justify" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Layout;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022FlexRow Grow\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n (FlexRow(\r\n Subtitle(\u0022Files\u0022),\r\n TextBlock(\u0022\u0022).Flex(grow: 1),\r\n Button(\u0022Refresh\u0022),\r\n Button(\u0022Share\u0022)) with\r\n {\r\n AlignItems = FlexAlign.Center,\r\n ColumnGap = 8,\r\n })\r\n .Padding(16)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: flexrow-with-grow\r\n// intent: CSS-flexbox row with one child growing to fill space\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Layout;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022FlexRow Grow\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n (FlexRow(\r\n Subtitle(\u0022Files\u0022),\r\n TextBlock(\u0022\u0022).Flex(grow: 1),\r\n Button(\u0022Refresh\u0022),\r\n Button(\u0022Share\u0022)) with\r\n {\r\n AlignItems = FlexAlign.Center,\r\n ColumnGap = 8,\r\n })\r\n .Padding(16)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "grid-basic", + "category": "layout", + "title": "Basic Grid Layout", + "intent": "two-column grid using auto and star tracks with explicit child placement", + "tags": [ + "grid", + "columns", + "rows", + "sizing", + "placement" + ], + "factoryAnchors": [ + "Grid" + ], + "notesKey": "Grid", + "relatedIds": [ + "grid-spans" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Grid Basic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n (Grid(\r\n new[] { GridSize.Auto, GridSize.Star() },\r\n new[] { GridSize.Auto, GridSize.Auto, GridSize.Star() },\r\n TextBlock(\u0022Name\u0022).Grid(row: 0, column: 0).Foreground(Theme.SecondaryText),\r\n TextField(\u0022Ada Lovelace\u0022, _ =\u003E { }).Grid(row: 0, column: 1),\r\n TextBlock(\u0022Team\u0022).Grid(row: 1, column: 0).Foreground(Theme.SecondaryText),\r\n TextField(\u0022Layout systems\u0022, _ =\u003E { }).Grid(row: 1, column: 1),\r\n Border(TextBlock(\u0022The bottom row expands because the second column and third row use star sizing.\u0022).Padding(12))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8)\r\n .Grid(row: 2, column: 0, columnSpan: 2)) with\r\n {\r\n ColumnSpacing = 12,\r\n RowSpacing = 12,\r\n })\r\n .Padding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: grid-basic\r\n// intent: 2D grid with column and row sizes\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Grid Basic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n (Grid(\r\n new[] { GridSize.Auto, GridSize.Star() },\r\n new[] { GridSize.Auto, GridSize.Auto, GridSize.Star() },\r\n TextBlock(\u0022Name\u0022).Grid(row: 0, column: 0).Foreground(Theme.SecondaryText),\r\n TextField(\u0022Ada Lovelace\u0022, _ =\u003E { }).Grid(row: 0, column: 1),\r\n TextBlock(\u0022Team\u0022).Grid(row: 1, column: 0).Foreground(Theme.SecondaryText),\r\n TextField(\u0022Layout systems\u0022, _ =\u003E { }).Grid(row: 1, column: 1),\r\n Border(TextBlock(\u0022The bottom row expands because the second column and third row use star sizing.\u0022).Padding(12))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8)\r\n .Grid(row: 2, column: 0, columnSpan: 2)) with\r\n {\r\n ColumnSpacing = 12,\r\n RowSpacing = 12,\r\n })\r\n .Padding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "grid-spans", + "category": "layout", + "title": "Grid With Spans", + "intent": "dashboard-style grid that uses both row spans and column spans", + "tags": [ + "grid", + "span", + "columnspan", + "rowspan", + "dashboard" + ], + "factoryAnchors": [ + "Grid" + ], + "notesKey": "Grid", + "relatedIds": [ + "grid-basic" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Grid Spans\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n (Grid(\r\n new[] { GridSize.Star(2), GridSize.Star(), GridSize.Star() },\r\n new[] { GridSize.Auto, GridSize.Star() },\r\n Card(VStack(8,\r\n Subtitle(\u0022Overview\u0022),\r\n TextBlock(\u0022This tile spans two columns across the top.\u0022).Foreground(Theme.SecondaryText)))\r\n .Grid(row: 0, column: 0, columnSpan: 2),\r\n Card(VStack(8,\r\n Subtitle(\u0022Alerts\u0022),\r\n Caption(\u00227 items need attention.\u0022).Foreground(Theme.SecondaryText)))\r\n .Grid(row: 0, column: 2, rowSpan: 2),\r\n Card(TextBlock(\u0022Traffic\u0022)).Grid(row: 1, column: 0),\r\n Card(TextBlock(\u0022Tasks\u0022)).Grid(row: 1, column: 1)) with\r\n {\r\n ColumnSpacing = 12,\r\n RowSpacing = 12,\r\n })\r\n .Padding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: grid-spans\r\n// intent: grid with row and column spans\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Grid Spans\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n (Grid(\r\n new[] { GridSize.Star(2), GridSize.Star(), GridSize.Star() },\r\n new[] { GridSize.Auto, GridSize.Star() },\r\n Card(VStack(8,\r\n Subtitle(\u0022Overview\u0022),\r\n TextBlock(\u0022This tile spans two columns across the top.\u0022).Foreground(Theme.SecondaryText)))\r\n .Grid(row: 0, column: 0, columnSpan: 2),\r\n Card(VStack(8,\r\n Subtitle(\u0022Alerts\u0022),\r\n Caption(\u00227 items need attention.\u0022).Foreground(Theme.SecondaryText)))\r\n .Grid(row: 0, column: 2, rowSpan: 2),\r\n Card(TextBlock(\u0022Traffic\u0022)).Grid(row: 1, column: 0),\r\n Card(TextBlock(\u0022Tasks\u0022)).Grid(row: 1, column: 1)) with\r\n {\r\n ColumnSpacing = 12,\r\n RowSpacing = 12,\r\n })\r\n .Padding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "hstack-basic", + "category": "layout", + "title": "Horizontal Stack Basics", + "intent": "horizontal stack for a label, input, and action button", + "tags": [ + "hstack", + "horizontal", + "stack", + "spacing" + ], + "factoryAnchors": [ + "HStack", + "TextBlock", + "Button" + ], + "notesKey": "HStack", + "relatedIds": [ + "vstack-basic" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022HStack Basic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n HStack(8,\r\n TextBlock(\u0022Name\u0022).Width(48),\r\n TextField(\u0022Ada\u0022, _ =\u003E { }, placeholder: \u0022Display name\u0022).Width(180),\r\n Button(\u0022Save\u0022))\r\n .Padding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: hstack-basic\r\n// intent: horizontal shrink-wrap stack with spacing\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022HStack Basic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n HStack(8,\r\n TextBlock(\u0022Name\u0022).Width(48),\r\n TextField(\u0022Ada\u0022, _ =\u003E { }, placeholder: \u0022Display name\u0022).Width(180),\r\n Button(\u0022Save\u0022))\r\n .Padding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "named-styles", + "category": "layout", + "title": "Named Style Fluents", + "intent": "apply theme-aware named styles to buttons, hyperlinks, and InfoBars", + "tags": [ + "styles", + "theme", + "button", + "hyperlink", + "infobar" + ], + "factoryAnchors": [ + "Button", + "HyperlinkButton", + "InfoBar" + ], + "notesKey": null, + "relatedIds": [ + "hyperlink-button" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Named Styles\u0022, width: 560, height: 520);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render() =\u003E\r\n ScrollView(\r\n VStack(20,\r\n Subtitle(\u0022Buttons\u0022),\r\n HStack(8,\r\n Button(\u0022Default\u0022, () =\u003E { }),\r\n Button(\u0022Accent\u0022, () =\u003E { }).AccentButton(),\r\n Button(\u0022Subtle\u0022, () =\u003E { }).SubtleButton(),\r\n Button(\u0022Text link\u0022, () =\u003E { }).TextLink()),\r\n Subtitle(\u0022Hyperlinks\u0022),\r\n HyperlinkButton(\u0022Open docs\u0022).TextLink(),\r\n Subtitle(\u0022InfoBars\u0022),\r\n InfoBar(\u0022Tip\u0022, \u0022You can drag the divider.\u0022).Informational(),\r\n InfoBar(\u0022Saved\u0022, \u0022Changes written to disk.\u0022).Success(),\r\n InfoBar(\u0022Heads up\u0022, \u0022Unsaved changes will be discarded.\u0022).Warning(),\r\n InfoBar(\u0022Failed\u0022, \u0022Couldn\u0027t reach the server.\u0022).Error())\r\n .Padding(24));\r\n}\r\n", + "rawCode": "// id: named-styles\r\n// intent: apply theme-aware named styles to buttons, hyperlinks, and InfoBars\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Named Styles\u0022, width: 560, height: 520);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render() =\u003E\r\n ScrollView(\r\n VStack(20,\r\n Subtitle(\u0022Buttons\u0022),\r\n HStack(8,\r\n Button(\u0022Default\u0022, () =\u003E { }),\r\n Button(\u0022Accent\u0022, () =\u003E { }).AccentButton(),\r\n Button(\u0022Subtle\u0022, () =\u003E { }).SubtleButton(),\r\n Button(\u0022Text link\u0022, () =\u003E { }).TextLink()),\r\n Subtitle(\u0022Hyperlinks\u0022),\r\n HyperlinkButton(\u0022Open docs\u0022).TextLink(),\r\n Subtitle(\u0022InfoBars\u0022),\r\n InfoBar(\u0022Tip\u0022, \u0022You can drag the divider.\u0022).Informational(),\r\n InfoBar(\u0022Saved\u0022, \u0022Changes written to disk.\u0022).Success(),\r\n InfoBar(\u0022Heads up\u0022, \u0022Unsaved changes will be discarded.\u0022).Warning(),\r\n InfoBar(\u0022Failed\u0022, \u0022Couldn\u0027t reach the server.\u0022).Error())\r\n .Padding(24));\r\n}\r\n" + }, + { + "id": "scrollviewer-vertical", + "category": "layout", + "title": "Vertical ScrollView", + "intent": "scrollable region that wraps a long vertical stack of content cards", + "tags": [ + "scroll", + "scrollview", + "vertical", + "overflow" + ], + "factoryAnchors": [ + "ScrollView", + "VStack" + ], + "notesKey": "ScrollView", + "relatedIds": [ + "vstack-basic" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ScrollView Vertical\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n static Element Row(int index) =\u003E\r\n Border(\r\n HStack(12,\r\n Subtitle($\u0022Item {index}\u0022),\r\n TextBlock(\u0022Scroll to reveal more content.\u0022).Foreground(Theme.SecondaryText))\r\n .Padding(12))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8);\r\n\r\n public override Element Render()\r\n {\r\n return Border(\r\n ScrollView(\r\n VStack(12,\r\n Subtitle(\u0022Recent activity\u0022),\r\n Row(1), Row(2), Row(3), Row(4), Row(5),\r\n Row(6), Row(7), Row(8), Row(9), Row(10))\r\n .Padding(20))\r\n .Height(220)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: scrollviewer-vertical\r\n// intent: scrollable vertical content region\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022ScrollView Vertical\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n static Element Row(int index) =\u003E\r\n Border(\r\n HStack(12,\r\n Subtitle($\u0022Item {index}\u0022),\r\n TextBlock(\u0022Scroll to reveal more content.\u0022).Foreground(Theme.SecondaryText))\r\n .Padding(12))\r\n .Background(Theme.CardBackground)\r\n .WithBorder(Theme.CardStroke, 1)\r\n .CornerRadius(8);\r\n\r\n public override Element Render()\r\n {\r\n return Border(\r\n ScrollView(\r\n VStack(12,\r\n Subtitle(\u0022Recent activity\u0022),\r\n Row(1), Row(2), Row(3), Row(4), Row(5),\r\n Row(6), Row(7), Row(8), Row(9), Row(10))\r\n .Padding(20))\r\n .Height(220)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "vstack-basic", + "category": "layout", + "title": "Vertical Stack Basics", + "intent": "vertical shrink-wrap stack with consistent spacing between actions", + "tags": [ + "vstack", + "vertical", + "stack", + "spacing" + ], + "factoryAnchors": [ + "VStack", + "TextBlock", + "Button" + ], + "notesKey": "VStack", + "relatedIds": [ + "hstack-basic" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022VStack Basic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n VStack(12,\r\n Subtitle(\u0022Daily checklist\u0022),\r\n TextBlock(\u0022Keep related actions grouped in a clean vertical stack.\u0022)\r\n .Foreground(Theme.SecondaryText),\r\n Button(\u0022Review changes\u0022),\r\n Button(\u0022Run validation\u0022),\r\n Button(\u0022Ship update\u0022))\r\n .Padding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n", + "rawCode": "// id: vstack-basic\r\n// intent: vertical shrink-wrap stack with spacing\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022VStack Basic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return Border(\r\n VStack(12,\r\n Subtitle(\u0022Daily checklist\u0022),\r\n TextBlock(\u0022Keep related actions grouped in a clean vertical stack.\u0022)\r\n .Foreground(Theme.SecondaryText),\r\n Button(\u0022Review changes\u0022),\r\n Button(\u0022Run validation\u0022),\r\n Button(\u0022Ship update\u0022))\r\n .Padding(20)\r\n )\r\n .Background(Theme.SolidBackground);\r\n }\r\n}\r\n" + }, + { + "id": "list-add-delete-toggle", + "category": "lists", + "title": "Add, Delete, Toggle Todo List", + "intent": "manage a mutable list with UseReducer and immutable updates", + "tags": [ + "list", + "add", + "delete", + "toggle", + "todo", + "reducer", + "crud" + ], + "factoryAnchors": [ + "ForEach", + "UseReducer" + ], + "notesKey": "lists", + "relatedIds": [ + "list-with-empty-state" + ], + "priority": "P0", + "code": "using System.Collections.Immutable;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Todo List\u0022, width: 500, height: 400);\r\n\r\nrecord TodoItem(string Id, string Text, bool Done);\r\nrecord TodoState(ImmutableList\u003CTodoItem\u003E Items);\r\nabstract record TodoAction;\r\nrecord AddAction(string Text) : TodoAction;\r\nrecord ToggleAction(string Id) : TodoAction;\r\nrecord DeleteAction(string Id) : TodoAction;\r\n\r\nclass App : Component\r\n{\r\n private static TodoState Reduce(TodoState state, TodoAction action) =\u003E action switch\r\n {\r\n AddAction add when !string.IsNullOrWhiteSpace(add.Text)\r\n =\u003E state with { Items = state.Items.Add(new TodoItem(System.Guid.NewGuid().ToString(), add.Text.Trim(), false)) },\r\n ToggleAction toggle\r\n =\u003E state with { Items = state.Items.Select(item =\u003E item.Id == toggle.Id ? item with { Done = !item.Done } : item).ToImmutableList() },\r\n DeleteAction delete\r\n =\u003E state with { Items = state.Items.RemoveAll(item =\u003E item.Id == delete.Id) },\r\n _ =\u003E state,\r\n };\r\n\r\n public override Element Render()\r\n {\r\n var (state, dispatch) = UseReducer\u003CTodoState, TodoAction\u003E(Reduce,\r\n new TodoState(ImmutableList.Create(new TodoItem(\u00221\u0022, \u0022Write docs\u0022, false), new TodoItem(\u00222\u0022, \u0022Ship sample\u0022, true))));\r\n var (draft, setDraft) = UseState(\u0022\u0022);\r\n\r\n return VStack(12,\r\n Heading($\u0022Todo list ({state.Items.Count(item =\u003E item.Done)}/{state.Items.Count})\u0022),\r\n HStack(8,\r\n TextField(draft, setDraft, placeholder: \u0022Add an item\u0022).Width(300),\r\n Button(\u0022Add\u0022, () =\u003E { dispatch(new AddAction(draft)); setDraft(\u0022\u0022); }).IsEnabled(!string.IsNullOrWhiteSpace(draft))),\r\n VStack(8,\r\n ForEach(state.Items, item =\u003E\r\n HStack(8,\r\n CheckBox(item.Done, _ =\u003E dispatch(new ToggleAction(item.Id))),\r\n TextBlock(item.Text).Width(240).Opacity(item.Done ? 0.5 : 1.0),\r\n Button(\u0022Delete\u0022, () =\u003E dispatch(new DeleteAction(item.Id))))\r\n .WithKey(item.Id))))\r\n .Padding(16);\r\n }\r\n}\r\n", + "rawCode": "// id: list-add-delete-toggle\r\n// intent: dynamic list with add, delete, and toggle using UseReducer\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n#:property TargetFramework=net10.0-windows10.0.22621.0\r\n#:property UseWinUI=true\r\n#:property WindowsPackageType=None\r\n\r\nusing System.Collections.Immutable;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Todo List\u0022, width: 500, height: 400);\r\n\r\nrecord TodoItem(string Id, string Text, bool Done);\r\nrecord TodoState(ImmutableList\u003CTodoItem\u003E Items);\r\nabstract record TodoAction;\r\nrecord AddAction(string Text) : TodoAction;\r\nrecord ToggleAction(string Id) : TodoAction;\r\nrecord DeleteAction(string Id) : TodoAction;\r\n\r\nclass App : Component\r\n{\r\n private static TodoState Reduce(TodoState state, TodoAction action) =\u003E action switch\r\n {\r\n AddAction add when !string.IsNullOrWhiteSpace(add.Text)\r\n =\u003E state with { Items = state.Items.Add(new TodoItem(System.Guid.NewGuid().ToString(), add.Text.Trim(), false)) },\r\n ToggleAction toggle\r\n =\u003E state with { Items = state.Items.Select(item =\u003E item.Id == toggle.Id ? item with { Done = !item.Done } : item).ToImmutableList() },\r\n DeleteAction delete\r\n =\u003E state with { Items = state.Items.RemoveAll(item =\u003E item.Id == delete.Id) },\r\n _ =\u003E state,\r\n };\r\n\r\n public override Element Render()\r\n {\r\n var (state, dispatch) = UseReducer\u003CTodoState, TodoAction\u003E(Reduce,\r\n new TodoState(ImmutableList.Create(new TodoItem(\u00221\u0022, \u0022Write docs\u0022, false), new TodoItem(\u00222\u0022, \u0022Ship sample\u0022, true))));\r\n var (draft, setDraft) = UseState(\u0022\u0022);\r\n\r\n return VStack(12,\r\n Heading($\u0022Todo list ({state.Items.Count(item =\u003E item.Done)}/{state.Items.Count})\u0022),\r\n HStack(8,\r\n TextField(draft, setDraft, placeholder: \u0022Add an item\u0022).Width(300),\r\n Button(\u0022Add\u0022, () =\u003E { dispatch(new AddAction(draft)); setDraft(\u0022\u0022); }).IsEnabled(!string.IsNullOrWhiteSpace(draft))),\r\n VStack(8,\r\n ForEach(state.Items, item =\u003E\r\n HStack(8,\r\n CheckBox(item.Done, _ =\u003E dispatch(new ToggleAction(item.Id))),\r\n TextBlock(item.Text).Width(240).Opacity(item.Done ? 0.5 : 1.0),\r\n Button(\u0022Delete\u0022, () =\u003E dispatch(new DeleteAction(item.Id))))\r\n .WithKey(item.Id))))\r\n .Padding(16);\r\n }\r\n}\r\n" + }, + { + "id": "list-basic-foreach", + "category": "lists", + "title": "Basic ForEach", + "intent": "render a static list using ForEach with stable keys", + "tags": [ + "list", + "foreach", + "render", + "items", + "collection" + ], + "factoryAnchors": [ + "ForEach" + ], + "notesKey": "ForEach", + "relatedIds": [ + "list-add-delete-toggle", + "virtualized-large-list" + ], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Basic ForEach\u0022, width: 500, height: 400);\r\n\r\nrecord GroceryItem(string Id, string Name, string Aisle);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var items = new[]\r\n {\r\n new GroceryItem(\u0022fruit\u0022, \u0022Apples\u0022, \u0022Produce\u0022),\r\n new GroceryItem(\u0022bread\u0022, \u0022Sourdough\u0022, \u0022Bakery\u0022),\r\n new GroceryItem(\u0022milk\u0022, \u0022Whole Milk\u0022, \u0022Dairy\u0022),\r\n };\r\n\r\n return VStack(12,\r\n Heading(\u0022Static shopping list\u0022),\r\n TextBlock(\u0022ForEach maps fixed data into UI rows.\u0022),\r\n VStack(8,\r\n ForEach(items, item =\u003E\r\n HStack(12,\r\n TextBlock(item.Name).Bold().Width(160),\r\n TextBlock(item.Aisle).Opacity(0.7))\r\n .Padding(8)\r\n .WithKey(item.Id))))\r\n .Padding(16);\r\n }\r\n}\r\n", + "rawCode": "// id: list-basic-foreach\r\n// intent: render a static list using ForEach\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n#:property TargetFramework=net10.0-windows10.0.22621.0\r\n#:property UseWinUI=true\r\n#:property WindowsPackageType=None\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Basic ForEach\u0022, width: 500, height: 400);\r\n\r\nrecord GroceryItem(string Id, string Name, string Aisle);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var items = new[]\r\n {\r\n new GroceryItem(\u0022fruit\u0022, \u0022Apples\u0022, \u0022Produce\u0022),\r\n new GroceryItem(\u0022bread\u0022, \u0022Sourdough\u0022, \u0022Bakery\u0022),\r\n new GroceryItem(\u0022milk\u0022, \u0022Whole Milk\u0022, \u0022Dairy\u0022),\r\n };\r\n\r\n return VStack(12,\r\n Heading(\u0022Static shopping list\u0022),\r\n TextBlock(\u0022ForEach maps fixed data into UI rows.\u0022),\r\n VStack(8,\r\n ForEach(items, item =\u003E\r\n HStack(12,\r\n TextBlock(item.Name).Bold().Width(160),\r\n TextBlock(item.Aisle).Opacity(0.7))\r\n .Padding(8)\r\n .WithKey(item.Id))))\r\n .Padding(16);\r\n }\r\n}\r\n" + }, + { + "id": "list-with-empty-state", + "category": "lists", + "title": "List With Empty State", + "intent": "show a placeholder when a list has no items and render rows when data exists", + "tags": [ + "list", + "empty", + "placeholder", + "conditional" + ], + "factoryAnchors": [ + "ForEach" + ], + "notesKey": "lists", + "relatedIds": [ + "list-add-delete-toggle" + ], + "priority": "P0", + "code": "using System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Empty State\u0022, width: 500, height: 400);\r\n\r\nrecord Favorite(string Id, string Label);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (showItems, setShowItems) = UseState(false);\r\n var items = showItems\r\n ? new[] { new Favorite(\u0022one\u0022, \u0022Design notes\u0022), new Favorite(\u0022two\u0022, \u0022Release checklist\u0022) }\r\n : Array.Empty\u003CFavorite\u003E();\r\n\r\n return VStack(12,\r\n Heading(\u0022Favorites\u0022),\r\n Button(showItems ? \u0022Clear items\u0022 : \u0022Load sample items\u0022, () =\u003E setShowItems(!showItems)),\r\n items.Length == 0\r\n ? Border(TextBlock(\u0022No items yet\u0022).Opacity(0.7)).Padding(16)\r\n : VStack(8,\r\n ForEach(items, item =\u003E\r\n TextBlock(item.Label)\r\n .Padding(8)\r\n .WithKey(item.Id))))\r\n .Padding(16);\r\n }\r\n}\r\n", + "rawCode": "// id: list-with-empty-state\r\n// intent: list with empty-state placeholder when no items exist\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n#:property TargetFramework=net10.0-windows10.0.22621.0\r\n#:property UseWinUI=true\r\n#:property WindowsPackageType=None\r\n\r\nusing System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Empty State\u0022, width: 500, height: 400);\r\n\r\nrecord Favorite(string Id, string Label);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (showItems, setShowItems) = UseState(false);\r\n var items = showItems\r\n ? new[] { new Favorite(\u0022one\u0022, \u0022Design notes\u0022), new Favorite(\u0022two\u0022, \u0022Release checklist\u0022) }\r\n : Array.Empty\u003CFavorite\u003E();\r\n\r\n return VStack(12,\r\n Heading(\u0022Favorites\u0022),\r\n Button(showItems ? \u0022Clear items\u0022 : \u0022Load sample items\u0022, () =\u003E setShowItems(!showItems)),\r\n items.Length == 0\r\n ? Border(TextBlock(\u0022No items yet\u0022).Opacity(0.7)).Padding(16)\r\n : VStack(8,\r\n ForEach(items, item =\u003E\r\n TextBlock(item.Label)\r\n .Padding(8)\r\n .WithKey(item.Id))))\r\n .Padding(16);\r\n }\r\n}\r\n" + }, + { + "id": "list-with-loading", + "category": "lists", + "title": "List With Loading State", + "intent": "show a spinner first and then render fetched list items", + "tags": [ + "list", + "loading", + "spinner", + "async", + "fetch" + ], + "factoryAnchors": [ + "ForEach", + "ProgressRing", + "UseState" + ], + "notesKey": null, + "relatedIds": [ + "list-with-empty-state" + ], + "priority": "P0", + "code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Threading.Tasks;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Loading List\u0022, width: 500, height: 400);\r\n\r\nrecord Order(string Id, string Label);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (loading, setLoading) = UseState(true, threadSafe: true);\r\n var (items, setItems) = UseState\u003CIReadOnlyList\u003COrder\u003E\u003E(Array.Empty\u003COrder\u003E(), threadSafe: true);\r\n\r\n UseEffect(() =\u003E\r\n {\r\n _ = Task.Run(async () =\u003E\r\n {\r\n await Task.Delay(900);\r\n setItems(new[] { new Order(\u0022101\u0022, \u0022Order #101\u0022), new Order(\u0022102\u0022, \u0022Order #102\u0022), new Order(\u0022103\u0022, \u0022Order #103\u0022) });\r\n setLoading(false);\r\n });\r\n }, Array.Empty\u003Cobject\u003E());\r\n\r\n return VStack(12,\r\n Heading(\u0022Recent orders\u0022),\r\n loading\r\n ? HStack(8, ProgressRing().IsActive(true).Width(20).Height(20), TextBlock(\u0022Loading items\u2026\u0022))\r\n : VStack(8,\r\n ForEach(items, item =\u003E\r\n TextBlock(item.Label)\r\n .Padding(8)\r\n .WithKey(item.Id))))\r\n .Padding(16);\r\n }\r\n}\r\n", + "rawCode": "// id: list-with-loading\r\n// intent: list with loading indicator during async fetch\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n#:property TargetFramework=net10.0-windows10.0.22621.0\r\n#:property UseWinUI=true\r\n#:property WindowsPackageType=None\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Threading.Tasks;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Loading List\u0022, width: 500, height: 400);\r\n\r\nrecord Order(string Id, string Label);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var (loading, setLoading) = UseState(true, threadSafe: true);\r\n var (items, setItems) = UseState\u003CIReadOnlyList\u003COrder\u003E\u003E(Array.Empty\u003COrder\u003E(), threadSafe: true);\r\n\r\n UseEffect(() =\u003E\r\n {\r\n _ = Task.Run(async () =\u003E\r\n {\r\n await Task.Delay(900);\r\n setItems(new[] { new Order(\u0022101\u0022, \u0022Order #101\u0022), new Order(\u0022102\u0022, \u0022Order #102\u0022), new Order(\u0022103\u0022, \u0022Order #103\u0022) });\r\n setLoading(false);\r\n });\r\n }, Array.Empty\u003Cobject\u003E());\r\n\r\n return VStack(12,\r\n Heading(\u0022Recent orders\u0022),\r\n loading\r\n ? HStack(8, ProgressRing().IsActive(true).Width(20).Height(20), TextBlock(\u0022Loading items\u2026\u0022))\r\n : VStack(8,\r\n ForEach(items, item =\u003E\r\n TextBlock(item.Label)\r\n .Padding(8)\r\n .WithKey(item.Id))))\r\n .Padding(16);\r\n }\r\n}\r\n" + }, + { + "id": "master-detail", + "category": "lists", + "title": "Master Detail Selection", + "intent": "use a selectable list on the left to drive detail content on the right", + "tags": [ + "master-detail", + "selection", + "list", + "detail", + "split" + ], + "factoryAnchors": [ + "ForEach", + "UseState", + "HStack" + ], + "notesKey": null, + "relatedIds": [ + "list-basic-foreach" + ], + "priority": "P0", + "code": "using System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Master Detail\u0022, width: 500, height: 400);\r\n\r\nrecord Topic(string Id, string Title, string Detail);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var topics = new[]\r\n {\r\n new Topic(\u0022layout\u0022, \u0022Layout\u0022, \u0022Use stacks to compose responsive UI.\u0022),\r\n new Topic(\u0022state\u0022, \u0022State\u0022, \u0022Hooks keep selection and local state in sync.\u0022),\r\n new Topic(\u0022lists\u0022, \u0022Lists\u0022, \u0022ForEach renders rows while state tracks selection.\u0022),\r\n };\r\n var (selectedId, setSelectedId) = UseState(topics[0].Id);\r\n var selected = topics.First(topic =\u003E topic.Id == selectedId);\r\n\r\n return HStack(16,\r\n VStack(8,\r\n Heading(\u0022Topics\u0022),\r\n ForEach(topics, topic =\u003E\r\n Button(topic.Id == selectedId ? $\u0022\u003E {topic.Title}\u0022 : topic.Title, () =\u003E setSelectedId(topic.Id))\r\n .Width(180)\r\n .WithKey(topic.Id))),\r\n Border(\r\n VStack(8,\r\n Heading(selected.Title),\r\n TextBlock(selected.Detail),\r\n TextBlock($\u0022Selected: {selected.Id}\u0022).Opacity(0.7))\r\n ).Padding(12))\r\n .Padding(16);\r\n }\r\n}\r\n", + "rawCode": "// id: master-detail\r\n// intent: master-detail layout with list selection\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n#:property TargetFramework=net10.0-windows10.0.22621.0\r\n#:property UseWinUI=true\r\n#:property WindowsPackageType=None\r\n\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Master Detail\u0022, width: 500, height: 400);\r\n\r\nrecord Topic(string Id, string Title, string Detail);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var topics = new[]\r\n {\r\n new Topic(\u0022layout\u0022, \u0022Layout\u0022, \u0022Use stacks to compose responsive UI.\u0022),\r\n new Topic(\u0022state\u0022, \u0022State\u0022, \u0022Hooks keep selection and local state in sync.\u0022),\r\n new Topic(\u0022lists\u0022, \u0022Lists\u0022, \u0022ForEach renders rows while state tracks selection.\u0022),\r\n };\r\n var (selectedId, setSelectedId) = UseState(topics[0].Id);\r\n var selected = topics.First(topic =\u003E topic.Id == selectedId);\r\n\r\n return HStack(16,\r\n VStack(8,\r\n Heading(\u0022Topics\u0022),\r\n ForEach(topics, topic =\u003E\r\n Button(topic.Id == selectedId ? $\u0022\u003E {topic.Title}\u0022 : topic.Title, () =\u003E setSelectedId(topic.Id))\r\n .Width(180)\r\n .WithKey(topic.Id))),\r\n Border(\r\n VStack(8,\r\n Heading(selected.Title),\r\n TextBlock(selected.Detail),\r\n TextBlock($\u0022Selected: {selected.Id}\u0022).Opacity(0.7))\r\n ).Padding(12))\r\n .Padding(16);\r\n }\r\n}\r\n" + }, + { + "id": "virtualized-large-list", + "category": "lists", + "title": "Virtualized Large List", + "intent": "render a large data set efficiently with LazyVStack virtualization", + "tags": [ + "virtualized", + "lazy", + "large-list", + "performance" + ], + "factoryAnchors": [ + "LazyVStack" + ], + "notesKey": null, + "relatedIds": [ + "list-basic-foreach" + ], + "priority": "P0", + "code": "using System.Collections.Generic;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Virtualized List\u0022, width: 500, height: 400);\r\n\r\nrecord LogRow(string Id, string Message);\r\n\r\nclass App : Component\r\n{\r\n private static readonly IReadOnlyList\u003CLogRow\u003E Rows = Enumerable.Range(1, 1500)\r\n .Select(i =\u003E new LogRow($\u0022row-{i}\u0022, $\u0022Log entry {i}: visible rows are realized on demand.\u0022))\r\n .ToArray();\r\n\r\n public override Element Render()\r\n {\r\n return VStack(12,\r\n Heading($\u0022LazyVStack ({Rows.Count} items)\u0022),\r\n TextBlock(\u0022Use virtualization for long lists.\u0022),\r\n LazyVStack\u003CLogRow\u003E(\r\n Rows,\r\n row =\u003E row.Id,\r\n (row, index) =\u003E HStack(12,\r\n TextBlock($\u0022{index \u002B 1}\u0022).Width(50),\r\n TextBlock(row.Message))\r\n .Padding(8))\r\n .Height(300))\r\n .Padding(16);\r\n }\r\n}\r\n", + "rawCode": "// id: virtualized-large-list\r\n// intent: virtualized list for large datasets using LazyVStack\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n#:property TargetFramework=net10.0-windows10.0.22621.0\r\n#:property UseWinUI=true\r\n#:property WindowsPackageType=None\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Virtualized List\u0022, width: 500, height: 400);\r\n\r\nrecord LogRow(string Id, string Message);\r\n\r\nclass App : Component\r\n{\r\n private static readonly IReadOnlyList\u003CLogRow\u003E Rows = Enumerable.Range(1, 1500)\r\n .Select(i =\u003E new LogRow($\u0022row-{i}\u0022, $\u0022Log entry {i}: visible rows are realized on demand.\u0022))\r\n .ToArray();\r\n\r\n public override Element Render()\r\n {\r\n return VStack(12,\r\n Heading($\u0022LazyVStack ({Rows.Count} items)\u0022),\r\n TextBlock(\u0022Use virtualization for long lists.\u0022),\r\n LazyVStack\u003CLogRow\u003E(\r\n Rows,\r\n row =\u003E row.Id,\r\n (row, index) =\u003E HStack(12,\r\n TextBlock($\u0022{index \u002B 1}\u0022).Width(50),\r\n TextBlock(row.Message))\r\n .Padding(8))\r\n .Height(300))\r\n .Padding(16);\r\n }\r\n}\r\n" + }, + { + "id": "sidebar-nav", + "category": "navigation", + "title": "Sidebar Navigation", + "intent": "typed route navigation with NavigationView, NavigationHost, and UseNavigation", + "tags": [ + "navigation", + "sidebar", + "route", + "navigationview" + ], + "factoryAnchors": [ + "NavigationView", + "NavigationHost", + "UseNavigation" + ], + "notesKey": "NavigationView", + "relatedIds": [], + "priority": "P0", + "code": "using System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CShell\u003E(\u0022Sidebar Navigation\u0022, width: 1000, height: 700);\r\n\r\nenum Route { Home, Library, Settings }\r\n\r\nclass Shell : Component\r\n{\r\n static string ToTag(Route route) =\u003E route.ToString().ToLowerInvariant();\r\n static Route ToRoute(string tag) =\u003E Enum.Parse\u003CRoute\u003E(tag, ignoreCase: true);\r\n\r\n public override Element Render()\r\n {\r\n var nav = UseNavigation(Route.Home);\r\n\r\n return NavigationView(\r\n [\r\n NavItem(\u0022Home\u0022, icon: \u0022\uE80F\u0022, tag: ToTag(Route.Home)),\r\n NavItem(\u0022Library\u0022, icon: \u0022\uE8F1\u0022, tag: ToTag(Route.Library)),\r\n NavItem(\u0022Settings\u0022, icon: \u0022\uE713\u0022, tag: ToTag(Route.Settings)),\r\n ],\r\n NavigationHost(nav, route =\u003E route switch\r\n {\r\n Route.Home =\u003E Component\u003CHomePage\u003E(),\r\n Route.Library =\u003E Component\u003CLibraryPage\u003E(),\r\n Route.Settings =\u003E Component\u003CSettingsPage\u003E(),\r\n _ =\u003E TextBlock(\u0022Not found\u0022)\r\n }))\r\n .WithNavigation(nav, ToTag, ToRoute);\r\n }\r\n}\r\n\r\nclass HomePage : Component\r\n{\r\n public override Element Render() =\u003E\r\n VStack(12,\r\n Heading(\u0022Home\u0022),\r\n TextBlock(\u0022Welcome.\u0022))\r\n .Padding(24);\r\n}\r\n\r\nclass LibraryPage : Component\r\n{\r\n public override Element Render() =\u003E\r\n VStack(12,\r\n Heading(\u0022Library\u0022),\r\n TextBlock(\u0022Your stuff.\u0022))\r\n .Padding(24);\r\n}\r\n\r\nclass SettingsPage : Component\r\n{\r\n public override Element Render()\r\n {\r\n var nav = this.UseNavigation\u003CRoute\u003E();\r\n\r\n return VStack(12,\r\n Heading(\u0022Settings\u0022),\r\n Button(\u0022Back to Home\u0022, () =\u003E nav.Navigate(Route.Home)))\r\n .Padding(24);\r\n }\r\n}\r\n", + "rawCode": "// id: sidebar-nav\r\n// intent: typed sidebar routing with NavigationView and NavigationHost\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Reactor.Core;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CShell\u003E(\u0022Sidebar Navigation\u0022, width: 1000, height: 700);\r\n\r\nenum Route { Home, Library, Settings }\r\n\r\nclass Shell : Component\r\n{\r\n static string ToTag(Route route) =\u003E route.ToString().ToLowerInvariant();\r\n static Route ToRoute(string tag) =\u003E Enum.Parse\u003CRoute\u003E(tag, ignoreCase: true);\r\n\r\n public override Element Render()\r\n {\r\n var nav = UseNavigation(Route.Home);\r\n\r\n return NavigationView(\r\n [\r\n NavItem(\u0022Home\u0022, icon: \u0022\uE80F\u0022, tag: ToTag(Route.Home)),\r\n NavItem(\u0022Library\u0022, icon: \u0022\uE8F1\u0022, tag: ToTag(Route.Library)),\r\n NavItem(\u0022Settings\u0022, icon: \u0022\uE713\u0022, tag: ToTag(Route.Settings)),\r\n ],\r\n NavigationHost(nav, route =\u003E route switch\r\n {\r\n Route.Home =\u003E Component\u003CHomePage\u003E(),\r\n Route.Library =\u003E Component\u003CLibraryPage\u003E(),\r\n Route.Settings =\u003E Component\u003CSettingsPage\u003E(),\r\n _ =\u003E TextBlock(\u0022Not found\u0022)\r\n }))\r\n .WithNavigation(nav, ToTag, ToRoute);\r\n }\r\n}\r\n\r\nclass HomePage : Component\r\n{\r\n public override Element Render() =\u003E\r\n VStack(12,\r\n Heading(\u0022Home\u0022),\r\n TextBlock(\u0022Welcome.\u0022))\r\n .Padding(24);\r\n}\r\n\r\nclass LibraryPage : Component\r\n{\r\n public override Element Render() =\u003E\r\n VStack(12,\r\n Heading(\u0022Library\u0022),\r\n TextBlock(\u0022Your stuff.\u0022))\r\n .Padding(24);\r\n}\r\n\r\nclass SettingsPage : Component\r\n{\r\n public override Element Render()\r\n {\r\n var nav = this.UseNavigation\u003CRoute\u003E();\r\n\r\n return VStack(12,\r\n Heading(\u0022Settings\u0022),\r\n Button(\u0022Back to Home\u0022, () =\u003E nav.Navigate(Route.Home)))\r\n .Padding(24);\r\n }\r\n}\r\n" + }, + { + "id": "body-bodystrong", + "category": "text", + "title": "Body with BodyStrong Emphasis", + "intent": "body text with emphasis using BodyStrong", + "tags": [ + "body", + "bodystrong", + "emphasis", + "paragraph" + ], + "factoryAnchors": [ + "Body", + "BodyStrong" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Body and BodyStrong\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return VStack(\r\n Body(\u0022Body is useful for longer explanatory text in a layout.\u0022),\r\n BodyStrong(\u0022BodyStrong highlights the sentence you want readers to notice first.\u0022),\r\n Body(\u0022Mix them together to keep paragraphs readable while emphasizing key details.\u0022));\r\n }\r\n}\r\n", + "rawCode": "// id: body-bodystrong\r\n// intent: body text with emphasis using BodyStrong\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Body and BodyStrong\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return VStack(\r\n Body(\u0022Body is useful for longer explanatory text in a layout.\u0022),\r\n BodyStrong(\u0022BodyStrong highlights the sentence you want readers to notice first.\u0022),\r\n Body(\u0022Mix them together to keep paragraphs readable while emphasizing key details.\u0022));\r\n }\r\n}\r\n" + }, + { + "id": "heading-subhead-caption", + "category": "text", + "title": "Heading, Subtitle, and Caption", + "intent": "demonstrate the WinUI 3 type ramp (Heading, Subtitle, Caption)", + "tags": [ + "type-ramp", + "heading", + "subtitle", + "caption", + "typography" + ], + "factoryAnchors": [ + "Heading", + "Subtitle", + "Caption", + "Title", + "Body" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Type Ramp\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return VStack(\r\n Title(\u0022Title\u0022),\r\n Heading(\u0022Heading\u0022),\r\n SubHeading(\u0022SubHeading\u0022),\r\n Subtitle(\u0022Subtitle\u0022),\r\n Body(\u0022Body\u0022),\r\n BodyLarge(\u0022BodyLarge\u0022),\r\n BodyStrong(\u0022BodyStrong\u0022),\r\n Caption(\u0022Caption\u0022));\r\n }\r\n}\r\n", + "rawCode": "// id: heading-subhead-caption\r\n// intent: demonstrate the WinUI 3 type ramp (Heading, Subtitle, Caption)\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Type Ramp\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return VStack(\r\n Title(\u0022Title\u0022),\r\n Heading(\u0022Heading\u0022),\r\n SubHeading(\u0022SubHeading\u0022),\r\n Subtitle(\u0022Subtitle\u0022),\r\n Body(\u0022Body\u0022),\r\n BodyLarge(\u0022BodyLarge\u0022),\r\n BodyStrong(\u0022BodyStrong\u0022),\r\n Caption(\u0022Caption\u0022));\r\n }\r\n}\r\n" + }, + { + "id": "localized-text", + "category": "text", + "title": "Localized Text with LocaleProvider", + "intent": "localized text display using LocaleProvider", + "tags": [ + "localization", + "locale", + "i18n", + "text" + ], + "factoryAnchors": [ + "LocaleProvider", + "TextBlock" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Localized Text\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return VStack(\r\n TextBlock(\u0022LocaleProvider sets locale context for its subtree.\u0022),\r\n LocaleProvider(\u0022en-US\u0022, TextBlock(\u0022en-US: Hello from Reactor.\u0022)),\r\n LocaleProvider(\u0022ar-SA\u0022, TextBlock(\u0022ar-SA: \u0645\u0631\u062D\u0628\u0627 \u0645\u0646 Reactor.\u0022)));\r\n }\r\n}\r\n", + "rawCode": "// id: localized-text\r\n// intent: localized text display using LocaleProvider\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Localized Text\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return VStack(\r\n TextBlock(\u0022LocaleProvider sets locale context for its subtree.\u0022),\r\n LocaleProvider(\u0022en-US\u0022, TextBlock(\u0022en-US: Hello from Reactor.\u0022)),\r\n LocaleProvider(\u0022ar-SA\u0022, TextBlock(\u0022ar-SA: \u0645\u0631\u062D\u0628\u0627 \u0645\u0646 Reactor.\u0022)));\r\n }\r\n}\r\n" + }, + { + "id": "rich-text-inlines", + "category": "text", + "title": "Rich Text Inlines", + "intent": "rich text with bold, italic, and hyperlink runs", + "tags": [ + "rich-text", + "inline", + "bold", + "italic", + "hyperlink", + "run" + ], + "factoryAnchors": [ + "RichTextBlock", + "Paragraph", + "Run", + "Hyperlink" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using System;\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Rich Text Inlines\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return RichTextBlock(new[]\r\n {\r\n Paragraph(\r\n Run(\u0022Rich text can mix \u0022),\r\n Run(\u0022bold\u0022) with { IsBold = true },\r\n Run(\u0022, \u0022),\r\n Run(\u0022italic\u0022) with { IsItalic = true },\r\n Run(\u0022, and \u0022),\r\n Hyperlink(\u0022hyperlinks\u0022, new Uri(\u0022https://github.com/microsoft/microsoft-ui-reactor\u0022)),\r\n Run(\u0022 in one paragraph.\u0022))\r\n });\r\n }\r\n}\r\n", + "rawCode": "// id: rich-text-inlines\r\n// intent: rich text with bold, italic, and hyperlink runs\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing System;\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Rich Text Inlines\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return RichTextBlock(new[]\r\n {\r\n Paragraph(\r\n Run(\u0022Rich text can mix \u0022),\r\n Run(\u0022bold\u0022) with { IsBold = true },\r\n Run(\u0022, \u0022),\r\n Run(\u0022italic\u0022) with { IsItalic = true },\r\n Run(\u0022, and \u0022),\r\n Hyperlink(\u0022hyperlinks\u0022, new Uri(\u0022https://github.com/microsoft/microsoft-ui-reactor\u0022)),\r\n Run(\u0022 in one paragraph.\u0022))\r\n });\r\n }\r\n}\r\n" + }, + { + "id": "text-wrap-truncate", + "category": "text", + "title": "Wrap and Truncate Text", + "intent": "text wrapping and trimming with ellipsis", + "tags": [ + "wrap", + "truncate", + "ellipsis", + "maxlines", + "overflow" + ], + "factoryAnchors": [ + "TextBlock" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Xaml;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Wrap and Truncate\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var sample = \u0022A longer sentence shows how text behaves when the available width is smaller than the content.\u0022;\r\n\r\n return VStack(\r\n TextBlock(sample).TextWrapping(TextWrapping.Wrap).MaxLines(2),\r\n TextBlock(sample)\r\n .TextWrapping(TextWrapping.Wrap)\r\n .TextTrimming(TextTrimming.CharacterEllipsis)\r\n .MaxLines(1));\r\n }\r\n}\r\n", + "rawCode": "// id: text-wrap-truncate\r\n// intent: text wrapping and trimming with ellipsis\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing Microsoft.UI.Xaml;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022Wrap and Truncate\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n var sample = \u0022A longer sentence shows how text behaves when the available width is smaller than the content.\u0022;\r\n\r\n return VStack(\r\n TextBlock(sample).TextWrapping(TextWrapping.Wrap).MaxLines(2),\r\n TextBlock(sample)\r\n .TextWrapping(TextWrapping.Wrap)\r\n .TextTrimming(TextTrimming.CharacterEllipsis)\r\n .MaxLines(1));\r\n }\r\n}\r\n" + }, + { + "id": "textblock-basic", + "category": "text", + "title": "Basic TextBlock", + "intent": "display a simple text label", + "tags": [ + "text", + "textblock", + "label", + "display" + ], + "factoryAnchors": [ + "TextBlock" + ], + "notesKey": null, + "relatedIds": [], + "priority": "P0", + "code": "using Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022TextBlock Basic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return VStack(\r\n TextBlock(\u0022Hello from Reactor.\u0022),\r\n TextBlock(\u0022TextBlock displays read-only text content.\u0022),\r\n TextBlock(\u0022Use it for labels, hints, and short status messages.\u0022));\r\n }\r\n}\r\n", + "rawCode": "// id: textblock-basic\r\n// intent: display a simple text label\r\n#:package Microsoft.UI.Reactor@0.0.0-local\r\n#:property Platform=ARM64\r\n\r\nusing Microsoft.UI.Reactor;\r\nusing static Microsoft.UI.Reactor.Factories;\r\n\r\nReactorApp.Run\u003CApp\u003E(\u0022TextBlock Basic\u0022, width: 400, height: 300);\r\n\r\nclass App : Component\r\n{\r\n public override Element Render()\r\n {\r\n return VStack(\r\n TextBlock(\u0022Hello from Reactor.\u0022),\r\n TextBlock(\u0022TextBlock displays read-only text content.\u0022),\r\n TextBlock(\u0022Use it for labels, hints, and short status messages.\u0022));\r\n }\r\n}\r\n" + } + ], + "generatedAt": "2026-05-29T16:19:51.9199352\u002B00:00" +} \ No newline at end of file diff --git a/samples/scenarios/buttons/appbarbutton-in-commandbar/Scenario.cs b/samples/scenarios/buttons/appbarbutton-in-commandbar/Scenario.cs new file mode 100644 index 000000000..675022345 --- /dev/null +++ b/samples/scenarios/buttons/appbarbutton-in-commandbar/Scenario.cs @@ -0,0 +1,30 @@ +// id: appbarbutton-in-commandbar +// intent: command bar with app bar buttons +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("AppBarButtonInCommandBar", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (lastAction, setLastAction) = UseState("(none)"); + return VStack(12, + CommandBar( + primaryCommands: new[] + { + AppBarButton("Add", () => setLastAction("Add"), icon: "Add"), + AppBarButton("Share", () => setLastAction("Share"), icon: "Share") + }, + secondaryCommands: new[] + { + AppBarButton("Delete", () => setLastAction("Delete"), icon: "Delete") + }), + TextBlock($"Last action: {lastAction}").Padding(24)); + } +} \ No newline at end of file diff --git a/samples/scenarios/buttons/appbarbutton-in-commandbar/scenario.json b/samples/scenarios/buttons/appbarbutton-in-commandbar/scenario.json new file mode 100644 index 000000000..9260bd304 --- /dev/null +++ b/samples/scenarios/buttons/appbarbutton-in-commandbar/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "appbarbutton-in-commandbar", + "category": "buttons", + "title": "CommandBar with AppBarButtons", + "intent": "command bar with app bar buttons for a toolbar-style action surface", + "tags": ["commandbar", "appbarbutton", "toolbar", "actions"], + "factoryAnchors": ["CommandBar", "AppBarButton"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} \ No newline at end of file diff --git a/samples/scenarios/buttons/button-label-onclick/Scenario.cs b/samples/scenarios/buttons/button-label-onclick/Scenario.cs new file mode 100644 index 000000000..da50ffb6b --- /dev/null +++ b/samples/scenarios/buttons/button-label-onclick/Scenario.cs @@ -0,0 +1,23 @@ +// id: button-label-onclick +// intent: basic button with click handler +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("ButtonLabelOnClick", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (count, setCount) = UseState(0); + return VStack(12, + Heading("Click counter"), + TextBlock($"Clicked {count} time{(count == 1 ? "" : "s")}."), + Button("Increment", () => setCount(count + 1)).AccentButton()) + .Padding(24); + } +} \ No newline at end of file diff --git a/samples/scenarios/buttons/button-label-onclick/scenario.json b/samples/scenarios/buttons/button-label-onclick/scenario.json new file mode 100644 index 000000000..c74e22312 --- /dev/null +++ b/samples/scenarios/buttons/button-label-onclick/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "button-label-onclick", + "category": "buttons", + "title": "Button click counter", + "intent": "basic button with click handler that increments a counter", + "tags": ["button", "click", "counter", "onclick"], + "factoryAnchors": ["Button", "UseState"], + "notesKey": "Button", + "relatedIds": [], + "priority": "P0" +} \ No newline at end of file diff --git a/samples/scenarios/buttons/button-with-command/Scenario.cs b/samples/scenarios/buttons/button-with-command/Scenario.cs new file mode 100644 index 000000000..b97fbeef3 --- /dev/null +++ b/samples/scenarios/buttons/button-with-command/Scenario.cs @@ -0,0 +1,27 @@ +// id: button-with-command +// intent: button driven by a Command object (label, execute, canExecute) +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("ButtonWithCommand", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (runs, setRuns) = UseState(0); + var save = new Command + { + Label = runs < 3 ? $"Save ({3 - runs} left)" : "Done", + Execute = () => setRuns(runs + 1), + CanExecute = runs < 3 + }; + + return VStack(12, Button(save), TextBlock($"Executed: {runs}")) + .Padding(24); + } +} \ No newline at end of file diff --git a/samples/scenarios/buttons/button-with-command/scenario.json b/samples/scenarios/buttons/button-with-command/scenario.json new file mode 100644 index 000000000..16175c77a --- /dev/null +++ b/samples/scenarios/buttons/button-with-command/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "button-with-command", + "category": "buttons", + "title": "Button with Command", + "intent": "command-driven button where label action and enabled state come from a Command", + "tags": ["button", "command", "mvvm", "canexecute"], + "factoryAnchors": ["Button"], + "notesKey": "Button", + "relatedIds": [], + "priority": "P0" +} \ No newline at end of file diff --git a/samples/scenarios/buttons/button-with-icon/Scenario.cs b/samples/scenarios/buttons/button-with-icon/Scenario.cs new file mode 100644 index 000000000..bf6af3c12 --- /dev/null +++ b/samples/scenarios/buttons/button-with-icon/Scenario.cs @@ -0,0 +1,26 @@ +// id: button-with-icon +// intent: button with a symbol icon +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using Microsoft.UI.Xaml.Controls; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("ButtonWithIcon", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (status, setStatus) = UseState("Ready"); + return VStack(12, + Button(HStack(8, Icon(Symbol.Save), TextBlock("Save draft")), + () => setStatus("Draft saved")) + .AutomationName("Save draft") + .AccentButton(), + TextBlock(status).Foreground(Theme.SecondaryText)) + .Padding(24); + } +} \ No newline at end of file diff --git a/samples/scenarios/buttons/button-with-icon/scenario.json b/samples/scenarios/buttons/button-with-icon/scenario.json new file mode 100644 index 000000000..dda2f936e --- /dev/null +++ b/samples/scenarios/buttons/button-with-icon/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "button-with-icon", + "category": "buttons", + "title": "Button with icon", + "intent": "button with an icon and text using a symbol icon", + "tags": ["button", "icon", "symbol"], + "factoryAnchors": ["Button"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} \ No newline at end of file diff --git a/samples/scenarios/buttons/hyperlink-button/Scenario.cs b/samples/scenarios/buttons/hyperlink-button/Scenario.cs new file mode 100644 index 000000000..04666145c --- /dev/null +++ b/samples/scenarios/buttons/hyperlink-button/Scenario.cs @@ -0,0 +1,24 @@ +// id: hyperlink-button +// intent: inline hyperlink-style button +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("HyperlinkButton", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (status, setStatus) = UseState("Open docs"); + return VStack(12, + TextBlock("Need more details?"), + HyperlinkButton("Open docs", onClick: () => setStatus("Navigation requested")) + .TextLink(), + TextBlock(status).Foreground(Theme.SecondaryText)) + .Padding(24); + } +} \ No newline at end of file diff --git a/samples/scenarios/buttons/hyperlink-button/scenario.json b/samples/scenarios/buttons/hyperlink-button/scenario.json new file mode 100644 index 000000000..26de0d488 --- /dev/null +++ b/samples/scenarios/buttons/hyperlink-button/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "hyperlink-button", + "category": "buttons", + "title": "Hyperlink button", + "intent": "hyperlink-style button for link-like navigation or actions", + "tags": ["hyperlink", "link", "button"], + "factoryAnchors": ["HyperlinkButton"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} \ No newline at end of file diff --git a/samples/scenarios/buttons/togglebutton-basic/Scenario.cs b/samples/scenarios/buttons/togglebutton-basic/Scenario.cs new file mode 100644 index 000000000..7567b54c4 --- /dev/null +++ b/samples/scenarios/buttons/togglebutton-basic/Scenario.cs @@ -0,0 +1,23 @@ +// id: togglebutton-basic +// intent: toggle button with checked state +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("ToggleButtonBasic", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (isOn, setIsOn) = UseState(false); + return VStack(12, + ToggleButton("Notifications", isOn, setIsOn), + TextBlock(isOn ? "Notifications are on" : "Notifications are off") + .Foreground(Theme.SecondaryText)) + .Padding(24); + } +} \ No newline at end of file diff --git a/samples/scenarios/buttons/togglebutton-basic/scenario.json b/samples/scenarios/buttons/togglebutton-basic/scenario.json new file mode 100644 index 000000000..8a1dc3c55 --- /dev/null +++ b/samples/scenarios/buttons/togglebutton-basic/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "togglebutton-basic", + "category": "buttons", + "title": "Basic toggle button", + "intent": "toggle button that tracks checked state with UseState", + "tags": ["toggle", "togglebutton", "checked", "state"], + "factoryAnchors": ["ToggleButton", "UseState"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} \ No newline at end of file diff --git a/samples/scenarios/forms/form-async-submit/Scenario.cs b/samples/scenarios/forms/form-async-submit/Scenario.cs new file mode 100644 index 000000000..d3bb6c0a5 --- /dev/null +++ b/samples/scenarios/forms/form-async-submit/Scenario.cs @@ -0,0 +1,44 @@ +// id: form-async-submit +// intent: form submission with loading state +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System.Threading.Tasks; +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Form async submit", width: 500, height: 400); + +class App : Component +{ + public override Element Render() + { + var (name, setName) = UseState(""); + var (email, setEmail) = UseState(""); + var (isSubmitting, setIsSubmitting) = UseState(false, threadSafe: true); + var (status, setStatus) = UseState("Fill in the form.", threadSafe: true); + + return VStack(12, + Heading("Newsletter signup"), + TextField(name, setName, placeholder: "Full name", header: "Name"), + TextField(email, setEmail, placeholder: "you@example.com", header: "Email"), + HStack(8, + Button(isSubmitting ? "Submitting..." : "Submit", () => + { + if (isSubmitting) return; + setIsSubmitting(true); + setStatus("Submitting..."); + _ = Task.Run(async () => + { + await Task.Delay(1200); + setStatus($"Saved {name} ({email})."); + setIsSubmitting(false); + }); + }) + .AccentButton() + .Set(b => b.IsEnabled = !isSubmitting && !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(email)), + isSubmitting ? ProgressRing().IsActive(true).Width(20).Height(20) : Empty()), + TextBlock(status)) + .Padding(24); + } +} diff --git a/samples/scenarios/forms/form-async-submit/scenario.json b/samples/scenarios/forms/form-async-submit/scenario.json new file mode 100644 index 000000000..a1990ca6d --- /dev/null +++ b/samples/scenarios/forms/form-async-submit/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "form-async-submit", + "category": "forms", + "title": "Async submit form", + "intent": "form submission with loading state", + "tags": ["form", "submit", "async", "loading", "state"], + "factoryAnchors": ["Button", "UseState", "ProgressRing"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/forms/form-field-wrapper/Scenario.cs b/samples/scenarios/forms/form-field-wrapper/Scenario.cs new file mode 100644 index 000000000..4be6e68c6 --- /dev/null +++ b/samples/scenarios/forms/form-field-wrapper/Scenario.cs @@ -0,0 +1,35 @@ +// id: form-field-wrapper +// intent: FormField wrapper with label and required marker +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls.Validation; +using static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Form field wrapper", width: 500, height: 400); + +class App : Component +{ + public override Element Render() + { + var ctx = this.UseValidationContext(); + var (fullName, setFullName) = UseState(""); + var (company, setCompany) = UseState(""); + var (role, setRole) = UseState(""); + + return VStack(12, + Heading("Profile details"), + FormField(TextField(fullName, v => { setFullName(v); ctx.MarkTouched("fullName"); }, placeholder: "Ada Lovelace") + .Validate("fullName", fullName, Validate.Required("Full name is required")), + label: "Full name", required: true, description: "Shown on receipts"), + FormField(TextField(company, v => { setCompany(v); ctx.MarkTouched("company"); }, placeholder: "Contoso") + .Validate("company", company, Validate.MinLength(2, "Company name looks too short")), + label: "Company", description: "Optional but useful"), + FormField(TextField(role, v => { setRole(v); ctx.MarkTouched("role"); }, placeholder: "Product manager"), + label: "Role", description: "No validation on this field"), + Button("Show field state", () => ctx.MarkAllTouched())) + .Padding(24); + } +} diff --git a/samples/scenarios/forms/form-field-wrapper/scenario.json b/samples/scenarios/forms/form-field-wrapper/scenario.json new file mode 100644 index 000000000..4c5dbf6ab --- /dev/null +++ b/samples/scenarios/forms/form-field-wrapper/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "form-field-wrapper", + "category": "forms", + "title": "FormField wrapper", + "intent": "FormField wrapper with label and required marker", + "tags": ["form", "formfield", "label", "required", "wrapper"], + "factoryAnchors": ["FormField", "TextField"], + "notesKey": "FormField", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/forms/form-submit-gating/Scenario.cs b/samples/scenarios/forms/form-submit-gating/Scenario.cs new file mode 100644 index 000000000..f4203e3bd --- /dev/null +++ b/samples/scenarios/forms/form-submit-gating/Scenario.cs @@ -0,0 +1,36 @@ +// id: form-submit-gating +// intent: disable submit button until form is valid +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls.Validation; +using static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Form submit gating", width: 500, height: 400); + +class App : Component +{ + public override Element Render() + { + var ctx = this.UseValidationContext(); + var (email, setEmail) = UseState(""); + var (accessCode, setAccessCode) = UseState(""); + var (status, setStatus) = UseState("Waiting for valid input."); + + return VStack(12, + Heading("Submit gating"), + FormField(TextField(email, v => { setEmail(v); ctx.MarkTouched("email"); }, placeholder: "you@example.com") + .Validate("email", email, Validate.Required("Email is required"), Validate.Email("Enter a valid email")), + label: "Email", required: true), + FormField(TextField(accessCode, v => { setAccessCode(v); ctx.MarkTouched("accessCode"); }, placeholder: "ACCESS-123") + .Validate("accessCode", accessCode, Validate.Required("Code is required"), Validate.MinLength(8, "Use at least 8 characters")), + label: "Access code", required: true), + Button("Submit", () => setStatus($"Submitted for {email}.")) + .AccentButton() + .Set(b => b.IsEnabled = ctx.IsValid()), + TextBlock(status)) + .Padding(24); + } +} diff --git a/samples/scenarios/forms/form-submit-gating/scenario.json b/samples/scenarios/forms/form-submit-gating/scenario.json new file mode 100644 index 000000000..63d8c8f16 --- /dev/null +++ b/samples/scenarios/forms/form-submit-gating/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "form-submit-gating", + "category": "forms", + "title": "Submit gating", + "intent": "disable submit button until form is valid", + "tags": ["form", "submit", "gating", "validation", "enabled"], + "factoryAnchors": ["UseValidationContext", "Button", "FormField"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/forms/form-text-fields/Scenario.cs b/samples/scenarios/forms/form-text-fields/Scenario.cs new file mode 100644 index 000000000..7b695b675 --- /dev/null +++ b/samples/scenarios/forms/form-text-fields/Scenario.cs @@ -0,0 +1,27 @@ +// id: form-text-fields +// intent: basic form with multiple text inputs and labels +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Form text fields", width: 500, height: 400); + +class App : Component +{ + public override Element Render() + { + var (name, setName) = UseState(""); + var (email, setEmail) = UseState(""); + var (message, setMessage) = UseState(""); + + return VStack(12, + Heading("Contact form"), + TextField(name, setName, placeholder: "Full name", header: "Name"), + TextField(email, setEmail, placeholder: "you@example.com", header: "Email"), + TextField(message, setMessage, placeholder: "How can we help?", header: "Message"), + TextBlock($"Draft: {name} / {email}")) + .Padding(24); + } +} diff --git a/samples/scenarios/forms/form-text-fields/scenario.json b/samples/scenarios/forms/form-text-fields/scenario.json new file mode 100644 index 000000000..384e90564 --- /dev/null +++ b/samples/scenarios/forms/form-text-fields/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "form-text-fields", + "category": "forms", + "title": "Basic text fields form", + "intent": "basic form with multiple text inputs and labels", + "tags": ["form", "textfield", "input", "layout"], + "factoryAnchors": ["TextField", "VStack", "UseState"], + "notesKey": "TextField", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/forms/form-validation-context/Scenario.cs b/samples/scenarios/forms/form-validation-context/Scenario.cs new file mode 100644 index 000000000..6636664e6 --- /dev/null +++ b/samples/scenarios/forms/form-validation-context/Scenario.cs @@ -0,0 +1,39 @@ +// id: form-validation-context +// intent: form validation with UseValidationContext +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls.Validation; +using static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Form validation context", width: 500, height: 400); + +class App : Component +{ + public override Element Render() + { + var ctx = this.UseValidationContext(); + var (name, setName) = UseState(""); + var (email, setEmail) = UseState(""); + var (password, setPassword) = UseState(""); + var (submitted, setSubmitted) = UseState(false); + var showWhen = submitted ? ShowWhen.Always : ShowWhen.WhenTouched; + + return VStack(12, + Heading("Create account"), + FormField(TextField(name, v => { setName(v); ctx.MarkTouched("name"); }, placeholder: "Full name") + .Validate("name", name, Validate.Required("Name is required"), Validate.MinLength(2, "Use at least 2 characters")), + label: "Name", required: true, showWhen: showWhen), + FormField(TextField(email, v => { setEmail(v); ctx.MarkTouched("email"); }, placeholder: "you@example.com") + .Validate("email", email, Validate.Required("Email is required"), Validate.Email("Enter a valid email")), + label: "Email", required: true, showWhen: showWhen), + FormField(TextField(password, v => { setPassword(v); ctx.MarkTouched("password"); }, placeholder: "Minimum 8 characters") + .Validate("password", password, Validate.Required("Password is required"), Validate.MinLength(8, "Use at least 8 characters")), + label: "Password", required: true, showWhen: showWhen), + Button("Validate", () => { setSubmitted(true); ctx.MarkAllTouched(); }).AccentButton(), + TextBlock(ctx.IsValid() ? "All fields pass validation." : "Fix the errors above.")) + .Padding(24); + } +} diff --git a/samples/scenarios/forms/form-validation-context/scenario.json b/samples/scenarios/forms/form-validation-context/scenario.json new file mode 100644 index 000000000..09db2b3c1 --- /dev/null +++ b/samples/scenarios/forms/form-validation-context/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "form-validation-context", + "category": "forms", + "title": "Validation context form", + "intent": "form validation with UseValidationContext", + "tags": ["form", "validation", "required", "email", "error"], + "factoryAnchors": ["UseValidationContext", "FormField", "TextField"], + "notesKey": "UseValidationContext", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/forms/form-with-server-errors/Scenario.cs b/samples/scenarios/forms/form-with-server-errors/Scenario.cs new file mode 100644 index 000000000..01cb8b4cd --- /dev/null +++ b/samples/scenarios/forms/form-with-server-errors/Scenario.cs @@ -0,0 +1,49 @@ +// id: form-with-server-errors +// intent: display server-side validation errors on form fields +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls.Validation; +using static Microsoft.UI.Reactor.Controls.Validation.FormFieldDsl; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Form with server errors", width: 500, height: 400); + +class App : Component +{ + public override Element Render() + { + var ctx = this.UseValidationContext(); + var (email, setEmail) = UseState(""); + var (inviteCode, setInviteCode) = UseState(""); + var (status, setStatus) = UseState("Submit to simulate API validation."); + + return VStack(12, + Heading("Invite signup"), + FormField(TextField(email, v => { setEmail(v); ctx.MarkTouched("email"); }, placeholder: "user@contoso.com") + .Validate("email", email, Validate.Required("Email is required"), Validate.Email("Enter a valid email")), + label: "Email", required: true, showWhen: ShowWhen.Always), + FormField(TextField(inviteCode, v => { setInviteCode(v); ctx.MarkTouched("inviteCode"); }, placeholder: "INVITE-2025") + .Validate("inviteCode", inviteCode, Validate.Required("Invite code is required"), Validate.MinLength(6, "Use at least 6 characters")), + label: "Invite code", required: true, showWhen: ShowWhen.Always), + Button("Submit", () => + { + ctx.ClearExternal("email"); + ctx.ClearExternal("inviteCode"); + ctx.MarkAllTouched(); + if (!ctx.IsValid()) + { + setStatus("Fix the client-side errors first."); + return; + } + if (!email.EndsWith("@contoso.com", System.StringComparison.OrdinalIgnoreCase)) + ctx.AddExternal("email", "This email is not allowed by the server."); + if (inviteCode != "INVITE-2025") + ctx.AddExternal("inviteCode", "That invite code has expired."); + setStatus(ctx.IsValid() ? "Server accepted the form." : "Server returned field errors."); + }).AccentButton(), + TextBlock(status)) + .Padding(24); + } +} diff --git a/samples/scenarios/forms/form-with-server-errors/scenario.json b/samples/scenarios/forms/form-with-server-errors/scenario.json new file mode 100644 index 000000000..8c85c5594 --- /dev/null +++ b/samples/scenarios/forms/form-with-server-errors/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "form-with-server-errors", + "category": "forms", + "title": "Server-side field errors", + "intent": "display server-side validation errors on form fields", + "tags": ["form", "server", "errors", "validation", "api"], + "factoryAnchors": ["UseState", "FormField", "TextField"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/async-fetch-list/Scenario.cs b/samples/scenarios/hooks/async-fetch-list/Scenario.cs new file mode 100644 index 000000000..28ccf3e63 --- /dev/null +++ b/samples/scenarios/hooks/async-fetch-list/Scenario.cs @@ -0,0 +1,76 @@ +// id: async-fetch-list +// intent: fetch async data with UseResource and render loading, error, and reloading states +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using Microsoft.UI.Xaml.Controls; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Async Fetch List", width: 560, height: 520); + +record Repo(int Id, string Name, string Description); + +static class Api +{ + public static async Task> ListReposAsync(string owner, CancellationToken cancellationToken) + { + await Task.Delay(800, cancellationToken); + if (owner == "fail") + { + throw new InvalidOperationException("Owner not found"); + } + + return + [ + new(1, $"{owner}/alpha", "first repo"), + new(2, $"{owner}/beta", "second repo"), + new(3, $"{owner}/gamma", "third repo") + ]; + } +} + +class App : Component +{ + public override Element Render() + { + var (owner, setOwner) = UseState("microsoft"); + var repos = UseResource(cancellationToken => Api.ListReposAsync(owner, cancellationToken), deps: [owner]); + + return VStack(12, + TextField(owner, setOwner, placeholder: "GitHub owner"), + Caption("Try \"fail\" to see the error state."), + repos.Match( + loading: () => HStack(8, + ProgressRing().IsActive(true).Width(20).Height(20), + TextBlock("Loading…")), + data: list => VStack(8, + list.Select(repo => + Border( + VStack(2, + TextBlock(repo.Name).Bold(), + Caption(repo.Description))) + .Padding(12) + .CornerRadius(6) + .WithKey(repo.Id.ToString()) + ).ToArray()), + error: ex => InfoBar("Error", ex.Message).Severity(InfoBarSeverity.Error), + reloading: previous => VStack(8, + HStack(8, + ProgressRing().IsActive(true).Width(20).Height(20), + TextBlock("Refreshing…")), + VStack(8, + previous.Select(repo => + TextBlock(repo.Name) + .Opacity(0.5) + .WithKey(repo.Id.ToString())) + .ToArray())))) + .Padding(24); + } +} diff --git a/samples/scenarios/hooks/async-fetch-list/scenario.json b/samples/scenarios/hooks/async-fetch-list/scenario.json new file mode 100644 index 000000000..9a7c1470b --- /dev/null +++ b/samples/scenarios/hooks/async-fetch-list/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "async-fetch-list", + "category": "hooks", + "title": "Async Fetch List", + "intent": "fetch async data with UseResource and render loading, error, and reloading states", + "tags": ["async", "fetch", "resource", "loading", "error", "reloading"], + "factoryAnchors": ["UseResource", "InfoBar", "ProgressRing"], + "notesKey": "UseResource", + "relatedIds": ["list-with-loading"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/custom-hook-pattern/Scenario.cs b/samples/scenarios/hooks/custom-hook-pattern/Scenario.cs new file mode 100644 index 000000000..5395cb1af --- /dev/null +++ b/samples/scenarios/hooks/custom-hook-pattern/Scenario.cs @@ -0,0 +1,55 @@ +// id: custom-hook-pattern +// intent: compose hooks into a reusable custom Use* extension that debounces input +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Custom hooks should keep the Use* naming convention and compose other hooks internally. +ReactorApp.Run("CustomHookPattern", width: 400, height: 200); + +class App : Component +{ + public override Element Render() => RenderEachTime(UseDebouncedView); + + static Element UseDebouncedView(RenderContext ctx) + { + var (query, setQuery) = ctx.UseState(""); + var debounced = ctx.UseDebouncedValue(query, 400); + + return VStack(12, + TextBox(query, setQuery, "Type quickly", header: "Live query"), + TextBlock($"Immediate: {query}"), + TextBlock($"Debounced: {debounced}")); + } +} + +static class DebounceHooks +{ + public static T UseDebouncedValue(this RenderContext ctx, T value, int delayMs) + { + var (debounced, setDebounced) = ctx.UseState(value); + ctx.UseEffect(() => + { + var cts = new CancellationTokenSource(); + _ = Task.Run(async () => + { + try + { + await Task.Delay(delayMs, cts.Token); + setDebounced(value); + } + catch (TaskCanceledException) + { + } + }); + return () => cts.Cancel(); + }, value!, delayMs); + return debounced; + } +} diff --git a/samples/scenarios/hooks/custom-hook-pattern/scenario.json b/samples/scenarios/hooks/custom-hook-pattern/scenario.json new file mode 100644 index 000000000..5d7b91d26 --- /dev/null +++ b/samples/scenarios/hooks/custom-hook-pattern/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "custom-hook-pattern", + "category": "hooks", + "title": "Reusable custom hook", + "intent": "compose hooks into a reusable custom Use extension that debounces input", + "tags": ["custom", "hook", "extension", "reuse", "pattern", "naming"], + "factoryAnchors": ["UseState", "UseEffect"], + "notesKey": "UseState", + "relatedIds": ["use-effect-deps"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-callback/Scenario.cs b/samples/scenarios/hooks/use-callback/Scenario.cs new file mode 100644 index 000000000..10c3d3e32 --- /dev/null +++ b/samples/scenarios/hooks/use-callback/Scenario.cs @@ -0,0 +1,43 @@ +// id: use-callback +// intent: memoize a callback so child props stay stable across unrelated parent renders +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Stable callback identity helps memoized children skip unnecessary updates. +ReactorApp.Run("UseCallback", width: 400, height: 200); + +class App : Component +{ + record ChildProps(Action OnPress); + + public override Element Render() + { + var (parentRenders, setParentRenders) = UseState(0); + var (clicks, setClicks) = UseState(0); + var increment = UseCallback(() => setClicks(clicks + 1), clicks); + + return VStack(12, + Heading($"Clicks: {clicks}"), + Button("Re-render parent", () => setParentRenders(parentRenders + 1)), + Caption($"Unrelated parent renders: {parentRenders}"), + Component(new(increment))); + } + + class ChildButton : Component + { + int _renders; + + public override Element Render() + { + _renders++; + return VStack(8, + Caption($"Child renders: {_renders}"), + Button("Increment from child", Props.OnPress)); + } + } +} + diff --git a/samples/scenarios/hooks/use-callback/scenario.json b/samples/scenarios/hooks/use-callback/scenario.json new file mode 100644 index 000000000..d37796db0 --- /dev/null +++ b/samples/scenarios/hooks/use-callback/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-callback", + "category": "hooks", + "title": "Stable callback identity", + "intent": "memoize a callback so child props stay stable across unrelated parent re-renders", + "tags": ["callback", "memoize", "stable", "child", "performance"], + "factoryAnchors": ["UseCallback", "UseState", "Button"], + "notesKey": "UseCallback", + "relatedIds": ["use-memo"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-context-basic/Scenario.cs b/samples/scenarios/hooks/use-context-basic/Scenario.cs new file mode 100644 index 000000000..4ec0e1382 --- /dev/null +++ b/samples/scenarios/hooks/use-context-basic/Scenario.cs @@ -0,0 +1,36 @@ +// id: use-context-basic +// intent: provide a value at the root and consume it from a descendant +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Context removes prop-drilling when several descendants need the same value. +ReactorApp.Run("UseContextBasic", width: 400, height: 200); + +class App : Component +{ + internal static readonly Context ThemeContext = new("Light"); + + public override Element Render() + { + var (theme, setTheme) = UseState("Light"); + + return VStack(12, + Button(theme == "Light" ? "Switch to dark" : "Switch to light", + () => setTheme(theme == "Light" ? "Dark" : "Light")).AutomationName("Toggle theme"), + Component()) + .Provide(ThemeContext, theme); + } + + class ThemeBadge : Component + { + public override Element Render() + { + var theme = UseContext(ThemeContext); + return TextBlock($"Current theme from context: {theme}"); + } + } +} diff --git a/samples/scenarios/hooks/use-context-basic/scenario.json b/samples/scenarios/hooks/use-context-basic/scenario.json new file mode 100644 index 000000000..4ab620345 --- /dev/null +++ b/samples/scenarios/hooks/use-context-basic/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-context-basic", + "category": "hooks", + "title": "Basic context provider", + "intent": "provide a context value at the root and consume it in a nested child", + "tags": ["context", "provider", "consume", "share"], + "factoryAnchors": ["UseContext", "Provider", "VStack"], + "notesKey": "UseContext", + "relatedIds": ["use-context-multi", "use-reducer-with-context"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-context-multi/Scenario.cs b/samples/scenarios/hooks/use-context-multi/Scenario.cs new file mode 100644 index 000000000..f016b24a0 --- /dev/null +++ b/samples/scenarios/hooks/use-context-multi/Scenario.cs @@ -0,0 +1,37 @@ +// id: use-context-multi +// intent: compose multiple nested contexts and read them from the same child +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Multiple contexts can be layered so consumers read each concern independently. +ReactorApp.Run("UseContextMulti", width: 400, height: 200); + +class App : Component +{ + internal static readonly Context ThemeContext = new("Light"); + internal static readonly Context UserContext = new("Guest"); + + public override Element Render() + { + return VStack(12, + Component()) + .Provide(UserContext, "Ada") + .Provide(ThemeContext, "Dark"); + } + + class ProfileCard : Component + { + public override Element Render() + { + var user = UseContext(UserContext); + var theme = UseContext(ThemeContext); + return VStack(8, + Heading($"Hello, {user}"), + Caption($"Theme from context: {theme}")); + } + } +} diff --git a/samples/scenarios/hooks/use-context-multi/scenario.json b/samples/scenarios/hooks/use-context-multi/scenario.json new file mode 100644 index 000000000..067f65f6d --- /dev/null +++ b/samples/scenarios/hooks/use-context-multi/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-context-multi", + "category": "hooks", + "title": "Composed contexts", + "intent": "compose nested providers and consume multiple context values in one child", + "tags": ["context", "multi", "compose", "nested", "provider"], + "factoryAnchors": ["UseContext", "Provider", "VStack"], + "notesKey": "UseContext", + "relatedIds": ["use-context-basic"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-effect-cleanup/Scenario.cs b/samples/scenarios/hooks/use-effect-cleanup/Scenario.cs new file mode 100644 index 000000000..f29f3c3e5 --- /dev/null +++ b/samples/scenarios/hooks/use-effect-cleanup/Scenario.cs @@ -0,0 +1,40 @@ +// id: use-effect-cleanup +// intent: set up a timer in an effect and clean it up on unmount +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using Microsoft.UI.Xaml; +using static Microsoft.UI.Reactor.Factories; + +// Cleanup returns the timer to a stopped, unsubscribed state when the component leaves. +ReactorApp.Run("UseEffectCleanup", width: 400, height: 200); + +class App : Component +{ + public override Element Render() + { + var (seconds, updateSeconds) = UseReducer(0); + + UseEffect(() => + { + var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + EventHandler onTick = (_, _) => updateSeconds(value => value + 1); + timer.Tick += onTick; + timer.Start(); + + return () => + { + timer.Tick -= onTick; + timer.Stop(); + }; + }, Array.Empty()); + + return VStack(12, + Heading($"Elapsed: {seconds}s"), + Caption("The cleanup lambda stops the timer when this scenario unmounts.")); + } +} + diff --git a/samples/scenarios/hooks/use-effect-cleanup/scenario.json b/samples/scenarios/hooks/use-effect-cleanup/scenario.json new file mode 100644 index 000000000..1a06634b2 --- /dev/null +++ b/samples/scenarios/hooks/use-effect-cleanup/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-effect-cleanup", + "category": "hooks", + "title": "Effect cleanup with timer", + "intent": "set up a timer in an effect and dispose it from the cleanup callback", + "tags": ["effect", "cleanup", "dispose", "timer", "subscription"], + "factoryAnchors": ["UseEffect", "UseState", "TextBlock"], + "notesKey": "UseEffect", + "relatedIds": ["use-effect-mount"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-effect-deps/Scenario.cs b/samples/scenarios/hooks/use-effect-deps/Scenario.cs new file mode 100644 index 000000000..31818149a --- /dev/null +++ b/samples/scenarios/hooks/use-effect-deps/Scenario.cs @@ -0,0 +1,40 @@ +// id: use-effect-deps +// intent: re-run an effect whenever the search query dependency changes +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using System.Linq; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// The effect reruns for each query change and writes the latest derived results. +ReactorApp.Run("UseEffectDeps", width: 400, height: 200); + +class App : Component +{ + static readonly string[] Data = ["Hooks", "Reducer", "Memo", "Context", "Ref", "Effect"]; + + public override Element Render() + { + var (query, setQuery) = UseState(""); + var (runs, bumpRuns) = UseReducer(0); + var (results, setResults) = UseState(Array.Empty()); + + UseEffect(() => + { + bumpRuns(value => value + 1); + setResults(query.Length == 0 + ? Array.Empty() + : Data.Where(item => item.Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray()); + }, query); + + return VStack(12, + TextBox(query, setQuery, "Search hooks", header: "Search query"), + Caption($"Effect runs: {runs}"), + ForEach(results, item => TextBlock(item))); + } +} + diff --git a/samples/scenarios/hooks/use-effect-deps/scenario.json b/samples/scenarios/hooks/use-effect-deps/scenario.json new file mode 100644 index 000000000..c5ab2e88f --- /dev/null +++ b/samples/scenarios/hooks/use-effect-deps/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-effect-deps", + "category": "hooks", + "title": "Effect dependencies", + "intent": "rerun an effect when a dependency changes by watching the current search query", + "tags": ["effect", "deps", "dependency", "rerun"], + "factoryAnchors": ["UseEffect", "UseState", "TextBox", "VStack"], + "notesKey": "UseEffect", + "relatedIds": ["use-effect-mount"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-effect-mount/Scenario.cs b/samples/scenarios/hooks/use-effect-mount/Scenario.cs new file mode 100644 index 000000000..a3abf2925 --- /dev/null +++ b/samples/scenarios/hooks/use-effect-mount/Scenario.cs @@ -0,0 +1,31 @@ +// id: use-effect-mount +// intent: run a fire-once effect after the component mounts +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// An empty dependency array gives this effect mount-only semantics. +ReactorApp.Run("UseEffectMount", width: 400, height: 200); + +class App : Component +{ + public override Element Render() + { + var (status, setStatus) = UseState("Mounting..."); + + UseEffect(() => + { + setStatus("Loaded initial data on mount."); + }, Array.Empty()); + + return VStack(12, + Heading("Mount effect"), + TextBlock(status), + Caption("This effect runs once after the first render.")); + } +} + diff --git a/samples/scenarios/hooks/use-effect-mount/scenario.json b/samples/scenarios/hooks/use-effect-mount/scenario.json new file mode 100644 index 000000000..7778b9da4 --- /dev/null +++ b/samples/scenarios/hooks/use-effect-mount/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-effect-mount", + "category": "hooks", + "title": "Mount-only effect", + "intent": "run an effect once after mount by passing an empty dependency array", + "tags": ["effect", "mount", "once", "lifecycle"], + "factoryAnchors": ["UseEffect", "VStack", "TextBlock"], + "notesKey": "UseEffect", + "relatedIds": ["use-effect-cleanup", "use-effect-deps"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-memo/Scenario.cs b/samples/scenarios/hooks/use-memo/Scenario.cs new file mode 100644 index 000000000..0168e1729 --- /dev/null +++ b/samples/scenarios/hooks/use-memo/Scenario.cs @@ -0,0 +1,33 @@ +// id: use-memo +// intent: memoize derived filtered data so it recomputes only when inputs change +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using System.Linq; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// UseMemo is a good fit for derived values that are expensive or noisy to rebuild. +ReactorApp.Run("UseMemo", width: 400, height: 200); + +class App : Component +{ + static readonly string[] Items = ["Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Date"]; + + public override Element Render() + { + var (filter, setFilter) = UseState(""); + var filtered = UseMemo(() => Items + .Where(item => item.Contains(filter, StringComparison.OrdinalIgnoreCase)) + .ToArray(), filter); + + return VStack(12, + TextBox(filter, setFilter, "Filter fruit", header: "Filter"), + Caption($"Visible items: {filtered.Length}"), + ForEach(filtered, item => TextBlock(item))); + } +} + diff --git a/samples/scenarios/hooks/use-memo/scenario.json b/samples/scenarios/hooks/use-memo/scenario.json new file mode 100644 index 000000000..9d025937c --- /dev/null +++ b/samples/scenarios/hooks/use-memo/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-memo", + "category": "hooks", + "title": "Derived state with UseMemo", + "intent": "memoize a filtered view of data so derived state only recomputes when inputs change", + "tags": ["memo", "derived", "computed", "filter", "performance"], + "factoryAnchors": ["UseMemo", "UseState", "TextBox", "ForEach"], + "notesKey": "UseMemo", + "relatedIds": ["use-effect-deps"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-reducer-list/Scenario.cs b/samples/scenarios/hooks/use-reducer-list/Scenario.cs new file mode 100644 index 000000000..f0bd85235 --- /dev/null +++ b/samples/scenarios/hooks/use-reducer-list/Scenario.cs @@ -0,0 +1,51 @@ +// id: use-reducer-list +// intent: manage todo items with immutable add, toggle, and delete reducer actions +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// UseReducer keeps list updates centralized and immutable. +ReactorApp.Run("UseReducerList", width: 400, height: 200); + +class App : Component +{ + record Item(string Id, string Text, bool Done); + abstract record TodoAction; + record AddItem(string Text) : TodoAction; + record ToggleItem(string Id) : TodoAction; + record DeleteItem(string Id) : TodoAction; + + static IReadOnlyList Reduce(IReadOnlyList state, TodoAction action) => action switch + { + AddItem { Text: var text } when !string.IsNullOrWhiteSpace(text) + => [.. state, new Item(Guid.NewGuid().ToString("N"), text.Trim(), false)], + ToggleItem { Id: var id } + => state.Select(item => item.Id == id ? item with { Done = !item.Done } : item).ToArray(), + DeleteItem { Id: var id } + => state.Where(item => item.Id != id).ToArray(), + _ => state + }; + + public override Element Render() + { + var (draft, setDraft) = UseState(""); + var (items, dispatch) = UseReducer, TodoAction>(Reduce, Array.Empty()); + + return VStack(12, + Heading("Todo reducer"), + HStack(8, + TextBox(draft, setDraft, "Add a todo", header: "Todo"), + Button("Add", () => { dispatch(new AddItem(draft)); setDraft(""); })), + ListView(items, item => item.Id, (item, _) => HStack(8, + CheckBox(item.Done, isChecked => dispatch(new ToggleItem(item.Id)), item.Text), + Button("Delete", () => dispatch(new DeleteItem(item.Id)))))); + } +} + diff --git a/samples/scenarios/hooks/use-reducer-list/scenario.json b/samples/scenarios/hooks/use-reducer-list/scenario.json new file mode 100644 index 000000000..5e466d639 --- /dev/null +++ b/samples/scenarios/hooks/use-reducer-list/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-reducer-list", + "category": "hooks", + "title": "Todo list with UseReducer", + "intent": "manage list add delete and toggle operations through an immutable reducer", + "tags": ["reducer", "list", "add", "delete", "toggle", "immutable"], + "factoryAnchors": ["UseReducer", "ListView", "Button", "TextBox"], + "notesKey": "UseReducer", + "relatedIds": ["use-state-list-pitfall"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-reducer-typed/Scenario.cs b/samples/scenarios/hooks/use-reducer-typed/Scenario.cs new file mode 100644 index 000000000..3aa37f052 --- /dev/null +++ b/samples/scenarios/hooks/use-reducer-typed/Scenario.cs @@ -0,0 +1,43 @@ +// id: use-reducer-typed +// intent: strongly-typed reducer actions with pattern matching +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Typed action records make reducer flows explicit and easy to extend. +ReactorApp.Run("UseReducerTyped", width: 400, height: 200); + +class App : Component +{ + abstract record CounterAction; + record Increment(int Amount) : CounterAction; + record Decrement(int Amount) : CounterAction; + record Reset() : CounterAction; + record SetValue(int Value) : CounterAction; + + static int Reduce(int state, CounterAction action) => action switch + { + Increment(var amount) => state + amount, + Decrement(var amount) => state - amount, + Reset => 0, + SetValue(var value) => value, + _ => state + }; + + public override Element Render() + { + var (count, dispatch) = UseReducer(Reduce, 0); + + return VStack(12, + Heading($"Count: {count}"), + HStack(8, + Button("+1", () => dispatch(new Increment(1))), + Button("-1", () => dispatch(new Decrement(1))), + Button("Set 10", () => dispatch(new SetValue(10))), + Button("Reset", () => dispatch(new Reset())))); + } +} + diff --git a/samples/scenarios/hooks/use-reducer-typed/scenario.json b/samples/scenarios/hooks/use-reducer-typed/scenario.json new file mode 100644 index 000000000..4d016dabe --- /dev/null +++ b/samples/scenarios/hooks/use-reducer-typed/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-reducer-typed", + "category": "hooks", + "title": "Typed reducer actions", + "intent": "use strongly typed action records and pattern matching in a reducer", + "tags": ["reducer", "typed-actions", "dispatch", "pattern-matching"], + "factoryAnchors": ["UseReducer", "Button", "VStack"], + "notesKey": "UseReducer", + "relatedIds": ["use-state-basic"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-reducer-with-context/Scenario.cs b/samples/scenarios/hooks/use-reducer-with-context/Scenario.cs new file mode 100644 index 000000000..d3e6f82ea --- /dev/null +++ b/samples/scenarios/hooks/use-reducer-with-context/Scenario.cs @@ -0,0 +1,60 @@ +// id: use-reducer-with-context +// intent: combine reducer state and context to model global app state +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Provide both state and dispatch so nested components can read and update shared state. +ReactorApp.Run("UseReducerWithContext", width: 400, height: 200); + +class App : Component +{ + record CounterState(int Count); + abstract record CounterAction; + record Increment() : CounterAction; + record Decrement() : CounterAction; + + static readonly Context StateContext = new(new CounterState(0)); + static readonly Context> DispatchContext = new(_ => { }); + + static CounterState Reduce(CounterState state, CounterAction action) => action switch + { + Increment => state with { Count = state.Count + 1 }, + Decrement => state with { Count = state.Count - 1 }, + _ => state + }; + + public override Element Render() + { + var (state, dispatch) = UseReducer(Reduce, new CounterState(0)); + + return VStack(12, + Component(), + Component()) + .Provide(StateContext, state) + .Provide(DispatchContext, dispatch); + } + + class CounterValue : Component + { + public override Element Render() + { + var state = UseContext(StateContext); + return Heading($"Global count: {state.Count}"); + } + } + + class CounterButtons : Component + { + public override Element Render() + { + var dispatch = UseContext(DispatchContext); + return HStack(8, + Button("-1", () => dispatch(new Decrement())), + Button("+1", () => dispatch(new Increment()))); + } + } +} diff --git a/samples/scenarios/hooks/use-reducer-with-context/scenario.json b/samples/scenarios/hooks/use-reducer-with-context/scenario.json new file mode 100644 index 000000000..940b755ec --- /dev/null +++ b/samples/scenarios/hooks/use-reducer-with-context/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-reducer-with-context", + "category": "hooks", + "title": "Reducer with shared context", + "intent": "combine reducer state and context so nested components can dispatch global actions", + "tags": ["reducer", "context", "global", "dispatch", "pattern"], + "factoryAnchors": ["UseReducer", "UseContext", "Provider"], + "notesKey": "UseContext", + "relatedIds": ["use-context-basic", "use-reducer-list"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-ref-dom/Scenario.cs b/samples/scenarios/hooks/use-ref-dom/Scenario.cs new file mode 100644 index 000000000..8fff6955a --- /dev/null +++ b/samples/scenarios/hooks/use-ref-dom/Scenario.cs @@ -0,0 +1,43 @@ +// id: use-ref-dom +// intent: hand a mounted element reference to native APIs for focus and measurement +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Controls; +using Microsoft.UI.Reactor.Core; +using Microsoft.UI.Reactor.Hooks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using static Microsoft.UI.Reactor.Factories; + +// Pair a stable ref with a mount effect when native APIs need the real control instance. +ReactorApp.Run("UseRefDom", width: 400, height: 200); + +class App : Component +{ + public override Element Render() + { + var (text, setText) = UseState(""); + var (message, setMessage) = UseState("Waiting for mount..."); + var focusAttempts = UseRef(0); + var inputRef = this.UseElementRef(); + + UseEffect(() => + { + focusAttempts.Current++; + if (inputRef.Current is { } box) + { + box.Focus(FocusState.Programmatic); + setMessage($"Focus requested. ActualWidth = {box.ActualWidth:F0}px"); + } + }, Array.Empty()); + + return VStack(12, + TextBox(text, setText, "Focused on mount", header: "Focusable input").Width(240).Ref(inputRef), + Caption(message), + Caption($"Native ref uses: {focusAttempts.Current}")); + } +} + diff --git a/samples/scenarios/hooks/use-ref-dom/scenario.json b/samples/scenarios/hooks/use-ref-dom/scenario.json new file mode 100644 index 000000000..9608d110a --- /dev/null +++ b/samples/scenarios/hooks/use-ref-dom/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-ref-dom", + "category": "hooks", + "title": "Native element ref", + "intent": "hand a mounted element reference to native APIs for focus and measurement", + "tags": ["ref", "dom", "element", "focus", "native"], + "factoryAnchors": ["UseRef", "UseEffect", "TextBox"], + "notesKey": "UseRef", + "relatedIds": ["use-ref-mutable"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-ref-mutable/Scenario.cs b/samples/scenarios/hooks/use-ref-mutable/Scenario.cs new file mode 100644 index 000000000..794138f52 --- /dev/null +++ b/samples/scenarios/hooks/use-ref-mutable/Scenario.cs @@ -0,0 +1,31 @@ +// id: use-ref-mutable +// intent: store previous state in a mutable ref without causing re-renders +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Refs keep mutable values across renders without participating in diffing. +ReactorApp.Run("UseRefMutable", width: 400, height: 200); + +class App : Component +{ + public override Element Render() + { + var (count, setCount) = UseState(0); + var previous = UseRef(null); + + UseEffect(() => + { + previous.Current = count; + }, count); + + return VStack(12, + Heading($"Current: {count}"), + TextBlock($"Previous: {(previous.Current is int value ? value : -1)}"), + Button("+1", () => setCount(count + 1))); + } +} + diff --git a/samples/scenarios/hooks/use-ref-mutable/scenario.json b/samples/scenarios/hooks/use-ref-mutable/scenario.json new file mode 100644 index 000000000..1c72ed6ea --- /dev/null +++ b/samples/scenarios/hooks/use-ref-mutable/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-ref-mutable", + "category": "hooks", + "title": "Mutable ref storage", + "intent": "store a previous value in a ref so it persists without triggering re-renders", + "tags": ["ref", "mutable", "previous", "persist"], + "factoryAnchors": ["UseRef", "UseState", "UseEffect", "VStack"], + "notesKey": "UseRef", + "relatedIds": ["use-ref-dom"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-state-basic/Scenario.cs b/samples/scenarios/hooks/use-state-basic/Scenario.cs new file mode 100644 index 000000000..8623c9f12 --- /dev/null +++ b/samples/scenarios/hooks/use-state-basic/Scenario.cs @@ -0,0 +1,20 @@ +// id: use-state-basic +// intent: count clicks; demonstrate UseState with primitive value +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("UseStateBasic", width: 400, height: 200); + +class App : Component +{ + public override Element Render() + { + var (count, setCount) = UseState(0); + return VStack( + Heading($"Count: {count}"), + Button("+1", () => setCount(count + 1))); + } +} diff --git a/samples/scenarios/hooks/use-state-basic/scenario.json b/samples/scenarios/hooks/use-state-basic/scenario.json new file mode 100644 index 000000000..84b442882 --- /dev/null +++ b/samples/scenarios/hooks/use-state-basic/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-state-basic", + "category": "hooks", + "title": "Counter with UseState", + "intent": "increment a primitive value on click", + "tags": ["state", "counter", "hook", "useState", "primitive"], + "factoryAnchors": ["UseState", "Button", "VStack"], + "notesKey": "UseState", + "relatedIds": ["use-state-list-pitfall", "use-reducer-list"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-state-list-pitfall/Scenario.cs b/samples/scenarios/hooks/use-state-list-pitfall/Scenario.cs new file mode 100644 index 000000000..558434837 --- /dev/null +++ b/samples/scenarios/hooks/use-state-list-pitfall/Scenario.cs @@ -0,0 +1,34 @@ +// id: use-state-list-pitfall +// intent: show why mutating List in place is a UseState anti-pattern +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System.Collections.Generic; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Mutating the same List instance changes data but not state identity, so no re-render happens. +ReactorApp.Run("UseStateListPitfall", width: 400, height: 200); + +class App : Component +{ + public override Element Render() + { + var (items, _) = UseState(new List { "Alpha" }); + var (rerenders, setRerenders) = UseState(0); + + return VStack(12, + Heading($"Visible count: {items.Count}"), + Caption($"Unrelated renders: {rerenders}"), + Button("Mutate list in place (wrong)", () => + { + // Wrong: UseState still holds the same List reference, so Reactor + // does not detect a new state value and the UI stays stale. + items.Add($"Item {items.Count + 1}"); + }), + Button("Force unrelated re-render", () => setRerenders(rerenders + 1)), + ForEach(items, item => TextBlock(item))); + } +} + diff --git a/samples/scenarios/hooks/use-state-list-pitfall/scenario.json b/samples/scenarios/hooks/use-state-list-pitfall/scenario.json new file mode 100644 index 000000000..84cbc6906 --- /dev/null +++ b/samples/scenarios/hooks/use-state-list-pitfall/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-state-list-pitfall", + "category": "hooks", + "title": "List mutation anti-pattern", + "intent": "demonstrate why mutating a List in place does not trigger a UseState re-render", + "tags": ["state", "list", "anti-pattern", "pitfall"], + "factoryAnchors": ["UseState", "Button", "VStack"], + "notesKey": "UseState", + "relatedIds": ["use-reducer-list"], + "priority": "P0" +} diff --git a/samples/scenarios/hooks/use-state-record/Scenario.cs b/samples/scenarios/hooks/use-state-record/Scenario.cs new file mode 100644 index 000000000..2fe3d52f4 --- /dev/null +++ b/samples/scenarios/hooks/use-state-record/Scenario.cs @@ -0,0 +1,39 @@ +// id: use-state-record +// intent: record-shaped state with structural equality and with-expression updates +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +// Immutable record state stays easy to compare and update with `with` expressions. +ReactorApp.Run("UseStateRecord", width: 400, height: 200); + +class App : Component +{ + record Settings(bool DarkMode, int FontSize); + + public override Element Render() + { + var (settings, setSettings) = UseState(new Settings(false, 14)); + + return VStack(12, + Heading(settings.DarkMode ? "Dark mode" : "Light mode"), + ToggleSwitch( + settings.DarkMode, + isOn => setSettings(settings with { DarkMode = isOn }), + onContent: "On", + offContent: "Off", + header: "Theme"), + TextBlock($"Font size: {settings.FontSize}"), + Slider( + settings.FontSize, + min: 12, + max: 24, + onValueChanged: value => setSettings(settings with { FontSize = (int)Math.Round(value) })), + TextBlock("Preview text").FontSize(settings.FontSize)); + } +} + diff --git a/samples/scenarios/hooks/use-state-record/scenario.json b/samples/scenarios/hooks/use-state-record/scenario.json new file mode 100644 index 000000000..c982d428f --- /dev/null +++ b/samples/scenarios/hooks/use-state-record/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "use-state-record", + "category": "hooks", + "title": "Record state with UseState", + "intent": "update immutable record state with structural equality and with expressions", + "tags": ["state", "record", "immutable", "with-expression"], + "factoryAnchors": ["UseState", "ToggleSwitch", "Slider"], + "notesKey": "UseState", + "relatedIds": ["use-state-basic"], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/autosuggestbox-typeahead/Scenario.cs b/samples/scenarios/inputs/autosuggestbox-typeahead/Scenario.cs new file mode 100644 index 000000000..d0648dbc5 --- /dev/null +++ b/samples/scenarios/inputs/autosuggestbox-typeahead/Scenario.cs @@ -0,0 +1,28 @@ +// id: autosuggestbox-typeahead +// intent: search box with typeahead suggestions +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using System.Linq; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("AutoSuggestBox Typeahead", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var allItems = new[] { "Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Grape" }; + var (query, setQuery) = UseState(""); + var matches = allItems.Where(x => x.Contains(query, StringComparison.OrdinalIgnoreCase)).Take(5).ToArray(); + return VStack(12, + Heading("AutoSuggestBox"), + (AutoSuggestBox(query, setQuery) with { Header = "Fruit search", PlaceholderText = "Start typing", Suggestions = matches, IsSuggestionListOpen = query.Length > 0 && matches.Length > 0 }) + .AutomationName("Fruit search"), + TextBlock($"Current value: {query}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/autosuggestbox-typeahead/scenario.json b/samples/scenarios/inputs/autosuggestbox-typeahead/scenario.json new file mode 100644 index 000000000..96a552abe --- /dev/null +++ b/samples/scenarios/inputs/autosuggestbox-typeahead/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "autosuggestbox-typeahead", + "category": "inputs", + "title": "AutoSuggestBox Typeahead", + "intent": "search box with typeahead suggestions filtered from user input", + "tags": ["autosuggest", "typeahead", "search", "filter"], + "factoryAnchors": ["AutoSuggestBox", "UseState"], + "notesKey": "AutoSuggestBox", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/calendar-multiselect/Scenario.cs b/samples/scenarios/inputs/calendar-multiselect/Scenario.cs new file mode 100644 index 000000000..547608651 --- /dev/null +++ b/samples/scenarios/inputs/calendar-multiselect/Scenario.cs @@ -0,0 +1,33 @@ +// id: calendar-multiselect +// intent: select multiple dates in a calendar and summarize the chosen days +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using System.Linq; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using Microsoft.UI.Xaml.Controls; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Calendar Multi-select", width: 520, height: 420); + +class App : Component +{ + public override Element Render() + { + var (dates, setDates) = UseState>(Array.Empty()); + + return VStack(16, + Subtitle("Pick travel days"), + (CalendarView() with { SelectionMode = CalendarViewSelectionMode.Multiple }) + .SelectedDates(dates) + .SelectedDatesChanged(setDates), + Body(dates.Count == 0 + ? "No dates selected." + : $"{dates.Count} selected: {string.Join(", ", dates.Select(d => d.ToString("MMM d")))}"), + Button("Clear", () => setDates(Array.Empty())) + .SubtleButton()) + .Padding(24); + } +} diff --git a/samples/scenarios/inputs/calendar-multiselect/scenario.json b/samples/scenarios/inputs/calendar-multiselect/scenario.json new file mode 100644 index 000000000..d9b5c6b85 --- /dev/null +++ b/samples/scenarios/inputs/calendar-multiselect/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "calendar-multiselect", + "category": "inputs", + "title": "Calendar Multi-select", + "intent": "select multiple dates in a CalendarView and summarize the chosen days", + "tags": ["calendar", "multi-select", "dates", "selection"], + "factoryAnchors": ["CalendarView", "UseState"], + "notesKey": "CalendarView", + "relatedIds": ["calendardatepicker"], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/calendardatepicker/Scenario.cs b/samples/scenarios/inputs/calendardatepicker/Scenario.cs new file mode 100644 index 000000000..f5c9b79d7 --- /dev/null +++ b/samples/scenarios/inputs/calendardatepicker/Scenario.cs @@ -0,0 +1,24 @@ +// id: calendardatepicker +// intent: date selection via calendar popup +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("CalendarDatePicker", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (date, setDate) = UseState(DateTimeOffset.Now); + return VStack(12, + Heading("CalendarDatePicker"), + (CalendarDatePicker(date, setDate) with { Header = "Start date", DateFormat = "{month.full} {day.integer}, {year.full}" }), + TextBlock($"Selected: {(date is null ? "None" : date.Value.ToString("D"))}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/calendardatepicker/scenario.json b/samples/scenarios/inputs/calendardatepicker/scenario.json new file mode 100644 index 000000000..902734d35 --- /dev/null +++ b/samples/scenarios/inputs/calendardatepicker/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "calendardatepicker", + "category": "inputs", + "title": "CalendarDatePicker Selection", + "intent": "date selection via calendar popup with current date display", + "tags": ["calendar", "date", "picker", "datepicker"], + "factoryAnchors": ["CalendarDatePicker", "UseState"], + "notesKey": "CalendarDatePicker", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/checkbox-bool/Scenario.cs b/samples/scenarios/inputs/checkbox-bool/Scenario.cs new file mode 100644 index 000000000..e98533655 --- /dev/null +++ b/samples/scenarios/inputs/checkbox-bool/Scenario.cs @@ -0,0 +1,23 @@ +// id: checkbox-bool +// intent: checkbox bound to boolean state +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Checkbox Boolean", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (accepted, setAccepted) = UseState(false); + return VStack(12, + Heading("CheckBox"), + CheckBox(accepted, setAccepted, "Accept terms"), + TextBlock($"Accepted: {accepted}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/checkbox-bool/scenario.json b/samples/scenarios/inputs/checkbox-bool/scenario.json new file mode 100644 index 000000000..063909989 --- /dev/null +++ b/samples/scenarios/inputs/checkbox-bool/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "checkbox-bool", + "category": "inputs", + "title": "Checkbox Boolean State", + "intent": "checkbox bound to boolean state with text feedback", + "tags": ["checkbox", "boolean", "toggle"], + "factoryAnchors": ["CheckBox", "UseState"], + "notesKey": "CheckBox", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/combobox-from-list/Scenario.cs b/samples/scenarios/inputs/combobox-from-list/Scenario.cs new file mode 100644 index 000000000..84291f509 --- /dev/null +++ b/samples/scenarios/inputs/combobox-from-list/Scenario.cs @@ -0,0 +1,24 @@ +// id: combobox-from-list +// intent: dropdown picker from a string array +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("ComboBox List", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var cities = new[] { "Seattle", "London", "Tokyo", "Sydney" }; + var (selectedIndex, setSelectedIndex) = UseState(1); + return VStack(12, + Heading("ComboBox"), + (ComboBox(cities, selectedIndex, setSelectedIndex) with { Header = "Office", PlaceholderText = "Choose a city" }), + TextBlock($"Selected: {cities[selectedIndex]}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/combobox-from-list/scenario.json b/samples/scenarios/inputs/combobox-from-list/scenario.json new file mode 100644 index 000000000..1d9b3be28 --- /dev/null +++ b/samples/scenarios/inputs/combobox-from-list/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "combobox-from-list", + "category": "inputs", + "title": "ComboBox from String List", + "intent": "dropdown picker from a string array with selected item display", + "tags": ["combobox", "dropdown", "select", "picker"], + "factoryAnchors": ["ComboBox", "UseState"], + "notesKey": "ComboBox", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/combobox-of-elements/Scenario.cs b/samples/scenarios/inputs/combobox-of-elements/Scenario.cs new file mode 100644 index 000000000..4d54ff9c1 --- /dev/null +++ b/samples/scenarios/inputs/combobox-of-elements/Scenario.cs @@ -0,0 +1,25 @@ +// id: combobox-of-elements +// intent: dropdown with custom element items +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("ComboBox Elements", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var labels = new[] { "Low", "Medium", "High" }; + var items = new Element[] { TextBlock("🟢 Low"), TextBlock("🟡 Medium"), TextBlock("🔴 High") }; + var (selectedIndex, setSelectedIndex) = UseState(0); + return VStack(12, + Heading("ComboBox with Elements"), + (ComboBox(items, selectedIndex, setSelectedIndex) with { Header = "Priority" }), + TextBlock($"Selected: {labels[selectedIndex]}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/combobox-of-elements/scenario.json b/samples/scenarios/inputs/combobox-of-elements/scenario.json new file mode 100644 index 000000000..b1989ce26 --- /dev/null +++ b/samples/scenarios/inputs/combobox-of-elements/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "combobox-of-elements", + "category": "inputs", + "title": "ComboBox of Elements", + "intent": "dropdown with custom element items and selected value display", + "tags": ["combobox", "dropdown", "elements", "custom"], + "factoryAnchors": ["ComboBox", "UseState"], + "notesKey": "ComboBox", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/numberbox-validated/Scenario.cs b/samples/scenarios/inputs/numberbox-validated/Scenario.cs new file mode 100644 index 000000000..a75c7dfbf --- /dev/null +++ b/samples/scenarios/inputs/numberbox-validated/Scenario.cs @@ -0,0 +1,24 @@ +// id: numberbox-validated +// intent: number input with validation constraints +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("NumberBox Validation", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (age, setAge) = UseState(21.0); + return VStack(12, + Heading("NumberBox"), + (NumberBox(age, setAge) with { Header = "Age", Minimum = 0, Maximum = 120, Description = "Allowed range: 0 to 120" }) + .AutomationName("Age"), + TextBlock($"Current value: {age:0}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/numberbox-validated/scenario.json b/samples/scenarios/inputs/numberbox-validated/scenario.json new file mode 100644 index 000000000..e1580f90c --- /dev/null +++ b/samples/scenarios/inputs/numberbox-validated/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "numberbox-validated", + "category": "inputs", + "title": "NumberBox Validation", + "intent": "number input with validation constraints and current value display", + "tags": ["numberbox", "number", "input", "validation"], + "factoryAnchors": ["NumberBox", "UseState"], + "notesKey": "NumberBox", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/radiobuttons-group/Scenario.cs b/samples/scenarios/inputs/radiobuttons-group/Scenario.cs new file mode 100644 index 000000000..e0f020ab1 --- /dev/null +++ b/samples/scenarios/inputs/radiobuttons-group/Scenario.cs @@ -0,0 +1,24 @@ +// id: radiobuttons-group +// intent: radio button group for single selection +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("RadioButtons Group", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var items = new[] { "System", "Light", "Dark" }; + var (selectedIndex, setSelectedIndex) = UseState(0); + return VStack(12, + Heading("RadioButtons"), + RadioButtons(items, selectedIndex, setSelectedIndex) with { Header = "Theme" }, + TextBlock($"Selected: {items[selectedIndex]}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/radiobuttons-group/scenario.json b/samples/scenarios/inputs/radiobuttons-group/scenario.json new file mode 100644 index 000000000..58a2882f8 --- /dev/null +++ b/samples/scenarios/inputs/radiobuttons-group/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "radiobuttons-group", + "category": "inputs", + "title": "RadioButtons Group Selection", + "intent": "radio button group for single selection with selected index tracking", + "tags": ["radio", "radiobuttons", "selection", "group"], + "factoryAnchors": ["RadioButtons", "UseState"], + "notesKey": "RadioButtons", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/slider-range/Scenario.cs b/samples/scenarios/inputs/slider-range/Scenario.cs new file mode 100644 index 000000000..18bce3182 --- /dev/null +++ b/samples/scenarios/inputs/slider-range/Scenario.cs @@ -0,0 +1,23 @@ +// id: slider-range +// intent: slider for numeric range selection +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Slider Range", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (volume, setVolume) = UseState(35.0); + return VStack(12, + Heading("Slider"), + (Slider(volume, 0, 100, setVolume) with { Header = "Volume", StepFrequency = 5, TickFrequency = 10 }), + TextBlock($"Current value: {volume:0}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/slider-range/scenario.json b/samples/scenarios/inputs/slider-range/scenario.json new file mode 100644 index 000000000..9b917baed --- /dev/null +++ b/samples/scenarios/inputs/slider-range/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "slider-range", + "category": "inputs", + "title": "Slider Range Selection", + "intent": "slider for numeric range selection with live value display", + "tags": ["slider", "range", "numeric"], + "factoryAnchors": ["Slider", "UseState"], + "notesKey": "Slider", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/textfield-twoway/Scenario.cs b/samples/scenarios/inputs/textfield-twoway/Scenario.cs new file mode 100644 index 000000000..96fcdc03a --- /dev/null +++ b/samples/scenarios/inputs/textfield-twoway/Scenario.cs @@ -0,0 +1,23 @@ +// id: textfield-twoway +// intent: two-way text input binding +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("TextField Two-Way", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (name, setName) = UseState("Reactor"); + return VStack(12, + Heading("TextField"), + (TextField(name, setName, placeholder: "Type a name") with { Header = "Display name", MaxLength = 24 }), + TextBlock($"Current value: {name}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/textfield-twoway/scenario.json b/samples/scenarios/inputs/textfield-twoway/scenario.json new file mode 100644 index 000000000..c236c5ec4 --- /dev/null +++ b/samples/scenarios/inputs/textfield-twoway/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "textfield-twoway", + "category": "inputs", + "title": "TextField Two-Way Binding", + "intent": "two-way text input binding with UseState and immediate display updates", + "tags": ["textfield", "input", "twoway", "binding", "text"], + "factoryAnchors": ["TextField", "UseState"], + "notesKey": "TextField", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/inputs/toggleswitch/Scenario.cs b/samples/scenarios/inputs/toggleswitch/Scenario.cs new file mode 100644 index 000000000..5350e00b7 --- /dev/null +++ b/samples/scenarios/inputs/toggleswitch/Scenario.cs @@ -0,0 +1,23 @@ +// id: toggleswitch +// intent: toggle switch for on/off settings +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Toggle Switch", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var (wifiEnabled, setWifiEnabled) = UseState(true); + return VStack(12, + Heading("ToggleSwitch"), + (ToggleSwitch(wifiEnabled, setWifiEnabled, "On", "Off") with { Header = "Wi-Fi" }), + TextBlock($"Wi-Fi is {(wifiEnabled ? "On" : "Off")}")) + .Margin(16); + } +} diff --git a/samples/scenarios/inputs/toggleswitch/scenario.json b/samples/scenarios/inputs/toggleswitch/scenario.json new file mode 100644 index 000000000..ae687f9ce --- /dev/null +++ b/samples/scenarios/inputs/toggleswitch/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "toggleswitch", + "category": "inputs", + "title": "ToggleSwitch Settings Toggle", + "intent": "toggle switch for on off settings state", + "tags": ["toggle", "switch", "setting", "onoff"], + "factoryAnchors": ["ToggleSwitch", "UseState"], + "notesKey": "ToggleSwitch", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/layout/border-with-corner/Scenario.cs b/samples/scenarios/layout/border-with-corner/Scenario.cs new file mode 100644 index 000000000..c52e4e31b --- /dev/null +++ b/samples/scenarios/layout/border-with-corner/Scenario.cs @@ -0,0 +1,31 @@ +// id: border-with-corner +// intent: padded border with corner radius +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Border With Corner", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return Border( + Border( + VStack(8, + Subtitle("Pinned note"), + TextBlock("Border, padding, and corner radius create a clear content container.") + .Foreground(Theme.SecondaryText), + Caption("Updated just now").Foreground(Theme.TertiaryText)) + .Padding(16)) + .Background(Theme.CardBackground) + .WithBorder(Theme.CardStroke, 1) + .CornerRadius(14) + .Padding(4) + ) + .Padding(24) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/border-with-corner/scenario.json b/samples/scenarios/layout/border-with-corner/scenario.json new file mode 100644 index 000000000..a7206042b --- /dev/null +++ b/samples/scenarios/layout/border-with-corner/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "border-with-corner", + "category": "layout", + "title": "Border With Corner Radius", + "intent": "bordered content area with padding, rounded corners, and theme-aware surface colors", + "tags": ["border", "padding", "corner-radius", "themed"], + "factoryAnchors": ["Border"], + "notesKey": "Border", + "relatedIds": ["card-surface"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/canvas-positioning/Scenario.cs b/samples/scenarios/layout/canvas-positioning/Scenario.cs new file mode 100644 index 000000000..79eae8681 --- /dev/null +++ b/samples/scenarios/layout/canvas-positioning/Scenario.cs @@ -0,0 +1,39 @@ +// id: canvas-positioning +// intent: absolute positioning with Canvas and .Canvas(left, top) +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Canvas Positioning", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var accent = ThemeResource.Brush("AccentFillColorDefaultBrush"); + var stroke = ThemeResource.Brush("CardStrokeColorDefaultBrush"); + + return Border( + Canvas( + Line(72, 48, 180, 108).Stroke(stroke).StrokeThickness(2), + Border(TextBlock("A").Padding(8)) + .Background(Theme.CardBackground) + .WithBorder(Theme.CardStroke, 1) + .CornerRadius(8) + .Canvas(left: 48, top: 32), + Ellipse().Width(24).Height(24).Fill(accent).CenterAt(x: 180, y: 108), + Line(192, 120, 280, 160).Stroke(accent).StrokeThickness(2), + Border(TextBlock("Focus").Padding(8)) + .Background(Theme.LayerFill) + .WithBorder(Theme.CardStroke, 1) + .CornerRadius(8) + .Canvas(left: 280, top: 160)) + .Width(340) + .Height(220) + ) + .Padding(20) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/canvas-positioning/scenario.json b/samples/scenarios/layout/canvas-positioning/scenario.json new file mode 100644 index 000000000..ae5e7d115 --- /dev/null +++ b/samples/scenarios/layout/canvas-positioning/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "canvas-positioning", + "category": "layout", + "title": "Canvas Positioning", + "intent": "absolute layout with positioned elements, connector lines, and centered shapes", + "tags": ["canvas", "absolute", "position", "shapes"], + "factoryAnchors": ["Canvas"], + "notesKey": "Canvas", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/layout/card-surface/Scenario.cs b/samples/scenarios/layout/card-surface/Scenario.cs new file mode 100644 index 000000000..cde6d59ee --- /dev/null +++ b/samples/scenarios/layout/card-surface/Scenario.cs @@ -0,0 +1,28 @@ +// id: card-surface +// intent: themed card surface following Win11 design +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Core.Theme; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Card Surface", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return Border( + Card( + VStack(12, + Subtitle("Sprint planning"), + TextBlock("Card() applies the canonical WinUI surface, stroke, radius, and padding.") + .Foreground(SecondaryText), + Button("Open board"))) + .Width(280) + ) + .Padding(24) + .Background(SolidBackground); + } +} diff --git a/samples/scenarios/layout/card-surface/scenario.json b/samples/scenarios/layout/card-surface/scenario.json new file mode 100644 index 000000000..3a85189ef --- /dev/null +++ b/samples/scenarios/layout/card-surface/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "card-surface", + "category": "layout", + "title": "Card Surface", + "intent": "theme-aware card surface with title, body copy, and an action button", + "tags": ["card", "theme", "surface", "win11", "design"], + "factoryAnchors": ["Card", "Border"], + "notesKey": "Card", + "relatedIds": ["border-with-corner"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/flexcolumn-with-justify/Scenario.cs b/samples/scenarios/layout/flexcolumn-with-justify/Scenario.cs new file mode 100644 index 000000000..47ed7bbb3 --- /dev/null +++ b/samples/scenarios/layout/flexcolumn-with-justify/Scenario.cs @@ -0,0 +1,36 @@ +// id: flexcolumn-with-justify +// intent: flexbox column with alignment and justification +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Layout; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("FlexColumn Justify", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return Border( + (FlexColumn( + VStack(4, + Subtitle("Welcome"), + TextBlock("Header content stays at the top.").Foreground(Theme.SecondaryText)), + Border(TextBlock("Centered content").Padding(12)) + .Background(Theme.CardBackground) + .WithBorder(Theme.CardStroke, 1) + .CornerRadius(8), + Caption("Footer status or actions can sit at the bottom.") + .Foreground(Theme.SecondaryText)) with + { + JustifyContent = FlexJustify.SpaceBetween, + AlignItems = FlexAlign.Center, + }) + .Height(220) + .FlexPadding(20) + ) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/flexcolumn-with-justify/scenario.json b/samples/scenarios/layout/flexcolumn-with-justify/scenario.json new file mode 100644 index 000000000..38f3ac6de --- /dev/null +++ b/samples/scenarios/layout/flexcolumn-with-justify/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "flexcolumn-with-justify", + "category": "layout", + "title": "Flex Column With Justify", + "intent": "full-height column with aligned items spaced across the available height", + "tags": ["flex", "flexcolumn", "justify", "align", "page-layout"], + "factoryAnchors": ["FlexColumn"], + "notesKey": "FlexColumn", + "relatedIds": ["flexrow-with-grow"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/flexrow-with-grow/Scenario.cs b/samples/scenarios/layout/flexrow-with-grow/Scenario.cs new file mode 100644 index 000000000..3481eaf30 --- /dev/null +++ b/samples/scenarios/layout/flexrow-with-grow/Scenario.cs @@ -0,0 +1,30 @@ +// id: flexrow-with-grow +// intent: CSS-flexbox row with one child growing to fill space +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Layout; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("FlexRow Grow", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return Border( + (FlexRow( + Subtitle("Files"), + TextBlock("").Flex(grow: 1), + Button("Refresh"), + Button("Share")) with + { + AlignItems = FlexAlign.Center, + ColumnGap = 8, + }) + .Padding(16) + ) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/flexrow-with-grow/scenario.json b/samples/scenarios/layout/flexrow-with-grow/scenario.json new file mode 100644 index 000000000..9b9024fd2 --- /dev/null +++ b/samples/scenarios/layout/flexrow-with-grow/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "flexrow-with-grow", + "category": "layout", + "title": "Flex Row With Grow", + "intent": "toolbar layout with a growing spacer that pushes actions to the end", + "tags": ["flex", "flexrow", "grow", "toolbar", "spacer"], + "factoryAnchors": ["FlexRow"], + "notesKey": "FlexRow", + "relatedIds": ["flexcolumn-with-justify"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/grid-basic/Scenario.cs b/samples/scenarios/layout/grid-basic/Scenario.cs new file mode 100644 index 000000000..faeff88aa --- /dev/null +++ b/samples/scenarios/layout/grid-basic/Scenario.cs @@ -0,0 +1,36 @@ +// id: grid-basic +// intent: 2D grid with column and row sizes +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Grid Basic", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return Border( + (Grid( + new[] { GridSize.Auto, GridSize.Star() }, + new[] { GridSize.Auto, GridSize.Auto, GridSize.Star() }, + TextBlock("Name").Grid(row: 0, column: 0).Foreground(Theme.SecondaryText), + TextField("Ada Lovelace", _ => { }).Grid(row: 0, column: 1), + TextBlock("Team").Grid(row: 1, column: 0).Foreground(Theme.SecondaryText), + TextField("Layout systems", _ => { }).Grid(row: 1, column: 1), + Border(TextBlock("The bottom row expands because the second column and third row use star sizing.").Padding(12)) + .Background(Theme.CardBackground) + .WithBorder(Theme.CardStroke, 1) + .CornerRadius(8) + .Grid(row: 2, column: 0, columnSpan: 2)) with + { + ColumnSpacing = 12, + RowSpacing = 12, + }) + .Padding(20) + ) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/grid-basic/scenario.json b/samples/scenarios/layout/grid-basic/scenario.json new file mode 100644 index 000000000..9bacf4e8d --- /dev/null +++ b/samples/scenarios/layout/grid-basic/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "grid-basic", + "category": "layout", + "title": "Basic Grid Layout", + "intent": "two-column grid using auto and star tracks with explicit child placement", + "tags": ["grid", "columns", "rows", "sizing", "placement"], + "factoryAnchors": ["Grid"], + "notesKey": "Grid", + "relatedIds": ["grid-spans"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/grid-spans/Scenario.cs b/samples/scenarios/layout/grid-spans/Scenario.cs new file mode 100644 index 000000000..d3c693e57 --- /dev/null +++ b/samples/scenarios/layout/grid-spans/Scenario.cs @@ -0,0 +1,37 @@ +// id: grid-spans +// intent: grid with row and column spans +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Grid Spans", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return Border( + (Grid( + new[] { GridSize.Star(2), GridSize.Star(), GridSize.Star() }, + new[] { GridSize.Auto, GridSize.Star() }, + Card(VStack(8, + Subtitle("Overview"), + TextBlock("This tile spans two columns across the top.").Foreground(Theme.SecondaryText))) + .Grid(row: 0, column: 0, columnSpan: 2), + Card(VStack(8, + Subtitle("Alerts"), + Caption("7 items need attention.").Foreground(Theme.SecondaryText))) + .Grid(row: 0, column: 2, rowSpan: 2), + Card(TextBlock("Traffic")).Grid(row: 1, column: 0), + Card(TextBlock("Tasks")).Grid(row: 1, column: 1)) with + { + ColumnSpacing = 12, + RowSpacing = 12, + }) + .Padding(20) + ) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/grid-spans/scenario.json b/samples/scenarios/layout/grid-spans/scenario.json new file mode 100644 index 000000000..fde66aadd --- /dev/null +++ b/samples/scenarios/layout/grid-spans/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "grid-spans", + "category": "layout", + "title": "Grid With Spans", + "intent": "dashboard-style grid that uses both row spans and column spans", + "tags": ["grid", "span", "columnspan", "rowspan", "dashboard"], + "factoryAnchors": ["Grid"], + "notesKey": "Grid", + "relatedIds": ["grid-basic"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/hstack-basic/Scenario.cs b/samples/scenarios/layout/hstack-basic/Scenario.cs new file mode 100644 index 000000000..720ae016f --- /dev/null +++ b/samples/scenarios/layout/hstack-basic/Scenario.cs @@ -0,0 +1,24 @@ +// id: hstack-basic +// intent: horizontal shrink-wrap stack with spacing +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("HStack Basic", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return Border( + HStack(8, + TextBlock("Name").Width(48), + TextField("Ada", _ => { }, placeholder: "Display name").Width(180), + Button("Save")) + .Padding(20) + ) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/hstack-basic/scenario.json b/samples/scenarios/layout/hstack-basic/scenario.json new file mode 100644 index 000000000..a7ba4d27d --- /dev/null +++ b/samples/scenarios/layout/hstack-basic/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "hstack-basic", + "category": "layout", + "title": "Horizontal Stack Basics", + "intent": "horizontal stack for a label, input, and action button", + "tags": ["hstack", "horizontal", "stack", "spacing"], + "factoryAnchors": ["HStack", "TextBlock", "Button"], + "notesKey": "HStack", + "relatedIds": ["vstack-basic"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/named-styles/Scenario.cs b/samples/scenarios/layout/named-styles/Scenario.cs new file mode 100644 index 000000000..cac64ffcb --- /dev/null +++ b/samples/scenarios/layout/named-styles/Scenario.cs @@ -0,0 +1,31 @@ +// id: named-styles +// intent: apply theme-aware named styles to buttons, hyperlinks, and InfoBars +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Named Styles", width: 560, height: 520); + +class App : Component +{ + public override Element Render() => + ScrollView( + VStack(20, + Subtitle("Buttons"), + HStack(8, + Button("Default", () => { }), + Button("Accent", () => { }).AccentButton(), + Button("Subtle", () => { }).SubtleButton(), + Button("Text link", () => { }).TextLink()), + Subtitle("Hyperlinks"), + HyperlinkButton("Open docs").TextLink(), + Subtitle("InfoBars"), + InfoBar("Tip", "You can drag the divider.").Informational(), + InfoBar("Saved", "Changes written to disk.").Success(), + InfoBar("Heads up", "Unsaved changes will be discarded.").Warning(), + InfoBar("Failed", "Couldn't reach the server.").Error()) + .Padding(24)); +} diff --git a/samples/scenarios/layout/named-styles/scenario.json b/samples/scenarios/layout/named-styles/scenario.json new file mode 100644 index 000000000..53697e908 --- /dev/null +++ b/samples/scenarios/layout/named-styles/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "named-styles", + "category": "layout", + "title": "Named Style Fluents", + "intent": "apply theme-aware named styles to buttons, hyperlinks, and InfoBars", + "tags": ["styles", "theme", "button", "hyperlink", "infobar"], + "factoryAnchors": ["Button", "HyperlinkButton", "InfoBar"], + "notesKey": null, + "relatedIds": ["hyperlink-button"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/scrollviewer-vertical/Scenario.cs b/samples/scenarios/layout/scrollviewer-vertical/Scenario.cs new file mode 100644 index 000000000..13e440c2c --- /dev/null +++ b/samples/scenarios/layout/scrollviewer-vertical/Scenario.cs @@ -0,0 +1,36 @@ +// id: scrollviewer-vertical +// intent: scrollable vertical content region +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("ScrollView Vertical", width: 400, height: 300); + +class App : Component +{ + static Element Row(int index) => + Border( + HStack(12, + Subtitle($"Item {index}"), + TextBlock("Scroll to reveal more content.").Foreground(Theme.SecondaryText)) + .Padding(12)) + .Background(Theme.CardBackground) + .WithBorder(Theme.CardStroke, 1) + .CornerRadius(8); + + public override Element Render() + { + return Border( + ScrollView( + VStack(12, + Subtitle("Recent activity"), + Row(1), Row(2), Row(3), Row(4), Row(5), + Row(6), Row(7), Row(8), Row(9), Row(10)) + .Padding(20)) + .Height(220) + ) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/scrollviewer-vertical/scenario.json b/samples/scenarios/layout/scrollviewer-vertical/scenario.json new file mode 100644 index 000000000..ea13acad3 --- /dev/null +++ b/samples/scenarios/layout/scrollviewer-vertical/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "scrollviewer-vertical", + "category": "layout", + "title": "Vertical ScrollView", + "intent": "scrollable region that wraps a long vertical stack of content cards", + "tags": ["scroll", "scrollview", "vertical", "overflow"], + "factoryAnchors": ["ScrollView", "VStack"], + "notesKey": "ScrollView", + "relatedIds": ["vstack-basic"], + "priority": "P0" +} diff --git a/samples/scenarios/layout/vstack-basic/Scenario.cs b/samples/scenarios/layout/vstack-basic/Scenario.cs new file mode 100644 index 000000000..0cbcb1a81 --- /dev/null +++ b/samples/scenarios/layout/vstack-basic/Scenario.cs @@ -0,0 +1,27 @@ +// id: vstack-basic +// intent: vertical shrink-wrap stack with spacing +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("VStack Basic", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return Border( + VStack(12, + Subtitle("Daily checklist"), + TextBlock("Keep related actions grouped in a clean vertical stack.") + .Foreground(Theme.SecondaryText), + Button("Review changes"), + Button("Run validation"), + Button("Ship update")) + .Padding(20) + ) + .Background(Theme.SolidBackground); + } +} diff --git a/samples/scenarios/layout/vstack-basic/scenario.json b/samples/scenarios/layout/vstack-basic/scenario.json new file mode 100644 index 000000000..a7c8d6996 --- /dev/null +++ b/samples/scenarios/layout/vstack-basic/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "vstack-basic", + "category": "layout", + "title": "Vertical Stack Basics", + "intent": "vertical shrink-wrap stack with consistent spacing between actions", + "tags": ["vstack", "vertical", "stack", "spacing"], + "factoryAnchors": ["VStack", "TextBlock", "Button"], + "notesKey": "VStack", + "relatedIds": ["hstack-basic"], + "priority": "P0" +} diff --git a/samples/scenarios/lists/list-add-delete-toggle/Scenario.cs b/samples/scenarios/lists/list-add-delete-toggle/Scenario.cs new file mode 100644 index 000000000..a84a049fe --- /dev/null +++ b/samples/scenarios/lists/list-add-delete-toggle/Scenario.cs @@ -0,0 +1,57 @@ +// id: list-add-delete-toggle +// intent: dynamic list with add, delete, and toggle using UseReducer +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property WindowsPackageType=None + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Todo List", width: 500, height: 400); + +record TodoItem(string Id, string Text, bool Done); +record TodoState(ImmutableList Items); +abstract record TodoAction; +record AddAction(string Text) : TodoAction; +record ToggleAction(string Id) : TodoAction; +record DeleteAction(string Id) : TodoAction; + +class App : Component +{ + private static TodoState Reduce(TodoState state, TodoAction action) => action switch + { + AddAction add when !string.IsNullOrWhiteSpace(add.Text) + => state with { Items = state.Items.Add(new TodoItem(System.Guid.NewGuid().ToString(), add.Text.Trim(), false)) }, + ToggleAction toggle + => state with { Items = state.Items.Select(item => item.Id == toggle.Id ? item with { Done = !item.Done } : item).ToImmutableList() }, + DeleteAction delete + => state with { Items = state.Items.RemoveAll(item => item.Id == delete.Id) }, + _ => state, + }; + + public override Element Render() + { + var (state, dispatch) = UseReducer(Reduce, + new TodoState(ImmutableList.Create(new TodoItem("1", "Write docs", false), new TodoItem("2", "Ship sample", true)))); + var (draft, setDraft) = UseState(""); + + return VStack(12, + Heading($"Todo list ({state.Items.Count(item => item.Done)}/{state.Items.Count})"), + HStack(8, + TextField(draft, setDraft, placeholder: "Add an item").Width(300), + Button("Add", () => { dispatch(new AddAction(draft)); setDraft(""); }).IsEnabled(!string.IsNullOrWhiteSpace(draft))), + VStack(8, + ForEach(state.Items, item => + HStack(8, + CheckBox(item.Done, _ => dispatch(new ToggleAction(item.Id))), + TextBlock(item.Text).Width(240).Opacity(item.Done ? 0.5 : 1.0), + Button("Delete", () => dispatch(new DeleteAction(item.Id)))) + .WithKey(item.Id)))) + .Padding(16); + } +} diff --git a/samples/scenarios/lists/list-add-delete-toggle/scenario.json b/samples/scenarios/lists/list-add-delete-toggle/scenario.json new file mode 100644 index 000000000..0941f49a8 --- /dev/null +++ b/samples/scenarios/lists/list-add-delete-toggle/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "list-add-delete-toggle", + "category": "lists", + "title": "Add, Delete, Toggle Todo List", + "intent": "manage a mutable list with UseReducer and immutable updates", + "tags": ["list", "add", "delete", "toggle", "todo", "reducer", "crud"], + "factoryAnchors": ["ForEach", "UseReducer"], + "notesKey": "lists", + "relatedIds": ["list-with-empty-state"], + "priority": "P0" +} diff --git a/samples/scenarios/lists/list-basic-foreach/Scenario.cs b/samples/scenarios/lists/list-basic-foreach/Scenario.cs new file mode 100644 index 000000000..4dd5882c5 --- /dev/null +++ b/samples/scenarios/lists/list-basic-foreach/Scenario.cs @@ -0,0 +1,40 @@ +// id: list-basic-foreach +// intent: render a static list using ForEach +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property WindowsPackageType=None + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Basic ForEach", width: 500, height: 400); + +record GroceryItem(string Id, string Name, string Aisle); + +class App : Component +{ + public override Element Render() + { + var items = new[] + { + new GroceryItem("fruit", "Apples", "Produce"), + new GroceryItem("bread", "Sourdough", "Bakery"), + new GroceryItem("milk", "Whole Milk", "Dairy"), + }; + + return VStack(12, + Heading("Static shopping list"), + TextBlock("ForEach maps fixed data into UI rows."), + VStack(8, + ForEach(items, item => + HStack(12, + TextBlock(item.Name).Bold().Width(160), + TextBlock(item.Aisle).Opacity(0.7)) + .Padding(8) + .WithKey(item.Id)))) + .Padding(16); + } +} diff --git a/samples/scenarios/lists/list-basic-foreach/scenario.json b/samples/scenarios/lists/list-basic-foreach/scenario.json new file mode 100644 index 000000000..b2a0b6282 --- /dev/null +++ b/samples/scenarios/lists/list-basic-foreach/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "list-basic-foreach", + "category": "lists", + "title": "Basic ForEach", + "intent": "render a static list using ForEach with stable keys", + "tags": ["list", "foreach", "render", "items", "collection"], + "factoryAnchors": ["ForEach"], + "notesKey": "ForEach", + "relatedIds": ["list-add-delete-toggle", "virtualized-large-list"], + "priority": "P0" +} diff --git a/samples/scenarios/lists/list-with-empty-state/Scenario.cs b/samples/scenarios/lists/list-with-empty-state/Scenario.cs new file mode 100644 index 000000000..cca463f66 --- /dev/null +++ b/samples/scenarios/lists/list-with-empty-state/Scenario.cs @@ -0,0 +1,39 @@ +// id: list-with-empty-state +// intent: list with empty-state placeholder when no items exist +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property WindowsPackageType=None + +using System; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Empty State", width: 500, height: 400); + +record Favorite(string Id, string Label); + +class App : Component +{ + public override Element Render() + { + var (showItems, setShowItems) = UseState(false); + var items = showItems + ? new[] { new Favorite("one", "Design notes"), new Favorite("two", "Release checklist") } + : Array.Empty(); + + return VStack(12, + Heading("Favorites"), + Button(showItems ? "Clear items" : "Load sample items", () => setShowItems(!showItems)), + items.Length == 0 + ? Border(TextBlock("No items yet").Opacity(0.7)).Padding(16) + : VStack(8, + ForEach(items, item => + TextBlock(item.Label) + .Padding(8) + .WithKey(item.Id)))) + .Padding(16); + } +} diff --git a/samples/scenarios/lists/list-with-empty-state/scenario.json b/samples/scenarios/lists/list-with-empty-state/scenario.json new file mode 100644 index 000000000..2c880f011 --- /dev/null +++ b/samples/scenarios/lists/list-with-empty-state/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "list-with-empty-state", + "category": "lists", + "title": "List With Empty State", + "intent": "show a placeholder when a list has no items and render rows when data exists", + "tags": ["list", "empty", "placeholder", "conditional"], + "factoryAnchors": ["ForEach"], + "notesKey": "lists", + "relatedIds": ["list-add-delete-toggle"], + "priority": "P0" +} diff --git a/samples/scenarios/lists/list-with-loading/Scenario.cs b/samples/scenarios/lists/list-with-loading/Scenario.cs new file mode 100644 index 000000000..5f66ad977 --- /dev/null +++ b/samples/scenarios/lists/list-with-loading/Scenario.cs @@ -0,0 +1,48 @@ +// id: list-with-loading +// intent: list with loading indicator during async fetch +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property WindowsPackageType=None + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Loading List", width: 500, height: 400); + +record Order(string Id, string Label); + +class App : Component +{ + public override Element Render() + { + var (loading, setLoading) = UseState(true, threadSafe: true); + var (items, setItems) = UseState>(Array.Empty(), threadSafe: true); + + UseEffect(() => + { + _ = Task.Run(async () => + { + await Task.Delay(900); + setItems(new[] { new Order("101", "Order #101"), new Order("102", "Order #102"), new Order("103", "Order #103") }); + setLoading(false); + }); + }, Array.Empty()); + + return VStack(12, + Heading("Recent orders"), + loading + ? HStack(8, ProgressRing().IsActive(true).Width(20).Height(20), TextBlock("Loading items…")) + : VStack(8, + ForEach(items, item => + TextBlock(item.Label) + .Padding(8) + .WithKey(item.Id)))) + .Padding(16); + } +} diff --git a/samples/scenarios/lists/list-with-loading/scenario.json b/samples/scenarios/lists/list-with-loading/scenario.json new file mode 100644 index 000000000..d5517e919 --- /dev/null +++ b/samples/scenarios/lists/list-with-loading/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "list-with-loading", + "category": "lists", + "title": "List With Loading State", + "intent": "show a spinner first and then render fetched list items", + "tags": ["list", "loading", "spinner", "async", "fetch"], + "factoryAnchors": ["ForEach", "ProgressRing", "UseState"], + "notesKey": null, + "relatedIds": ["list-with-empty-state"], + "priority": "P0" +} diff --git a/samples/scenarios/lists/master-detail/Scenario.cs b/samples/scenarios/lists/master-detail/Scenario.cs new file mode 100644 index 000000000..eed63cb16 --- /dev/null +++ b/samples/scenarios/lists/master-detail/Scenario.cs @@ -0,0 +1,46 @@ +// id: master-detail +// intent: master-detail layout with list selection +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property WindowsPackageType=None + +using System.Linq; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Master Detail", width: 500, height: 400); + +record Topic(string Id, string Title, string Detail); + +class App : Component +{ + public override Element Render() + { + var topics = new[] + { + new Topic("layout", "Layout", "Use stacks to compose responsive UI."), + new Topic("state", "State", "Hooks keep selection and local state in sync."), + new Topic("lists", "Lists", "ForEach renders rows while state tracks selection."), + }; + var (selectedId, setSelectedId) = UseState(topics[0].Id); + var selected = topics.First(topic => topic.Id == selectedId); + + return HStack(16, + VStack(8, + Heading("Topics"), + ForEach(topics, topic => + Button(topic.Id == selectedId ? $"> {topic.Title}" : topic.Title, () => setSelectedId(topic.Id)) + .Width(180) + .WithKey(topic.Id))), + Border( + VStack(8, + Heading(selected.Title), + TextBlock(selected.Detail), + TextBlock($"Selected: {selected.Id}").Opacity(0.7)) + ).Padding(12)) + .Padding(16); + } +} diff --git a/samples/scenarios/lists/master-detail/scenario.json b/samples/scenarios/lists/master-detail/scenario.json new file mode 100644 index 000000000..03ae92f5d --- /dev/null +++ b/samples/scenarios/lists/master-detail/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "master-detail", + "category": "lists", + "title": "Master Detail Selection", + "intent": "use a selectable list on the left to drive detail content on the right", + "tags": ["master-detail", "selection", "list", "detail", "split"], + "factoryAnchors": ["ForEach", "UseState", "HStack"], + "notesKey": null, + "relatedIds": ["list-basic-foreach"], + "priority": "P0" +} diff --git a/samples/scenarios/lists/virtualized-large-list/Scenario.cs b/samples/scenarios/lists/virtualized-large-list/Scenario.cs new file mode 100644 index 000000000..01dbd5885 --- /dev/null +++ b/samples/scenarios/lists/virtualized-large-list/Scenario.cs @@ -0,0 +1,40 @@ +// id: virtualized-large-list +// intent: virtualized list for large datasets using LazyVStack +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true +#:property WindowsPackageType=None + +using System.Collections.Generic; +using System.Linq; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Virtualized List", width: 500, height: 400); + +record LogRow(string Id, string Message); + +class App : Component +{ + private static readonly IReadOnlyList Rows = Enumerable.Range(1, 1500) + .Select(i => new LogRow($"row-{i}", $"Log entry {i}: visible rows are realized on demand.")) + .ToArray(); + + public override Element Render() + { + return VStack(12, + Heading($"LazyVStack ({Rows.Count} items)"), + TextBlock("Use virtualization for long lists."), + LazyVStack( + Rows, + row => row.Id, + (row, index) => HStack(12, + TextBlock($"{index + 1}").Width(50), + TextBlock(row.Message)) + .Padding(8)) + .Height(300)) + .Padding(16); + } +} diff --git a/samples/scenarios/lists/virtualized-large-list/scenario.json b/samples/scenarios/lists/virtualized-large-list/scenario.json new file mode 100644 index 000000000..4411a48ac --- /dev/null +++ b/samples/scenarios/lists/virtualized-large-list/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "virtualized-large-list", + "category": "lists", + "title": "Virtualized Large List", + "intent": "render a large data set efficiently with LazyVStack virtualization", + "tags": ["virtualized", "lazy", "large-list", "performance"], + "factoryAnchors": ["LazyVStack"], + "notesKey": null, + "relatedIds": ["list-basic-foreach"], + "priority": "P0" +} diff --git a/samples/scenarios/navigation/sidebar-nav/Scenario.cs b/samples/scenarios/navigation/sidebar-nav/Scenario.cs new file mode 100644 index 000000000..b18f1ceb3 --- /dev/null +++ b/samples/scenarios/navigation/sidebar-nav/Scenario.cs @@ -0,0 +1,70 @@ +// id: sidebar-nav +// intent: typed sidebar routing with NavigationView and NavigationHost +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Sidebar Navigation", width: 1000, height: 700); + +enum Route { Home, Library, Settings } + +class Shell : Component +{ + static string ToTag(Route route) => route.ToString().ToLowerInvariant(); + static Route ToRoute(string tag) => Enum.Parse(tag, ignoreCase: true); + + public override Element Render() + { + var nav = UseNavigation(Route.Home); + + return NavigationView( + [ + NavItem("Home", icon: "", tag: ToTag(Route.Home)), + NavItem("Library", icon: "", tag: ToTag(Route.Library)), + NavItem("Settings", icon: "", tag: ToTag(Route.Settings)), + ], + NavigationHost(nav, route => route switch + { + Route.Home => Component(), + Route.Library => Component(), + Route.Settings => Component(), + _ => TextBlock("Not found") + })) + .WithNavigation(nav, ToTag, ToRoute); + } +} + +class HomePage : Component +{ + public override Element Render() => + VStack(12, + Heading("Home"), + TextBlock("Welcome.")) + .Padding(24); +} + +class LibraryPage : Component +{ + public override Element Render() => + VStack(12, + Heading("Library"), + TextBlock("Your stuff.")) + .Padding(24); +} + +class SettingsPage : Component +{ + public override Element Render() + { + var nav = this.UseNavigation(); + + return VStack(12, + Heading("Settings"), + Button("Back to Home", () => nav.Navigate(Route.Home))) + .Padding(24); + } +} diff --git a/samples/scenarios/navigation/sidebar-nav/scenario.json b/samples/scenarios/navigation/sidebar-nav/scenario.json new file mode 100644 index 000000000..101dd8857 --- /dev/null +++ b/samples/scenarios/navigation/sidebar-nav/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "sidebar-nav", + "category": "navigation", + "title": "Sidebar Navigation", + "intent": "typed route navigation with NavigationView, NavigationHost, and UseNavigation", + "tags": ["navigation", "sidebar", "route", "navigationview"], + "factoryAnchors": ["NavigationView", "NavigationHost", "UseNavigation"], + "notesKey": "NavigationView", + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/text/body-bodystrong/Scenario.cs b/samples/scenarios/text/body-bodystrong/Scenario.cs new file mode 100644 index 000000000..7ee80f914 --- /dev/null +++ b/samples/scenarios/text/body-bodystrong/Scenario.cs @@ -0,0 +1,20 @@ +// id: body-bodystrong +// intent: body text with emphasis using BodyStrong +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Body and BodyStrong", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return VStack( + Body("Body is useful for longer explanatory text in a layout."), + BodyStrong("BodyStrong highlights the sentence you want readers to notice first."), + Body("Mix them together to keep paragraphs readable while emphasizing key details.")); + } +} diff --git a/samples/scenarios/text/body-bodystrong/scenario.json b/samples/scenarios/text/body-bodystrong/scenario.json new file mode 100644 index 000000000..56c2f915c --- /dev/null +++ b/samples/scenarios/text/body-bodystrong/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "body-bodystrong", + "category": "text", + "title": "Body with BodyStrong Emphasis", + "intent": "body text with emphasis using BodyStrong", + "tags": ["body", "bodystrong", "emphasis", "paragraph"], + "factoryAnchors": ["Body", "BodyStrong"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/text/heading-subhead-caption/Scenario.cs b/samples/scenarios/text/heading-subhead-caption/Scenario.cs new file mode 100644 index 000000000..1cefa769c --- /dev/null +++ b/samples/scenarios/text/heading-subhead-caption/Scenario.cs @@ -0,0 +1,25 @@ +// id: heading-subhead-caption +// intent: demonstrate the WinUI 3 type ramp (Heading, Subtitle, Caption) +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Type Ramp", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return VStack( + Title("Title"), + Heading("Heading"), + SubHeading("SubHeading"), + Subtitle("Subtitle"), + Body("Body"), + BodyLarge("BodyLarge"), + BodyStrong("BodyStrong"), + Caption("Caption")); + } +} diff --git a/samples/scenarios/text/heading-subhead-caption/scenario.json b/samples/scenarios/text/heading-subhead-caption/scenario.json new file mode 100644 index 000000000..68a64ec50 --- /dev/null +++ b/samples/scenarios/text/heading-subhead-caption/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "heading-subhead-caption", + "category": "text", + "title": "Heading, Subtitle, and Caption", + "intent": "demonstrate the WinUI 3 type ramp (Heading, Subtitle, Caption)", + "tags": ["type-ramp", "heading", "subtitle", "caption", "typography"], + "factoryAnchors": ["Heading", "Subtitle", "Caption", "Title", "Body"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/text/localized-text/Scenario.cs b/samples/scenarios/text/localized-text/Scenario.cs new file mode 100644 index 000000000..1b391ec6e --- /dev/null +++ b/samples/scenarios/text/localized-text/Scenario.cs @@ -0,0 +1,20 @@ +// id: localized-text +// intent: localized text display using LocaleProvider +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Localized Text", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return VStack( + TextBlock("LocaleProvider sets locale context for its subtree."), + LocaleProvider("en-US", TextBlock("en-US: Hello from Reactor.")), + LocaleProvider("ar-SA", TextBlock("ar-SA: مرحبا من Reactor."))); + } +} diff --git a/samples/scenarios/text/localized-text/scenario.json b/samples/scenarios/text/localized-text/scenario.json new file mode 100644 index 000000000..579bf5d0f --- /dev/null +++ b/samples/scenarios/text/localized-text/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "localized-text", + "category": "text", + "title": "Localized Text with LocaleProvider", + "intent": "localized text display using LocaleProvider", + "tags": ["localization", "locale", "i18n", "text"], + "factoryAnchors": ["LocaleProvider", "TextBlock"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/text/rich-text-inlines/Scenario.cs b/samples/scenarios/text/rich-text-inlines/Scenario.cs new file mode 100644 index 000000000..140bc42d9 --- /dev/null +++ b/samples/scenarios/text/rich-text-inlines/Scenario.cs @@ -0,0 +1,28 @@ +// id: rich-text-inlines +// intent: rich text with bold, italic, and hyperlink runs +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using System; +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Rich Text Inlines", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return RichTextBlock(new[] + { + Paragraph( + Run("Rich text can mix "), + Run("bold") with { IsBold = true }, + Run(", "), + Run("italic") with { IsItalic = true }, + Run(", and "), + Hyperlink("hyperlinks", new Uri("https://github.com/microsoft/microsoft-ui-reactor")), + Run(" in one paragraph.")) + }); + } +} diff --git a/samples/scenarios/text/rich-text-inlines/scenario.json b/samples/scenarios/text/rich-text-inlines/scenario.json new file mode 100644 index 000000000..bf984508c --- /dev/null +++ b/samples/scenarios/text/rich-text-inlines/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "rich-text-inlines", + "category": "text", + "title": "Rich Text Inlines", + "intent": "rich text with bold, italic, and hyperlink runs", + "tags": ["rich-text", "inline", "bold", "italic", "hyperlink", "run"], + "factoryAnchors": ["RichTextBlock", "Paragraph", "Run", "Hyperlink"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/text/text-wrap-truncate/Scenario.cs b/samples/scenarios/text/text-wrap-truncate/Scenario.cs new file mode 100644 index 000000000..f434faff9 --- /dev/null +++ b/samples/scenarios/text/text-wrap-truncate/Scenario.cs @@ -0,0 +1,25 @@ +// id: text-wrap-truncate +// intent: text wrapping and trimming with ellipsis +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using Microsoft.UI.Xaml; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Wrap and Truncate", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + var sample = "A longer sentence shows how text behaves when the available width is smaller than the content."; + + return VStack( + TextBlock(sample).TextWrapping(TextWrapping.Wrap).MaxLines(2), + TextBlock(sample) + .TextWrapping(TextWrapping.Wrap) + .TextTrimming(TextTrimming.CharacterEllipsis) + .MaxLines(1)); + } +} diff --git a/samples/scenarios/text/text-wrap-truncate/scenario.json b/samples/scenarios/text/text-wrap-truncate/scenario.json new file mode 100644 index 000000000..405efa574 --- /dev/null +++ b/samples/scenarios/text/text-wrap-truncate/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "text-wrap-truncate", + "category": "text", + "title": "Wrap and Truncate Text", + "intent": "text wrapping and trimming with ellipsis", + "tags": ["wrap", "truncate", "ellipsis", "maxlines", "overflow"], + "factoryAnchors": ["TextBlock"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/samples/scenarios/text/textblock-basic/Scenario.cs b/samples/scenarios/text/textblock-basic/Scenario.cs new file mode 100644 index 000000000..e1821b435 --- /dev/null +++ b/samples/scenarios/text/textblock-basic/Scenario.cs @@ -0,0 +1,20 @@ +// id: textblock-basic +// intent: display a simple text label +#:package Microsoft.UI.Reactor@0.0.0-local +#:property Platform=ARM64 + +using Microsoft.UI.Reactor; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("TextBlock Basic", width: 400, height: 300); + +class App : Component +{ + public override Element Render() + { + return VStack( + TextBlock("Hello from Reactor."), + TextBlock("TextBlock displays read-only text content."), + TextBlock("Use it for labels, hints, and short status messages.")); + } +} diff --git a/samples/scenarios/text/textblock-basic/scenario.json b/samples/scenarios/text/textblock-basic/scenario.json new file mode 100644 index 000000000..51d09ce4b --- /dev/null +++ b/samples/scenarios/text/textblock-basic/scenario.json @@ -0,0 +1,11 @@ +{ + "id": "textblock-basic", + "category": "text", + "title": "Basic TextBlock", + "intent": "display a simple text label", + "tags": ["text", "textblock", "label", "display"], + "factoryAnchors": ["TextBlock"], + "notesKey": null, + "relatedIds": [], + "priority": "P0" +} diff --git a/src/Reactor.Cli/Find/BM25.cs b/src/Reactor.Cli/Find/BM25.cs new file mode 100644 index 000000000..6f139db42 --- /dev/null +++ b/src/Reactor.Cli/Find/BM25.cs @@ -0,0 +1,45 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal static class BM25 +{ + private const double K1 = 1.2; + private const double B = 0.75; + + public static double Score(string[] queryTerms, WeightedDoc doc, CorpusStats stats) + { + ArgumentNullException.ThrowIfNull(queryTerms); + ArgumentNullException.ThrowIfNull(doc); + ArgumentNullException.ThrowIfNull(stats); + + if (queryTerms.Length == 0 || stats.DocCount <= 0 || doc.DocLength <= 0 || stats.AvgDocLength <= 0) + { + return 0.0; + } + + var score = 0.0; + var norm = K1 * (1.0 - B + B * (doc.DocLength / stats.AvgDocLength)); + + foreach (var term in queryTerms) + { + if (!doc.TermWeights.TryGetValue(term, out var tf) || tf <= 0.0) + { + continue; + } + + stats.DocFrequency.TryGetValue(term, out var n); + var idf = Math.Log(((stats.DocCount - n + 0.5) / (n + 0.5)) + 1.0); + score += idf * ((tf * (K1 + 1.0)) / (tf + norm)); + } + + return score; + } +} + +internal record WeightedDoc(Dictionary TermWeights, int DocLength); + +internal record CorpusStats(int DocCount, double AvgDocLength, Dictionary DocFrequency); diff --git a/src/Reactor.Cli/Find/DataLoader.cs b/src/Reactor.Cli/Find/DataLoader.cs new file mode 100644 index 000000000..b6141325f --- /dev/null +++ b/src/Reactor.Cli/Find/DataLoader.cs @@ -0,0 +1,17 @@ +#nullable enable + +using System.Text.Json; + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal static class DataLoader +{ + public static ScenarioCatalogue Load() + { + var assembly = typeof(DataLoader).Assembly; + using var stream = assembly.GetManifestResourceStream("scenarios.json") + ?? throw new InvalidOperationException("Embedded scenarios.json not found."); + return JsonSerializer.Deserialize(stream, FindJsonContext.Default.ScenarioCatalogue) + ?? throw new InvalidOperationException("Failed to deserialize scenarios.json."); + } +} diff --git a/src/Reactor.Cli/Find/FindCommand.cs b/src/Reactor.Cli/Find/FindCommand.cs new file mode 100644 index 000000000..cb04a6c06 --- /dev/null +++ b/src/Reactor.Cli/Find/FindCommand.cs @@ -0,0 +1,89 @@ +#nullable enable + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal static class FindCommand +{ + public static int Run(string[] args) + { + if (args.Any(static a => a is "--help" or "-h" or "-?")) + { + ShowHelp(); + return 0; + } + + var maxResults = 5; + string? category = null; + var includeAntiPatterns = false; + var queryParts = new List(); + + for (var i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--max": + if (i + 1 >= args.Length || !int.TryParse(args[++i], out maxResults) || maxResults <= 0) + { + Console.Error.WriteLine("Error: --max requires a positive integer."); + return 1; + } + break; + + case "--category": + if (i + 1 >= args.Length) + { + Console.Error.WriteLine("Error: --category requires a value."); + return 1; + } + category = args[++i]; + break; + + case "--include-anti-patterns": + includeAntiPatterns = true; + break; + + default: + if (args[i].StartsWith("-", StringComparison.Ordinal)) + { + Console.Error.WriteLine($"Error: Unknown option '{args[i]}'."); + return 1; + } + + queryParts.Add(args[i]); + break; + } + } + + if (queryParts.Count == 0) + { + ShowHelp(); + return 1; + } + + var query = string.Join(" ", queryParts); + var catalogue = DataLoader.Load(); + var engine = new SearchEngine(catalogue); + var results = engine.Search(query, maxResults, category, includeAntiPatterns); + + if (results.Length == 0) + { + Console.WriteLine($"No matches found for \"{query}\"."); + return 0; + } + + Console.WriteLine($"Found {results.Length} matches for \"{query}\":"); + foreach (var result in results) + { + Console.WriteLine($" {result.Scenario.Id.PadRight(24)} {result.Scenario.Title.PadRight(40)} → SKILL: {result.Scenario.Category}"); + } + + Console.WriteLine("To get full code: mur get "); + return 0; + } + + private static void ShowHelp() + { + Console.WriteLine("Usage: mur find [--max N] [--category ] [--include-anti-patterns]"); + Console.WriteLine("Search the sample catalogue."); + } +} diff --git a/src/Reactor.Cli/Find/GetCommand.cs b/src/Reactor.Cli/Find/GetCommand.cs new file mode 100644 index 000000000..5b9200381 --- /dev/null +++ b/src/Reactor.Cli/Find/GetCommand.cs @@ -0,0 +1,91 @@ +#nullable enable + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal static class GetCommand +{ + public static int Run(string[] args) + { + if (args.Any(static a => a is "--help" or "-h" or "-?")) + { + ShowHelp(); + return 0; + } + + var raw = false; + string? scenarioId = null; + + foreach (var arg in args) + { + switch (arg) + { + case "--raw": + raw = true; + break; + + default: + if (arg.StartsWith("-", StringComparison.Ordinal)) + { + Console.Error.WriteLine($"Error: Unknown option '{arg}'."); + return 1; + } + + if (scenarioId is not null) + { + Console.Error.WriteLine("Error: Only one scenario id may be provided."); + return 1; + } + + scenarioId = arg; + break; + } + } + + if (string.IsNullOrWhiteSpace(scenarioId)) + { + ShowHelp(); + return 1; + } + + var catalogue = DataLoader.Load(); + var scenario = catalogue.Scenarios.FirstOrDefault(s => string.Equals(s.Id, scenarioId, StringComparison.OrdinalIgnoreCase)); + if (scenario is null) + { + Console.WriteLine($"Scenario '{scenarioId}' not found. Use 'mur list' to see all scenarios."); + return 1; + } + + Console.WriteLine($"## {scenario.Title}"); + Console.WriteLine($"*Category: {scenario.Category} · Intent: {scenario.Intent}*"); + Console.WriteLine(); + Console.WriteLine("**C#:**"); + Console.WriteLine("```csharp"); + Console.WriteLine(raw ? scenario.RawCode : scenario.Code); + Console.WriteLine("```"); + + var notes = Notes.GetNotes(scenario.NotesKey); + if (notes is { Length: > 0 } && scenario.NotesKey is not null) + { + Console.WriteLine(); + Console.WriteLine($"**Important (Notes for `{scenario.NotesKey}`):**"); + foreach (var note in notes) + { + Console.WriteLine($"- {note}"); + } + } + + if (scenario.RelatedIds.Length > 0) + { + Console.WriteLine(); + Console.WriteLine($"**See also:** {string.Join(", ", scenario.RelatedIds.Select(id => $"`{id}`"))}"); + } + + return 0; + } + + private static void ShowHelp() + { + Console.WriteLine("Usage: mur get [--raw]"); + Console.WriteLine("Show a sample scenario."); + } +} diff --git a/src/Reactor.Cli/Find/ListCommand.cs b/src/Reactor.Cli/Find/ListCommand.cs new file mode 100644 index 000000000..ea2bfef78 --- /dev/null +++ b/src/Reactor.Cli/Find/ListCommand.cs @@ -0,0 +1,78 @@ +#nullable enable + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal static class ListCommand +{ + public static int Run(string[] args) + { + if (args.Any(static a => a is "--help" or "-h" or "-?")) + { + ShowHelp(); + return 0; + } + + string? category = null; + + for (var i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--category": + if (i + 1 >= args.Length) + { + Console.Error.WriteLine("Error: --category requires a value."); + return 1; + } + + category = args[++i]; + break; + + default: + Console.Error.WriteLine($"Error: Unknown option '{args[i]}'."); + return 1; + } + } + + var catalogue = DataLoader.Load(); + var scenarios = catalogue.Scenarios.AsEnumerable(); + if (category is not null) + { + scenarios = scenarios.Where(s => string.Equals(s.Category, category, StringComparison.OrdinalIgnoreCase)); + } + + var groups = scenarios + .GroupBy(s => s.Category, StringComparer.OrdinalIgnoreCase) + .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (groups.Count == 0 && category is not null) + { + Console.WriteLine($"No scenarios in category '{category}'."); + return 0; + } + + for (var index = 0; index < groups.Count; index++) + { + var group = groups[index]; + Console.WriteLine(group.Key.ToUpperInvariant()); + foreach (var scenario in group.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase)) + { + Console.WriteLine($" {scenario.Id.PadRight(24)} {scenario.Title}"); + } + + if (index < groups.Count - 1) + { + Console.WriteLine(); + } + } + + return 0; + } + + private static void ShowHelp() + { + Console.WriteLine("Usage: mur list [--category ]"); + Console.WriteLine("List all scenarios."); + } +} diff --git a/src/Reactor.Cli/Find/Models.cs b/src/Reactor.Cli/Find/Models.cs new file mode 100644 index 000000000..5604916bc --- /dev/null +++ b/src/Reactor.Cli/Find/Models.cs @@ -0,0 +1,37 @@ +#nullable enable + +using System.Text.Json.Serialization; + +namespace Microsoft.UI.Reactor.Cli.Find; + +public record Scenario( + string Id, + string Category, + string Title, + string Intent, + string[] Tags, + string[] FactoryAnchors, + string? NotesKey, + string[] RelatedIds, + string Priority, + string Code, + string RawCode +); + +public record ScenarioCatalogue( + Scenario[] Scenarios, + string GeneratedAt +); + +public record SearchResult( + Scenario Scenario, + double Score +); + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(ScenarioCatalogue))] +[JsonSerializable(typeof(Scenario))] +[JsonSerializable(typeof(Scenario[]))] +internal partial class FindJsonContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/src/Reactor.Cli/Find/Notes.cs b/src/Reactor.Cli/Find/Notes.cs new file mode 100644 index 000000000..6881dbb30 --- /dev/null +++ b/src/Reactor.Cli/Find/Notes.cs @@ -0,0 +1,181 @@ +#nullable enable + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal static class Notes +{ + public static string[]? GetNotes(string? notesKey) + { + if (notesKey is null) return null; + return _notes.GetValueOrDefault(notesKey); + } + + private static readonly Dictionary _notes = new() + { + ["Button"] = + [ + "Button(label, onClick) is the basic factory. For icon buttons, use Button(Icon(symbol), onClick).", + "Use .AccentButton() for primary actions, .SubtleButton() for toolbar/chrome, .TextLink() for hyperlink-style.", + "For command-pattern buttons, use Button(command) where Command has Label, Execute, and CanExecute." + ], + ["CheckBox"] = + [ + "CheckBox(isChecked, onIsCheckedChanged, label) is the basic checkbox. Two-way binding pattern.", + "For three-state (checked/unchecked/indeterminate), use ThreeStateCheckBox with bool? state.", + "Label is optional — if omitted, place the CheckBox next to a TextBlock in an HStack." + ], + ["ComboBox"] = + [ + "ComboBox(items, selectedIndex, onSelectedIndexChanged) for string arrays. ComboBox(elements, selectedIndex, onChange) for custom elements.", + "selectedIndex is 0-based. -1 means no selection. The onChange callback receives the new index.", + "For searchable/filterable dropdowns, use AutoSuggestBox instead." + ], + ["ContentDialog"] = + [ + "ContentDialog is non-routed — show it via `UseDialog().Show(...)` from a hook, not by mounting it as a child element.", + "Primary/secondary/close buttons map to the three result branches. For yes/no/cancel, provide all three texts." + ], + ["DataGrid"] = + [ + "DataGrid takes an IDataSource, not a raw list. Wrap with IDataSource.From(items) for in-memory data; use a custom IDataSource for virtualized fetch.", + "Column(...) is the column builder. The accessor returns the cell value; format with the format parameter, don't synthesize strings.", + "For sortable/filterable in-memory data, pass IDataSource.From(items) and the source handles sort/filter internally." + ], + ["FlexColumn"] = + [ + "FlexColumn is a CSS flexbox column container. Children flow top-to-bottom with flex properties.", + "Use .Flex(grow: 1) on a child to fill remaining vertical space. Common for page layouts.", + "Combine with .Backdrop(BackdropKind.Mica) on the root FlexColumn for Win11 window chrome." + ], + ["FlexRow"] = + [ + "FlexRow is a CSS flexbox row container. Children flow left-to-right with flex properties (grow, shrink, basis, alignSelf).", + "Use .Flex(grow: 1) on a child to fill remaining horizontal space. .Flex(shrink: 0) prevents a child from shrinking.", + "FlexRow defaults to FlexWrap.NoWrap. Set .FlexWrap(FlexWrap.Wrap) for wrapping content." + ], + ["ForEach"] = + [ + "ForEach(items, render) maps a collection to elements. Always add .WithKey(uniqueId) on the outer element of each item.", + "ForEach is not virtualized — it renders all items immediately. For large lists (100+ items), use ListView or LazyVStack.", + "The render function receives (item) or (item, index). Prefer the item overload; index-based keys break on reorder." + ], + ["FormField"] = + [ + "FormField wraps an input with label + required marker + error display. Use with UseValidationContext for validation.", + "The showWhen parameter controls error visibility: ShowWhen.WhenTouched (default) hides errors until the user interacts; ShowWhen.Always shows immediately.", + "FormField is a layout wrapper, not a validator. Attach .Validate() on the input element inside the FormField." + ], + ["Grid"] = + [ + "Grid(columns, rows, children) creates a WinUI Grid. Use GridSize helpers: Auto, Star(n), Pixel(n).", + "Place children with .Grid(column, row) or .Grid(column, row, columnSpan, rowSpan).", + "For simple stacks, prefer VStack/HStack. Grid is for 2D layouts with explicit column/row sizing." + ], + ["HStack"] = + [ + "HStack arranges children horizontally with optional spacing. HStack(8, child1, child2) adds 8px between children.", + "Children are laid out left-to-right. Use .Flex(grow: 1) on a child to make it fill remaining space.", + "HStack shrink-wraps. Combine with VStack for simple layouts; use FlexRow for complex flex scenarios." + ], + ["Image"] = + [ + "Image(source) takes a URI string (ms-appx:///Assets/..., https://..., or file path).", + "Use .Width(n).Height(n) to constrain size. Without constraints, Image expands to natural size.", + "For icon-sized images, prefer FontIcon or SymbolIcon for better scaling and theming." + ], + ["lists"] = + [ + "Lists produced by `items.Select(...).ToArray()` MUST include `.WithKey(item.Id)` on every element. Without keys, focus, animation, and child state drift across reorders.", + "`UseState>` mutating in place does not re-render. Use `UseReducer` or `UseCollection`." + ], + ["NavigationView"] = + [ + "NavigationView provides sidebar/hamburger navigation. Wire with .WithNavigation(nav, toTag, toRoute) for typed routing.", + "NavItem tag must be a string. Map to your Route enum via toTag/toRoute functions.", + "NavigationHost renders the matched page. Child pages access the nav handle via UseNavigation()." + ], + ["ScrollView"] = + [ + "ScrollView(child) is the modern scrolling container (WinUI ScrollView). Use for scrollable content regions.", + "Don't wrap a ListView or ItemsRepeater in ScrollView — they have built-in scrolling. Double-scrolling breaks virtualization.", + "For legacy ScrollViewer compatibility, use ScrollViewer(child) — but prefer ScrollView for new code." + ], + ["TextField"] = + [ + "TextField(value, onChanged, placeholder) is the text input factory. Two-way binding: pass state value and setter.", + "Use .EmailInput(), .NumericInput(), .PhoneInput() for input scope hints. .MaxLength(n) caps input.", + ".Validate(fieldName, value, ...validators) attaches form validation. Requires UseValidationContext ancestor." + ], + ["Theme"] = + [ + "Use Theme.* tokens (Theme.PrimaryText, Theme.CardBackground). Hardcoded colors trip REACTOR_THEME_001.", + "`.Resources(r => r.Set(\"ButtonBackground\", …))` applies lightweight styling without a global Theme override.", + "Theme tokens automatically re-resolve on light/dark/high-contrast switches. Hardcoded values don't." + ], + ["UseCallback"] = + [ + "UseCallback memoizes a delegate so child components receiving it don't re-render when the parent renders. Wrap event handlers passed as props.", + "Deps array determines when the callback is recreated. Capture only the values you need — stale closures over state are the #1 bug.", + "If the callback needs current state, consider UseReducer + dispatch (which is stable) instead of UseCallback with state deps." + ], + ["UseContext"] = + [ + "UseContext reads the nearest ancestor Provider's value. If no provider exists, it throws — always wrap with a provider at or above the consumer.", + "Context re-renders all consumers when the value changes. For fine-grained updates, split into multiple contexts or use selectors.", + "The provider value should be memoized (UseMemo) if it's an object/record — otherwise every parent render creates a new reference and all consumers re-render." + ], + ["UseEffect"] = + [ + "Effects run AFTER render commits. Don't read state set inside the same render unless via UseEffect's cleanup or a deps change.", + "Return a cleanup lambda when the effect subscribes to anything. The cleanup runs before the next effect AND on unmount.", + "Empty deps `[]` means 'run once on mount' — but the effect still re-runs if the component remounts due to key change." + ], + ["UseMemo"] = + [ + "UseMemo caches a computed value and only recalculates when deps change. Use for expensive derivations, not for simple field access.", + "Deps are compared by value (record equality or reference equality for objects). A freshly-allocated array/list in deps defeats memoization.", + "UseMemo runs during render — don't put side effects in the factory. Use UseEffect for side effects." + ], + ["UseReducer"] = + [ + "UseReducer is the recommended hook for list/collection state. UseState> won't re-render on .Add()/.Remove() because the reference is unchanged.", + "The reducer function must be pure — same (state, action) always produces the same next-state. Side effects belong in UseEffect, not the reducer.", + "Dispatch is stable across renders — safe to pass to child components without wrapping in UseCallback." + ], + ["UseRef"] = + [ + "UseRef returns a mutable container that persists across renders without triggering re-render on assignment.", + "For DOM element refs, use UseRef() and attach via .Ref(myRef). The ref is populated after mount, not during Render().", + "Don't read UseRef during render to make decisions — the value is from the previous render. Use UseState if the value should trigger re-render." + ], + ["UseResource"] = + [ + "UseResource re-runs the fetcher when deps change, on retry, and on focus revalidation. Use UseMutation for writes (POST/PUT/DELETE).", + "Match on AsyncValue to render loading / error / data — don't unwrap by checking null.", + "Deps must be scalar values or memoized references. A freshly-allocated array in deps causes infinite re-fetch." + ], + ["UseState"] = + [ + "UseState with a List does NOT re-render on `.Add()` / `.Remove()` — same reference. Use UseReducer for collections.", + "UseState returns (value, setter). The setter is stable across renders — safe to omit from dependency arrays.", + "Call UseState unconditionally at the top of Render. Hooks track slot identity by call order." + ], + ["UseValidationContext"] = + [ + "UseValidationContext owns per-field validation state. Inputs attach via .Validate(fieldName, currentValue, ...validators).", + "Call ctx.MarkTouched(fieldName) in the input's onChange handler to trigger error display for WhenTouched mode.", + "ctx.IsValid() returns true only when ALL registered fields pass. Use for submit button gating." + ], + ["VStack"] = + [ + "VStack arranges children vertically with optional spacing. VStack(12, child1, child2) adds 12px between children.", + "VStack shrink-wraps to content. For a VStack that fills available space, wrap in a Flex container or use .Flex(grow: 1).", + "For horizontal layout, use HStack. For CSS flexbox-style layout, use FlexColumn/FlexRow." + ], + ["WithKey"] = + [ + "Required on every element produced from `.Select(...)` inside a layout container. Without it the analyzer emits `REACTOR_DSL_001` and reordering breaks focus/animation.", + "Key must be stable across renders. Don't key by index for reorderable lists — that defeats the purpose." + ] + }; +} diff --git a/src/Reactor.Cli/Find/SearchEngine.cs b/src/Reactor.Cli/Find/SearchEngine.cs new file mode 100644 index 000000000..cc80407e2 --- /dev/null +++ b/src/Reactor.Cli/Find/SearchEngine.cs @@ -0,0 +1,199 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal partial class SearchEngine +{ + private readonly ScenarioCatalogue _catalogue; + private readonly CorpusStats _stats; + private readonly ScenarioEntry[] _entries; + + public SearchEngine(ScenarioCatalogue catalogue) + { + _catalogue = catalogue ?? throw new ArgumentNullException(nameof(catalogue)); + _entries = catalogue.Scenarios.Select(CreateEntry).ToArray(); + _stats = BuildStats(_entries.Select(entry => entry.ScenarioDoc)); + } + + public SearchResult[] Search(string query, int maxResults = 5, string? category = null, bool includeAntiPatterns = false) + { + ArgumentNullException.ThrowIfNull(query); + + if (maxResults <= 0 || _catalogue.Scenarios.Length == 0) + { + return []; + } + + var queryTerms = Synonyms.ProcessQuery(query); + if (queryTerms.Length == 0) + { + return []; + } + + var normalizedCategory = string.IsNullOrWhiteSpace(category) + ? null + : category.Trim(); + + var filteredEntries = _entries.Where(entry => + (includeAntiPatterns || !string.Equals(entry.Scenario.Priority, "anti-pattern", StringComparison.OrdinalIgnoreCase)) && + (normalizedCategory is null || string.Equals(entry.Scenario.Category, normalizedCategory, StringComparison.OrdinalIgnoreCase))); + + var filteredByFactory = filteredEntries + .GroupBy(entry => entry.Factory, StringComparer.Ordinal) + .ToDictionary(group => group.Key, group => group.ToArray(), StringComparer.Ordinal); + + if (filteredByFactory.Count == 0) + { + return []; + } + + var factoryStats = BuildStats(filteredByFactory.Values.Select(entries => MergeDocs(entries.Select(entry => entry.FactoryDoc)))); + var rawTerms = Tokenize(query); + + var topFactories = filteredByFactory + .Select(pair => + { + var factoryDoc = MergeDocs(pair.Value.Select(entry => entry.FactoryDoc)); + var score = BM25.Score(queryTerms, factoryDoc, factoryStats); + if (score > 0.0 && rawTerms.Contains(pair.Key, StringComparer.Ordinal)) + { + score *= 2.0; + } + + return new FactoryScore(pair.Key, score); + }) + .Where(item => item.Score > 0.0) + .OrderByDescending(item => item.Score) + .ThenBy(item => item.Factory, StringComparer.Ordinal) + .Take(Math.Max(maxResults, 5)) + .ToArray(); + + if (topFactories.Length == 0) + { + return []; + } + + var scenarioStats = includeAntiPatterns && normalizedCategory is null + ? _stats + : BuildStats(filteredByFactory.Values.SelectMany(entries => entries).Select(entry => entry.ScenarioDoc)); + + var results = topFactories + .SelectMany(factory => filteredByFactory[factory.Factory]) + .Select(entry => new SearchResult(entry.Scenario, BM25.Score(queryTerms, entry.ScenarioDoc, scenarioStats))) + .Where(result => result.Score > 0.0) + .OrderByDescending(result => result.Score) + .ThenBy(result => result.Scenario.Title, StringComparer.Ordinal) + .Take(maxResults) + .ToArray(); + + return results; + } + + private static ScenarioEntry CreateEntry(Scenario scenario) + { + var factory = scenario.FactoryAnchors.FirstOrDefault() ?? string.Empty; + var factoryDoc = BuildWeightedDoc( + [ + (scenario.FactoryAnchors, 3.0), + (scenario.Tags, 3.0), + (new[] { scenario.Title }, 2.0), + (new[] { scenario.Intent }, 1.5) + ]); + var scenarioDoc = BuildWeightedDoc( + [ + (scenario.Tags, 1.0), + (new[] { scenario.Title }, 1.0), + (new[] { scenario.Intent }, 1.0) + ]); + + return new ScenarioEntry(scenario, factory, factoryDoc, scenarioDoc); + } + + private static WeightedDoc BuildWeightedDoc((IEnumerable Values, double Weight)[] fields) + { + var termWeights = new Dictionary(StringComparer.Ordinal); + var docLength = 0; + + foreach (var (values, weight) in fields) + { + foreach (var value in values) + { + foreach (var term in Tokenize(value)) + { + termWeights[term] = termWeights.TryGetValue(term, out var existing) + ? existing + weight + : weight; + docLength++; + } + } + } + + return new WeightedDoc(termWeights, docLength); + } + + private static WeightedDoc MergeDocs(IEnumerable docs) + { + var mergedWeights = new Dictionary(StringComparer.Ordinal); + var docLength = 0; + + foreach (var doc in docs) + { + docLength += doc.DocLength; + foreach (var (term, weight) in doc.TermWeights) + { + mergedWeights[term] = mergedWeights.TryGetValue(term, out var existing) + ? existing + weight + : weight; + } + } + + return new WeightedDoc(mergedWeights, docLength); + } + + private static CorpusStats BuildStats(IEnumerable docs) + { + var docCount = 0; + var totalDocLength = 0; + var docFrequency = new Dictionary(StringComparer.Ordinal); + + foreach (var doc in docs) + { + docCount++; + totalDocLength += doc.DocLength; + + foreach (var term in doc.TermWeights.Keys) + { + docFrequency[term] = docFrequency.TryGetValue(term, out var count) + ? count + 1 + : 1; + } + } + + var avgDocLength = docCount == 0 ? 0.0 : (double)totalDocLength / docCount; + return new CorpusStats(docCount, avgDocLength, docFrequency); + } + + private static string[] Tokenize(string text) + { + ArgumentNullException.ThrowIfNull(text); + + return TokenRegex() + .Matches(text.ToLowerInvariant()) + .Cast() + .Select(match => match.Value) + .Where(term => term.Length > 0 && !StopWords.IsStopWord(term)) + .ToArray(); + } + + [GeneratedRegex("[a-z0-9]+", RegexOptions.CultureInvariant)] + private static partial Regex TokenRegex(); + + private sealed record ScenarioEntry(Scenario Scenario, string Factory, WeightedDoc FactoryDoc, WeightedDoc ScenarioDoc); + + private sealed record FactoryScore(string Factory, double Score); +} diff --git a/src/Reactor.Cli/Find/StopWords.cs b/src/Reactor.Cli/Find/StopWords.cs new file mode 100644 index 000000000..fa3122439 --- /dev/null +++ b/src/Reactor.Cli/Find/StopWords.cs @@ -0,0 +1,55 @@ +#nullable enable + +using System.Collections.Frozen; + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal static class StopWords +{ + private static readonly FrozenSet _set = new[] + { + "a", + "an", + "and", + "are", + "as", + "at", + "be", + "been", + "by", + "can", + "could", + "did", + "do", + "does", + "for", + "from", + "had", + "has", + "have", + "hook", + "in", + "is", + "it", + "may", + "might", + "of", + "on", + "or", + "reactor", + "should", + "that", + "the", + "this", + "to", + "was", + "were", + "will", + "with", + "would", + "element", + "factory" + }.ToFrozenSet(); + + public static bool IsStopWord(string term) => _set.Contains(term); +} diff --git a/src/Reactor.Cli/Find/Synonyms.cs b/src/Reactor.Cli/Find/Synonyms.cs new file mode 100644 index 000000000..8c6a81461 --- /dev/null +++ b/src/Reactor.Cli/Find/Synonyms.cs @@ -0,0 +1,251 @@ +#nullable enable + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.UI.Reactor.Cli.Find; + +internal static partial class Synonyms +{ + private static readonly FrozenDictionary _phraseMap = new Dictionary(StringComparer.Ordinal) + { + ["app bar button"] = "appbarbutton", + ["auto suggest"] = "autosuggestbox", + ["auto suggest box"] = "autosuggestbox", + ["breadcrumb bar"] = "breadcrumbbar", + ["calendar date picker"] = "calendardatepicker", + ["calendar view"] = "calendarview", + ["check box"] = "checkbox", + ["color picker"] = "colorpicker", + ["combo box"] = "combobox", + ["command bar"] = "commandbar", + ["command bar flyout"] = "commandbarflyout", + ["command host"] = "commandhost", + ["content dialog"] = "contentdialog", + ["content flyout"] = "contentflyout", + ["context menu"] = "contextmenu", + ["dark mode"] = "theme", + ["data grid"] = "datagrid", + ["date picker"] = "datepicker", + ["drop down"] = "dropdown", + ["drop down button"] = "dropdownbutton", + ["error boundary"] = "errorboundary", + ["flip view"] = "flipview", + ["form field"] = "formfield", + ["global state"] = "context", + ["grid view"] = "gridview", + ["hyperlink button"] = "hyperlinkbutton", + ["info badge"] = "infobadge", + ["info bar"] = "infobar", + ["infinite scroll"] = "infinite", + ["items repeater"] = "itemsrepeater", + ["items view"] = "itemsview", + ["list box"] = "listbox", + ["list view"] = "listview", + ["map control"] = "mapcontrol", + ["master detail"] = "masterdetail", + ["media player"] = "mediaplayerelement", + ["menu bar"] = "menubar", + ["menu flyout"] = "menuflyout", + ["navigation host"] = "navigationhost", + ["navigation view"] = "navigationview", + ["number box"] = "numberbox", + ["password box"] = "passwordbox", + ["person picture"] = "personpicture", + ["pips pager"] = "pipspager", + ["progress bar"] = "progressbar", + ["progress ring"] = "progressring", + ["pull to refresh"] = "pulltorefresh", + ["radio button"] = "radiobutton", + ["radio buttons"] = "radiobuttons", + ["rating control"] = "ratingcontrol", + ["rich edit"] = "richeditbox", + ["rich edit box"] = "richeditbox", + ["rich text"] = "richtextblock", + ["scroll view"] = "scrollview", + ["scroll viewer"] = "scrollviewer", + ["selector bar"] = "selectorbar", + ["semantic zoom"] = "semanticzoom", + ["sidebar nav"] = "sidebar", + ["split button"] = "splitbutton", + ["split view"] = "splitview", + ["swipe control"] = "swipecontrol", + ["tab view"] = "tabview", + ["teaching tip"] = "teachingtip", + ["text block"] = "textblock", + ["text field"] = "textfield", + ["theme switch"] = "theme", + ["time picker"] = "timepicker", + ["title bar"] = "titlebar", + ["toggle button"] = "togglebutton", + ["toggle switch"] = "toggleswitch", + ["tree view"] = "treeview", + ["use callback"] = "usecallback", + ["use collection"] = "usecollection", + ["use context"] = "usecontext", + ["use custom hook"] = "customhook", + ["use effect"] = "useeffect", + ["use memo"] = "usememo", + ["use mutation"] = "usemutation", + ["use navigation"] = "usenavigation", + ["use reducer"] = "usereducer", + ["use ref"] = "useref", + ["use resource"] = "useresource", + ["use state"] = "usestate", + ["use validation"] = "usevalidationcontext", + ["validation context"] = "usevalidationcontext", + ["virtual list"] = "virtuallist", + ["web view"] = "webview2", + ["wrap grid"] = "wrapgrid" + }.ToFrozenDictionary(StringComparer.Ordinal); + + private static readonly FrozenDictionary _synonymMap = new Dictionary(StringComparer.Ordinal) + { + ["accordion"] = ["expander"], + ["alert"] = ["infobar", "contentdialog"], + ["appbar"] = ["commandbar"], + ["autocomplete"] = ["autosuggestbox"], + ["avatar"] = ["personpicture"], + ["badge"] = ["infobadge"], + ["banner"] = ["infobar"], + ["breadcrumbs"] = ["breadcrumbbar"], + ["btn"] = ["button"], + ["button"] = ["button"], + ["card"] = ["card", "border"], + ["carousel"] = ["flipview"], + ["chart"] = ["linechart", "barchart", "areachart"], + ["checkbox"] = ["checkbox"], + ["chip"] = ["togglebutton"], + ["collapse"] = ["expander"], + ["counter"] = ["usestate"], + ["datagrid"] = ["datagrid"], + ["datepicker"] = ["calendardatepicker", "datepicker"], + ["dialog"] = ["contentdialog"], + ["div"] = ["flexrow", "flexcolumn", "vstack", "hstack"], + ["divider"] = ["divider", "rectangle"], + ["drawer"] = ["navigationview", "splitview"], + ["dropdown"] = ["combobox"], + ["elem"] = ["element"], + ["errorhandling"] = ["errorboundary"], + ["expander"] = ["expander"], + ["fetch"] = ["useresource"], + ["flex"] = ["flexrow", "flexcolumn"], + ["flexbox"] = ["flexrow", "flexcolumn"], + ["flexcol"] = ["flexcolumn"], + ["flyout"] = ["flyout", "contentflyout"], + ["form"] = ["formfield", "usevalidationcontext"], + ["grid"] = ["grid", "gridview"], + ["header"] = ["heading", "subtitle"], + ["hook"] = ["usestate", "useeffect", "usereducer"], + ["image"] = ["image"], + ["img"] = ["image"], + ["infinite"] = ["useinfiniteresource"], + ["input"] = ["textfield", "numberbox"], + ["label"] = ["caption", "formfield"], + ["layout"] = ["vstack", "hstack", "flexrow", "grid"], + ["link"] = ["hyperlinkbutton"], + ["list"] = ["listview", "foreach"], + ["loader"] = ["progressring", "progressbar"], + ["menu"] = ["menuflyout", "menubar"], + ["modal"] = ["contentdialog", "dialog"], + ["nav"] = ["navigationview", "usenavigation"], + ["notification"] = ["infobar", "teachingtip"], + ["pager"] = ["pipspager"], + ["password"] = ["passwordbox"], + ["picker"] = ["combobox", "calendardatepicker"], + ["popup"] = ["flyout", "contentdialog"], + ["progress"] = ["progressring", "progressbar"], + ["query"] = ["useresource"], + ["radio"] = ["radiobuttons"], + ["reducer"] = ["usereducer"], + ["richtext"] = ["richtextblock", "richeditbox"], + ["scroll"] = ["scrollview"], + ["search"] = ["autosuggestbox"], + ["select"] = ["combobox"], + ["sidebar"] = ["navigationview", "splitview"], + ["slider"] = ["slider"], + ["snackbar"] = ["infobar", "teachingtip"], + ["span"] = ["textblock"], + ["spinner"] = ["progressring"], + ["split"] = ["splitview", "splitbutton"], + ["state"] = ["usestate", "usereducer"], + ["stepper"] = ["numberbox"], + ["switch"] = ["toggleswitch"], + ["table"] = ["datagrid", "listview"], + ["tabs"] = ["tabview", "pivot"], + ["text"] = ["textblock", "textfield"], + ["textarea"] = ["textbox", "richeditbox"], + ["timepicker"] = ["timepicker"], + ["toast"] = ["infobar"], + ["toggle"] = ["toggleswitch", "togglebutton"], + ["toolbar"] = ["commandbar"], + ["tooltip"] = ["tooltipservice"], + ["tree"] = ["treeview"], + ["txt"] = ["textblock", "textfield"], + ["typeahead"] = ["autosuggestbox"], + ["usecallback"] = ["usecallback"], + ["usecontext"] = ["usecontext"], + ["useeffect"] = ["useeffect"], + ["usememo"] = ["usememo"], + ["usereducer"] = ["usereducer"], + ["useref"] = ["useref"], + ["usestate"] = ["usestate"], + ["video"] = ["mediaplayerelement"], + ["webview"] = ["webview2"] + }.ToFrozenDictionary(StringComparer.Ordinal); + + public static string CollapsePhrase(string query) + { + ArgumentNullException.ThrowIfNull(query); + + var collapsed = query.ToLowerInvariant(); + foreach (var (phrase, token) in _phraseMap) + { + collapsed = Regex.Replace( + collapsed, + $@"\b{Regex.Escape(phrase)}\b", + token, + RegexOptions.CultureInvariant); + } + + return collapsed; + } + + public static string[] Expand(string term) + { + ArgumentNullException.ThrowIfNull(term); + + var normalized = term.ToLowerInvariant(); + return _synonymMap.TryGetValue(normalized, out var expanded) + ? expanded + : [normalized]; + } + + public static string[] ProcessQuery(string query) + { + ArgumentNullException.ThrowIfNull(query); + + return Tokenize(CollapsePhrase(query)) + .Where(term => !StopWords.IsStopWord(term)) + .SelectMany(Expand) + .Where(term => !StopWords.IsStopWord(term)) + .ToArray(); + } + + private static IEnumerable Tokenize(string text) + { + foreach (Match match in TokenRegex().Matches(text.ToLowerInvariant())) + { + if (match.Value.Length > 0) + { + yield return match.Value; + } + } + } + + [GeneratedRegex("[a-z0-9]+", RegexOptions.CultureInvariant)] + private static partial Regex TokenRegex(); +} diff --git a/src/Reactor.Cli/Program.cs b/src/Reactor.Cli/Program.cs index efc9e9a44..9ef9079f4 100644 --- a/src/Reactor.Cli/Program.cs +++ b/src/Reactor.Cli/Program.cs @@ -64,6 +64,21 @@ return Microsoft.UI.Reactor.Cli.Docs.DocsCommand.Run(args.Skip(1).ToArray()); } +if (arg == "find") +{ + return Microsoft.UI.Reactor.Cli.Find.FindCommand.Run(args.Skip(1).ToArray()); +} + +if (arg == "get") +{ + return Microsoft.UI.Reactor.Cli.Find.GetCommand.Run(args.Skip(1).ToArray()); +} + +if (arg == "list") +{ + return Microsoft.UI.Reactor.Cli.Find.ListCommand.Run(args.Skip(1).ToArray()); +} + if (arg == "devtools") { return Microsoft.UI.Reactor.Cli.Devtools.DevtoolsSupervisor.Run(args.Skip(1).ToArray()); @@ -114,6 +129,9 @@ void ShowHelp() Console.WriteLine(" loc status Show translation coverage per locale"); Console.WriteLine(" loc prune Find unused localization keys"); Console.WriteLine(" docs compile Compile documentation from templates and doc apps"); + Console.WriteLine(" find Search the sample catalogue"); + Console.WriteLine(" get Show a sample scenario"); + Console.WriteLine(" list List all scenarios"); Console.WriteLine(" devtools Launch project with --devtools run and supervise reloads"); Console.WriteLine(" check [path] Build and emit one-line diagnostics with skill-file pointers"); Console.WriteLine(" pack-local Pack the in-source framework to /local-nupkgs/ as 0.0.0-local"); diff --git a/src/Reactor.Cli/Reactor.Cli.csproj b/src/Reactor.Cli/Reactor.Cli.csproj index f66673fb5..738808989 100644 --- a/src/Reactor.Cli/Reactor.Cli.csproj +++ b/src/Reactor.Cli/Reactor.Cli.csproj @@ -50,6 +50,8 @@ +