|
| 1 | +--- |
| 2 | +name: fieldworks-avalonia-ui |
| 3 | +description: "Build, review, or fix Avalonia UI code in FieldWorks: XAML, MVVM, view models, owned controls, headless tests, preview host, accessibility identity, and product-vs-preview wiring. Use for any change under Src/Common/FwAvalonia/, Src/Common/FwAvaloniaPreviewHost/, or Src/**/*.Avalonia/, and for net48/net8 Avalonia test changes — even if the request only mentions a control, a binding, a style, or a flaky UI test. For whole-surface migration planning use fieldworks-winforms-to-avalonia-migration first." |
| 4 | +--- |
| 5 | + |
| 6 | +# FieldWorks Avalonia UI |
| 7 | + |
| 8 | +## Use This For |
| 9 | + |
| 10 | +- Avalonia XAML, view models, commands, lifetimes, dispatching, and |
| 11 | + resource/style changes. |
| 12 | +- New or changed projects under `Src/**/**/*.Avalonia/`, |
| 13 | + `Src/Common/FwAvalonia/`, and `Src/Common/FwAvaloniaPreviewHost/`. |
| 14 | +- Preview Host module registration, sample data providers, and UI |
| 15 | + diagnostics (see `.github/instructions/avalonia.instructions.md` for |
| 16 | + build/preview commands and project layout rules). |
| 17 | +- UI host wiring that selects between Avalonia and legacy UI — apply |
| 18 | + `fieldworks-ui-wiring-review` alongside this skill. |
| 19 | + |
| 20 | +## Start From the Established Patterns |
| 21 | + |
| 22 | +Do not design controls or seams from scratch. The migration hub skill |
| 23 | +(`fieldworks-winforms-to-avalonia-migration`) documents the decided |
| 24 | +architecture; its `references/architecture-patterns.md` covers owned |
| 25 | +controls, writing-system text fields, dialogs/flyouts, validation, and |
| 26 | +lifetime. Canonical code to imitate: |
| 27 | + |
| 28 | +- Owned field controls: `Src/Common/FwAvalonia/Region/FwFieldControls.cs`, |
| 29 | + `FwOptionPicker.cs`, `RegionMenuFlyout.cs`, `HoverReveal.cs` |
| 30 | +- Region view + focus memory: `LexicalEditRegionView.cs`, |
| 31 | + `RegionFocusMemory.cs` |
| 32 | +- Seams (scheduler, lifetime, clipboard, edit sessions): |
| 33 | + `Src/Common/FwAvalonia/Seams/ISeams.cs` |
| 34 | +- Headless test setup: `Src/Common/FwAvalonia/FwAvaloniaTests/TestAppBuilder.cs`; |
| 35 | + examples in `RegionEditingTests.cs`, `VisualParityAndDensityTests.cs` |
| 36 | +- Density constants: `Src/Common/FwAvalonia/Poc/PocDensity.cs` |
| 37 | +- **Dialog kit (XAML + CommunityToolkit.Mvvm + compiled bindings):** |
| 38 | + `Src/Common/FwAvaloniaDialogs/` — `OptionsDialogView.axaml`/`.axaml.cs` + |
| 39 | + `OptionsDialogViewModel.cs`; headless tests in `FwAvaloniaDialogsTests/`. |
| 40 | + This is the verified template for hand-authored dialogs — see |
| 41 | + "Converting a WinForms dialog (MVVM kit)" below. |
| 42 | + |
| 43 | +**Re-implementing a Phase-1 deferred screen (JIRA tickets).** Many screens were documented |
| 44 | +and backed out to land the Phase-1 PR; each has a ticket + a `Docs/migration/<screen>.md` |
| 45 | +(what it is, legacy PNGs, parity checklist, gotchas, the git sha of the backed-out stub). |
| 46 | +Start there: recover the stub from git history as a skeleton, copy the **canonical screen** |
| 47 | +for that primitive (the hub skill's "Phase-1 Landing Strategy" names them — e.g. ChooserDialog |
| 48 | +for tree/multi-select, OptionsDialog for tabs, InsertEntryDialog for owned-control forms), then |
| 49 | +follow the per-region Workflow. Re-wire the call site behind `UIMode=New` (Legacy keeps the |
| 50 | +WinForms path). |
| 51 | + |
| 52 | +## Converting a WinForms dialog (MVVM kit) |
| 53 | + |
| 54 | +Hand-authored dialogs/wizards use **XAML + CommunityToolkit.Mvvm + compiled |
| 55 | +bindings** — NOT the region/IR pattern (that is only for XML-view-definition |
| 56 | +surfaces). Decided 2026-06-15; rationale in |
| 57 | +`avalonia-migration-roadmap/complete-migration-program.md` §11.3. Full |
| 58 | +step-by-step + the working template: |
| 59 | +`references/dialog-conversion.md`. The shape, per dialog: |
| 60 | + |
| 61 | +1. **View** `XyzDialogView.axaml` (+ `.axaml.cs`): a `UserControl` (not a |
| 62 | + `Window` — see modality below), `x:DataType` set to the view-model, |
| 63 | + compiled `{Binding}`s, and a stable `AutomationProperties.AutomationId` |
| 64 | + on every interactive control. Reuse owned controls (`FwMultiWsTextField`, |
| 65 | + `FwOptionPicker`) for writing-system fields and list pickers. |
| 66 | +2. **View-model** `XyzDialogViewModel.cs`: `ObservableObject` with |
| 67 | + `[ObservableProperty]` state and `[RelayCommand]` actions; expose the |
| 68 | + result (e.g. `Accepted`). Keep it LCModel-free for the view; bind real |
| 69 | + settings/domain through the app-settings/edit-session seams. |
| 70 | +3. **Tests** `XyzDialogTests.cs` (headless `[AvaloniaTest]`): assert the |
| 71 | + compiled bindings propagate both directions and the commands fire, AND |
| 72 | + capture a PNG at **each interaction stage** (empty, populated, error/disabled, |
| 73 | + post-action) for visual review — this is the per-dialog definition of done. |
| 74 | + |
| 75 | +### Style system (density + borders, per surface) |
| 76 | + |
| 77 | +The font/density tokens and the field-border rule are a GLOBAL system, calibrated to WinForms density |
| 78 | +(~12px font, ~22px control height) — not the roomy Fluent defaults — and applied per-control-tree (the only |
| 79 | +mechanism that renders in BOTH the runtime host and the headless tests). Full detail, the calibrated numbers, |
| 80 | +and the per-surface intent: **`references/style-system.md`**. The short version: |
| 81 | + |
| 82 | +- **Dialog inputs are BOXED** (`TextBox`/`ComboBox`, and the `PART_*Host` borders wrapping owned editors get a |
| 83 | + visible 1px gray border via `Border.fwFieldHost`); **detail/region values are FLAT** with subtle separators |
| 84 | + (the DataTree look — do not box them); **browse keeps its grid lines** — just denser font everywhere. |
| 85 | +- One source of truth per family: dialog density/borders in `DialogTheme.axaml` (applied by |
| 86 | + `DialogThemeBootstrap.Apply`); region/browse font in `FwSurfaceStyles` (applied in those view ctors). |
| 87 | +- **Borders/colors that must render headlessly use a CONCRETE value, never a Fluent `DynamicResource`** — the |
| 88 | + field-host border is `#FF7A7A7A`, because `ControlStrokeColorDefault` resolved to nothing headlessly. |
| 89 | + |
| 90 | +### Dialog spacing |
| 91 | + |
| 92 | +All dialog spacing/borders come from the shared tokens in |
| 93 | +`Src/Common/FwAvaloniaDialogs/DialogTheme.axaml` (applied to each dialog body by |
| 94 | +`DialogThemeBootstrap.Apply(this)`, called from every dialog view ctor). The current calibrated tokens: |
| 95 | +`DialogWindowPadding` 10, `DialogControlGap` 6, `DialogLabelFieldGap` 4, `DialogMinControlHeight` 22, |
| 96 | +`DialogFieldPadding` 3, `DialogFontSize` 12. Hard rules: |
| 97 | + |
| 98 | +- **Every dialog root carries `DialogWindowPadding`** — put `Classes="fwDialogRoot"` on the root |
| 99 | + `UserControl` and drop any root `Margin` literal. |
| 100 | +- **No text-bearing or `PART_*Host` control with 0 padding** — host borders carry `Classes="fwFieldHost"` |
| 101 | + (a real border + `DialogFieldPadding`); never leave a host border without a `BorderThickness`. |
| 102 | +- **OK/Cancel use the standard button strip gap** (`DialogButtonStripGap` + `DialogControlGapAbove`). |
| 103 | +- **Never hardcode a margin/spacing literal — use a token** (`DialogControlGap`, `DialogLabelFieldGap`, |
| 104 | + …); add a new token to `DialogTheme.axaml` rather than inlining a number. |
| 105 | +- The headless `DialogLayoutAssert.AssertNoCrowding(view)` tripwire gates this in every dialog's |
| 106 | + realized-view test. Full detail: `references/dialog-conversion.md` §2a-bis. |
| 107 | +- **Capture a PNG at EACH stage and look at it (hard rule, part of the per-dialog definition of done).** |
| 108 | + Every dialog test captures `DialogSnapshot.Capture(view, "<Surface>-<NN>-<stage>")` (→ gitignored |
| 109 | + per-surface folder `Output/Snapshots/<Surface>/<NN>-<stage>.png`) at every meaningful UI state it drives — empty/initial, populated, |
| 110 | + validation-error / OK-disabled, post-action — *before* any `AssertNoCrowding`. After running the dialog |
| 111 | + tests, the agent MUST **Read each per-stage PNG** and explicitly answer the **six subjective-quality |
| 112 | + questions** per image — (1) alignment of words/columns/icons/labels, (2) appropriate spacing between |
| 113 | + words/fields/graphics, (3) borders/containment, (4) clipping/overflow, (5) overlap, (6) legibility & |
| 114 | + consistency — writing the answers down, not just glancing. An off-looking PNG is a real finding even when |
| 115 | + `AssertNoCrowding` passes. The loop is: capture → run → Read each PNG → answer the six questions → fix |
| 116 | + view/token → re-capture. Applies to region/browse surfaces too. Full checklist + workflow: |
| 117 | + `references/visual-snapshot-testing.md`. |
| 118 | + |
| 119 | +Rules specific to dialogs: |
| 120 | + |
| 121 | +- **It lives in `Src/Common/FwAvaloniaDialogs/`** (the dedicated XAML project), |
| 122 | + never in the pure-C# `FwAvalonia` foundation. Add new dialog projects to |
| 123 | + `FieldWorks.sln` (restore + VS) but build them in `build.ps1`'s Avalonia |
| 124 | + loop; keep the XAML compile off the main `FieldWorks.proj` traversal (the |
| 125 | + exclude pattern there). Exclude any nested test folder from the library's |
| 126 | + compile glob (`<Compile Remove="XxxTests/**/*.cs"/>`). |
| 127 | +- **Modality during coexistence:** no Avalonia `Window.ShowDialog`. Show the |
| 128 | + dialog `UserControl` via **`AvaloniaDialogHost.ShowModal(owner, view, vm, title)`** |
| 129 | + (in `Src/Common/FwAvalonia/`), which hosts it in a WinForms-owned modal `Form` |
| 130 | + (per the hub's `dialog-ownership.md`). The view-model implements |
| 131 | + **`IDialogViewModel`** and raises `CloseRequested(bool)` from OK/Cancel — no |
| 132 | + windowing in the VM. A dialog is "view + VM + `ShowModal`." |
| 133 | +- **Scope:** simple/confirmation/settings dialogs are good junior+AI work; |
| 134 | + Views-engine-coupled dialogs (Find/Replace, Styles host `IVwRootSite`) |
| 135 | + belong with the document engine (Stage 9), NOT this kit. |
| 136 | + |
| 137 | +## Required Checks |
| 138 | + |
| 139 | +- Use current Avalonia docs for uncertain APIs; do not guess dispatcher, |
| 140 | + headless, automation, or binding behavior. |
| 141 | +- Keep field labels on the StringTable lane. Product-facing Avalonia chrome |
| 142 | + should join the existing LocalizationManager/L10NSharp XLIFF catalog, |
| 143 | + preferably by reusing existing `Palaso`/`Chorus` ids when they actually |
| 144 | + match; otherwise add unique Avalonia-prefixed ids there. The current |
| 145 | + English-default source is the accessor code, not a parallel Avalonia |
| 146 | + `.resx` runtime lane. Prototype hardcoded strings must be called out as |
| 147 | + gaps. |
| 148 | +- Stamp stable, nonlocalized `AutomationProperties.AutomationId` (derived |
| 149 | + from IR `StableId` where applicable) and localized |
| 150 | + `AutomationProperties.Name` on user-facing controls. |
| 151 | +- UI logic stays in bindings/view models where practical; avoid |
| 152 | + logic-heavy code-behind. |
| 153 | +- For any Avalonia "select from a list" surface, prefer the shared |
| 154 | + `FwOptionPicker` pattern in `Src/Common/FwAvalonia/Region/FwOptionPicker.cs` |
| 155 | + (AutoCompleteBox-based, keyboard-safe, search-capable, compact density) |
| 156 | + over ad hoc `ListBox` popups or one-off editable selectors. Reach for a raw |
| 157 | + `ComboBox` only when the UX explicitly needs an always-visible inline combo |
| 158 | + rather than the shared flyout selector. |
| 159 | +- Do not fix Avalonia keyboard, focus, filtering, selection, popup, or |
| 160 | + rendering bugs by patching `System.Windows.Forms` hosts, WinForms |
| 161 | + interop message handling, or other legacy host-only routes unless the |
| 162 | + task explicitly targets interop behavior. Default to fixing the issue |
| 163 | + inside the Avalonia control tree or Avalonia-owned seams. |
| 164 | +- Marshal to the UI thread through `IUiScheduler` (or Avalonia dispatcher |
| 165 | + in non-region code); no hidden `Task.Run`, no sync-over-async. |
| 166 | +- Keep preview data lightweight unless the change explicitly opts into |
| 167 | + LCModel/project data; product-facing paths use real edit-session/domain |
| 168 | + contracts — detached DTO-only models remain preview-only. |
| 169 | +- Headless tests: simulate input on `Window`, flush with |
| 170 | + `Dispatcher.UIThread.RunJobs()`, and capture visual regression frames |
| 171 | + with Skia (`UseHeadlessDrawing=false` + `CaptureRenderedFrame()`). |
| 172 | +- If Avalonia chrome uses LocalizationManager, bootstrap it once per |
| 173 | + preview-host or headless-test process before localized strings are |
| 174 | + requested; do not pay that cost per test. |
| 175 | +- Evidence runs through `./build.ps1` and `./test.ps1` via the normal repo |
| 176 | + graph, not branch-only lanes. |
| 177 | + |
| 178 | +## Review Red Flags |
| 179 | + |
| 180 | +- A Common project directly references a feature module without an |
| 181 | + explicit architecture decision. |
| 182 | +- Preview-only code launched from product UI without a feature gate. |
| 183 | +- Tests manually call `OnPropertyChanged(...)`, `ShowRecord()`, or similar |
| 184 | + instead of proving the real broadcast/wiring path. |
| 185 | +- The active Avalonia path drives hidden legacy rendering/menu |
| 186 | + infrastructure (see the hub skill's hard rules). |
| 187 | +- Sleep-based or timing-sensitive UI tests. |
| 188 | +- Claims of accessibility, localization, IME, or keyboard parity without |
| 189 | + executable evidence (see the hub skill's |
| 190 | + `references/parity-evidence.md` §"Evidence language"). |
| 191 | + |
| 192 | +## Handoff |
| 193 | + |
| 194 | +Report Avalonia docs consulted, tests run, remaining prototype gaps, |
| 195 | +whether the change is product-facing or preview-only, and how the live |
| 196 | +wiring path was validated for each affected host. For parity work, say |
| 197 | +whether visual evidence is control-level headless capture or live desktop |
| 198 | +capture, and which automation identities were assigned. |
| 199 | + |
| 200 | +## Keep This Skill Current |
| 201 | + |
| 202 | +When a control pattern, headless-test technique, or Avalonia API gotcha |
| 203 | +proves out (or a pointer above goes stale), update this skill in the same |
| 204 | +PR — and route durable architecture lessons through the protocol in |
| 205 | +`fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`. |
0 commit comments