Skip to content

Adopt Semi.Avalonia and a FieldWorks design-token system - #1083

Open
johnml1135 wants to merge 2 commits into
mainfrom
semi-avalonia
Open

Adopt Semi.Avalonia and a FieldWorks design-token system#1083
johnml1135 wants to merge 2 commits into
mainfrom
semi-avalonia

Conversation

@johnml1135

@johnml1135 johnml1135 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Replaces Avalonia's Fluent theme with Semi.Avalonia + Ursa and gives the Avalonia UI a real design-token system in place of colors/spacing scattered as literals — enforced by a new CI gate that fails the build on any hardcoded value in that surface. The Avalonia UI stays behind FW_AVALONIA, opt-in only; nothing here changes default FieldWorks behavior.

88 files is a lot for something with zero default-on behavior change — the honest reason: rebuilding the DataTree detail view on Ursa's layout primitive, and the token system it depends on, touch nearly everything already converted to Avalonia (9 dialogs + the detail-view foundation). The part actually worth scrutinizing is narrower than the diff: the token architecture and its enforcement gate. Most per-dialog changes are 1:1 literal→token swaps.

Where to look:

  • token-hygiene.ps1 is now a hard CI failure — whole-tree, no grandfathering, scoped to the Avalonia surface only. Why that scope, not global: docs/adr/0002.
  • FieldWorks tokens default to aliasing Semi's own semantic roles rather than inventing new colors, verified against the pinned Semi.Avalonia 11.3.14 source directly, not assumed: docs/adr/0001.
  • fwGroupBox (new titled-border primitive) is the one real visual change — applied only to the Options dialog, which had an actual reported defect; the other 8 were reviewed and left alone.
  • GenerateTokenKeys (new Build/Src/FwBuildTasks codegen) turns a stale/typo'd token key into a compile error — what makes "whole-tree, no exceptions" trustworthy rather than just strict-sounding.
  • DialogLayoutAssert gained 2 general checks (readable-font floor, group-box minimum gap) that run automatically on every dialog test already in the suite, not just new ones.

Deliberately not here: Dark/Compact/color-blind theming — the ThemeDictionaries structure supports all three later, none is built or visually verified now; Light is the only reviewed variant. No automated pixel-diff visual regression (why: docs/adr/0003) — verification is geometric assertions plus 9 committed baseline screenshots (Docs/migration/baseline-screenshots/). L10NSharp is a version bump only; live UI-language switching was investigated and explicitly not built (every language-change path in FieldWorks, old and new UI alike, already requires a restart).

Verification: Not stacked. Build: 0 errors. FwAvaloniaTests: 647 passed, 1 skipped (pre-existing). FwAvaloniaDialogsTests: 288 passed. Both hygiene gates clean. No native or installer files touched.


Reading this a year from now -- start here

This PR's working history (grilling sessions, adversarial reviews, live corrections) lived
in a long agent conversation, not in tree files — there was nothing to evict from the repo
because none of it was ever committed as scratch docs. What follows synthesizes that
conversation's decisions and evidence directly into this record.

The three ADRs (docs/adr/0001-0003) are the durable record of why; this section covers
the how it went, including the mistakes caught along the way.

The layer cake — token resolution, end to end

A view (C# in FwAvalonia/FwAvaloniaDialogs, or .axaml in the dialogs project) asks for
a value one of two ways:

  • XAML: {DynamicResource FwLabelBrush} or, for Semi's own roles now referenced
    directly, {StaticResource SemiColorDanger}.
  • C#: FwThemeResources.RequireBrush(GeneratedTokenKeys.FwLabelBrush) — a
    compile-time-checked constant, not a raw string, resolved at point-of-use via
    Application.Current.TryGetResource, never cached in a static field (Application.Current
    is null under beforefieldinit before the app starts).

The key resolves through merged Application.Resources: Src/Common/FwAvaloniaTheme's
FwColorTokens.axaml (Light/Dark ThemeDictionaries — shared brushes + the one
FwSurfaceFontSize) and DataTreeTokens.axaml (flat, non-themed DataTree layout
dimensions), both merged by FwAvaloniaApp/PreviewHostApp at Initialize(), plus
DialogTheme.axaml's own local Dialog* keys merged into that same dictionary and applied
per-dialog-body via DialogThemeBootstrap.Apply.

Underneath FieldWorks' tier sits Semi.Avalonia's own two-tier system: ~449 raw
color-ramp/spacing primitives (Layer 1, no meaning attached) and named semantic roles
(Layer 2: SemiColorText0-3, SemiColorBorder, SemiColorBackground0-4, a flat
spacing/radius/height scale) that alias them. FieldWorks' tier defaults to aliasing Layer 2
directly; a FieldWorks-owned value requires a written, checkable reason.

GenerateTokenKeys (a Build/Src/FwBuildTasks MSBuild Task, not a Roslyn generator — see
Decisions below) reads the token .axaml files' x:Keys at build time and emits
GeneratedTokenKeys.g.cs: the compile-time-checked constants above, plus baked Thickness
literal values (via Avalonia.Thickness.Parse) for the few spots
(CompactDialogStyles.cs/FwSurfaceStyles.cs) where Avalonia's compiled XAML rejects
x:Static, so a C# style builder can't read a token via {StaticResource} at all.

Decisions, and why

Alias Semi's semantic tier by default, not an independent FieldWorks palette. Semi
already ships primitive→semantic aliasing (the pattern every mature design system — Fluent
2, Carbon, Atlassian, Adobe Spectrum — uses); FieldWorks previously ignored it and picked
every color independently by eye from old WinForms screenshots. Verified empirically that
{StaticResource} reaches Semi's own Layer-2 keys fine from ordinary view XAML — the known
DynamicResource-only landmine on this branch is narrower than first assumed: it's specific
to DialogTheme.axaml's own Setters (grafted onto a view's .Styles at runtime), not
ThemeDictionaries crossing in general.

token-hygiene.ps1 is whole-tree and zero-grandfathering, deliberately unlike
comment-hygiene.ps1.
The scoped tree is new code with nothing to grandfather; current
design-token practice treats that as the correct case for full-strictness-from-day-one, the
same literature is equally clear it's the wrong call for retrofitting legacy code — which is
why the WinForms surface stays out of scope. Consequence accepted deliberately: since only
agents are required to run -TokenHygiene locally, one slipped-in violation on main fails
every unrelated PR touching the tree until fixed — no ratchet/baseline valve exists yet.

GenerateTokenKeys is a custom MSBuild Task, not a Roslyn source generator. Matches this
codebase's own precedent for "generate typed C# from a declarative source" — liblcm's
LcmGenerate — rather than introducing tooling nobody on this codebase has used yet.

Geometric layout assertions + reviewed screenshots, not automated pixel-diff. Real
current tooling for visual regression (Percy/Chromatic/Playwright) is a web/DOM-native
ecosystem with no mature managed equivalent for Avalonia/WPF; even mature web tooling needed
a dedicated AI-review layer to suppress anti-aliasing/font/DPI noise. A small, curated,
committed baseline set exists specifically so an "I looked, it's fine" claim survives past
the run that made it — previously all snapshots were ephemeral and gitignored.

Paths not taken
  • Live UI-language switching. The L10NSharp version bump was originally planned as a
    prerequisite for it. Investigated directly: every UI-language-change path in FieldWorks —
    WinForms and the existing Avalonia port alike — deliberately sets restartRequired = true
    rather than live-refreshing. Building live switching would be new, unrequested engineering
    inconsistent with the rest of the app, not a gap this branch needed to close.
  • A one-line suppression-comment mechanism for the hygiene gate, considered when
    discussing the gate's escape valve. Rejected once the actual existing exception (the
    x:Static compiled-XAML limitation) turned out to already be resolved better by
    GenerateTokenKeys generating the value outright, backed by
    DuplicateTokenPairConsistencyTests.cs as a regression guard — stronger than an unverified
    suppression comment would have been.
  • A generic "any two same-type siblings need a gap" rule for DialogLayoutAssert,
    rejected as too broad (would false-positive on deliberately tight pairs like a label over
    its field) in favor of a rule scoped specifically to fwGroupBox siblings.
What this does NOT authorize
  • Not a precedent for retrofitting token-hygiene.ps1-style enforcement onto the WinForms
    surface — that surface is out of scope by design (docs/adr/0002).
    Not a decision that Dark/Compact/color-blind theming is "done" — only that the
    ThemeDictionaries structure won't need a rearchitecture to add them later; none has been
    designed, built, or visually verified.
  • Not a claim that every FieldWorks-owned (non-aliased) color value has been re-verified
    pixel-for-pixel against Semi's real composited output — the 8 keys flagged in review were
    checked directly; the remaining KEEP-AS-NEW dimension tokens were not individually
    re-derived from Semi's spacing scale where no exact match existed.
Surprising findings
  • Two written justifications for keeping FieldWorks-owned colors instead of aliasing Semi
    were factually backwards: SemiColorBorder was described as "opaque, too heavy" for a 1px
    divider when the real value (verified against the pinned v11.3.14 tag's source) is
    8%-opacity and nearly invisible; Semi's Text0-3 were described as "resolving identically"
    when they're four distinct brushes with different baked opacities (0.8/0.62/0.35).
    Re-evaluating with the corrected facts didn't change any of the 8 affected keys' actual
    values — every one turned out to already be independently grounded in real legacy-WinForms
    pixel measurements — but the written reasoning in both FwColorTokens.axaml and
    docs/adr/0001 was wrong until corrected during review.
  • A branch-history squash (16 commits down to a handful, done via git commit-tree rather
    than an interactive rebase) initially produced a commit whose message described work
    actually contained in a different commit, because of a chronological mis-sequencing.
    Caught by an adversarial review that diffed each commit's actual content against its
    claimed content rather than trusting the message — re-sequenced and re-verified
    byte-identical to the pre-squash tree before proceeding.
Evidence
  • Build: .\build.ps1 -BuildTests -SkipNative -CommentHygiene -TokenHygiene — 0 errors, both
    hygiene gates clean (170 files scanned by token-hygiene.ps1, 0 violations).
  • Tests: FwAvaloniaTests 647 passed / 1 skipped (pre-existing, unrelated);
    FwAvaloniaDialogsTests 288 passed / 0 failed (284 baseline + 4 new fixture tests for the
    hardened DialogLayoutAssert checks).
  • gitlint --commits origin/main..HEAD clean.
  • Semi.Avalonia's actual resolved values (SemiColorBorder, SemiColorText0-3, the
    spacing/radius/height scale) were confirmed against the pinned 11.3.14 tag's real source
    (src/Semi.Avalonia/Tokens/Palette/Light.axaml) and, separately, a live headless resource
    walk under this repo's own TestAppBuilder/FwAvaloniaApp — not assumed from
    documentation or an earlier/different vendor version.
  • The two new DialogLayoutAssert checks are mutation-tested: a fixture with the real defect
    present (unreadable font size; a Margin="0" override defeating the themed group
    separation) fails, and the compliant case passes, for both.
  • No native, COM, or installer files are touched — confirmed via
    git diff --name-only origin/main...HEAD, entirely managed C#/XAML/PowerShell/docs.

This change is Reviewable

johnml1135 and others added 2 commits August 19, 2026 09:18
Replace the Avalonia Fluent theme with Semi.Avalonia + Ursa app-wide,
and rebuild the DataTree detail view on Ursa's Form/FormItem instead
of a hand-built Grid. The theme swap surfaced (and this fixes) real
layout regressions caught via actual screenshots: the pane not
filling its width, labels breaking mid-word, writing-system
abbreviations clipping, section headers centering instead of
left-aligning, duplicated header text. Field visibility on
collapse/expand now computes from the model (DetailVisibility)
instead of toggling realized controls.

Build a shared FieldWorks design-token system on top of that (new
Src/Common/FwAvaloniaTheme project, Light/Dark ThemeDictionaries),
replacing the color/spacing/font-size literals that used to be
scattered across FwAvalonia/FwAvaloniaDialogs. Every token defaults
to aliasing Semi's own semantic color/spacing roles (SemiColorText0,
SemiColorBorder, SemiColorBackground0, the Semi spacing/radius
scale, ...) rather than an independently-invented value; a
FieldWorks-owned value requires a written, checkable reason (see
FwColorTokens.axaml's comments) -- verified against the actual
pinned Semi.Avalonia 11.3.14 resources, not assumed.

Enforce this with Build/Agent/token-hygiene.ps1: unlike
comment-hygiene.ps1, it is not diff-scoped and has no grandfathering
-- every run scans the whole Avalonia surface (including the
Src/LexText/Src/xWorks trees future conversions will land in) and
fails on any hardcoded color/spacing literal. Wired into CI as a
hard failure. A new Build/Src/FwBuildTasks GenerateTokenKeys task
(following liblcm's LcmGenerate precedent, not a Roslyn source
generator) turns a typo'd/renamed token key into a build error
instead of a runtime throw, and bakes literal Thickness values for
the few spots where Avalonia's compiled-XAML x:Static limitation
previously forced a hand-duplicated literal.

Add a reusable fwGroupBox titled-border primitive (the WinForms
GroupBox analog) and apply it to the Options dialog, whose General/
Updates tabs previously applied one uniform spacing value to every
sibling alike -- an unrelated setting boundary read identically to a
label-to-its-field gap. Harden DialogLayoutAssert.AssertNoCrowding
with two general checks that run automatically on every dialog: a
readable-font-size floor, and a minimum gap between fwGroupBox
siblings. Commit a small, curated set of baseline screenshots
(Docs/migration/baseline-screenshots/) so a "this was reviewed and
looks right" claim has a surviving, checkable artifact instead of
living only in an ephemeral, gitignored capture.

Record the load-bearing decisions in docs/adr/0001-0003: aliasing
Semi's semantic tier by default, the whole-tree/no-grandfathering
hygiene gate scoped to the Avalonia surface only, and geometric
layout assertions plus reviewed screenshots instead of automated
pixel-diff visual regression testing.

Also upgrades L10NSharp 10.0.0-beta0004 to beta0014, a prerequisite
for future UI-language work that was investigated and explicitly not
built this branch: every UI-language-change path in FieldWorks,
WinForms and Avalonia alike, already deliberately requires a restart
rather than live-refreshing, so building live switching would be new
engineering inconsistent with the rest of the app, not a gap this
branch needed to close.

Independently rebuilt, retested, and hygiene-checked after every
commit throughout development, not just trusted from agent
self-reports -- caught and fixed a recurring CRLF/LF corruption bug,
an unauthorized subagent-forking-a-subagent race condition, a hygiene
gate that silently scanned zero files off-root, a test that didn't
test what it claimed to, a squash whose commit boundaries didn't
match its own messages, and (via independent adversarial review) a
hygiene gate that enforced a narrower slice than its commit message
claimed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found during PR review-summary alignment: the doc still described a
now-removed architecture (three independent DialogFontSize copies
that "must stay equal") and the pre-token-system value (12px). The
actual, current state: one source (FwSurfaceFontSize in
FwColorTokens.axaml, value 11), resolved by all three consumers
directly, not hand-kept-equal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   11m 7s ⏱️ +15s
5 811 tests +27  5 730 ✅ +27  81 💤 ±0  0 ❌ ±0 
5 820 runs  +27  5 739 ✅ +27  81 💤 ±0  0 ❌ ±0 

Results for commit 41e46a1. ± Comparison against base commit 6c6f9eb.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.35158% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.06%. Comparing base (6c6f9eb) to head (41e46a1).

Files with missing lines Patch % Lines
Src/Common/FwAvalonia/Detail/DetailVisibility.cs 88.67% 3 Missing and 3 partials ⚠️
Src/Common/FwAvalonia/FwSemiLocale.cs 80.00% 2 Missing and 4 partials ⚠️
Src/Common/FwAvalonia/FwThemeResources.cs 66.66% 3 Missing and 1 partial ⚠️
Src/Common/FwAvalonia/FwAvaloniaDensity.cs 94.73% 3 Missing ⚠️
Src/Common/FwAvalonia/Detail/FwFieldControls.cs 95.65% 1 Missing and 1 partial ⚠️
Src/Common/FwAvalonia/Detail/DataTree.cs 98.98% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1083      +/-   ##
==========================================
- Coverage   38.07%   38.06%   -0.01%     
==========================================
  Files        1499     1503       +4     
  Lines      350141   350158      +17     
  Branches    40238    40243       +5     
==========================================
- Hits       133304   133303       -1     
- Misses     187558   187569      +11     
- Partials    29279    29286       +7     
Files with missing lines Coverage Δ
Src/Common/FwAvalonia/CompactDialogStyles.cs 100.00% <100.00%> (ø)
...c/Common/FwAvalonia/Detail/DetailRichTextChrome.cs 79.26% <100.00%> (ø)
Src/Common/FwAvalonia/Detail/FwOptionChooser.cs 84.08% <100.00%> (ø)
.../Common/FwAvalonia/Detail/FwStructuredTextField.cs 78.39% <100.00%> (ø)
Src/Common/FwAvalonia/Detail/HoverReveal.cs 91.35% <100.00%> (ø)
Src/Common/FwAvalonia/Detail/SliceFactory.cs 91.56% <100.00%> (+0.20%) ⬆️
Src/Common/FwAvalonia/FilterableDropdownSupport.cs 83.33% <100.00%> (ø)
Src/Common/FwAvalonia/FwAvaloniaApp.cs 78.57% <100.00%> (+28.57%) ⬆️
Src/Common/FwAvalonia/FwPosChooser.cs 87.82% <100.00%> (ø)
Src/Common/FwAvalonia/FwSemiDensity.cs 100.00% <100.00%> (ø)
... and 25 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants