Adopt Semi.Avalonia and a FieldWorks design-token system - #1083
Open
johnml1135 wants to merge 2 commits into
Open
Adopt Semi.Avalonia and a FieldWorks design-token system#1083johnml1135 wants to merge 2 commits into
johnml1135 wants to merge 2 commits into
Conversation
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>
Codecov Report❌ Patch coverage is 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
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.ps1is now a hard CI failure — whole-tree, no grandfathering, scoped to the Avalonia surface only. Why that scope, not global: docs/adr/0002.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(newBuild/Src/FwBuildTaskscodegen) turns a stale/typo'd token key into a compile error — what makes "whole-tree, no exceptions" trustworthy rather than just strict-sounding.DialogLayoutAssertgained 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
ThemeDictionariesstructure 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 coversthe 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.axamlin the dialogs project) asks fora value one of two ways:
{DynamicResource FwLabelBrush}or, for Semi's own roles now referenceddirectly,
{StaticResource SemiColorDanger}.FwThemeResources.RequireBrush(GeneratedTokenKeys.FwLabelBrush)— acompile-time-checked constant, not a raw string, resolved at point-of-use via
Application.Current.TryGetResource, never cached in a static field (Application.Currentis null under
beforefieldinitbefore the app starts).The key resolves through merged
Application.Resources:Src/Common/FwAvaloniaTheme'sFwColorTokens.axaml(Light/DarkThemeDictionaries— shared brushes + the oneFwSurfaceFontSize) andDataTreeTokens.axaml(flat, non-themed DataTree layoutdimensions), both merged by
FwAvaloniaApp/PreviewHostAppatInitialize(), plusDialogTheme.axaml's own localDialog*keys merged into that same dictionary and appliedper-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 flatspacing/radius/height scale) that alias them. FieldWorks' tier defaults to aliasing Layer 2
directly; a FieldWorks-owned value requires a written, checkable reason.
GenerateTokenKeys(aBuild/Src/FwBuildTasksMSBuild Task, not a Roslyn generator — seeDecisions below) reads the token
.axamlfiles'x:Keys at build time and emitsGeneratedTokenKeys.g.cs: the compile-time-checked constants above, plus bakedThicknessliteral values (via
Avalonia.Thickness.Parse) for the few spots(
CompactDialogStyles.cs/FwSurfaceStyles.cs) where Avalonia's compiled XAML rejectsx: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 knownDynamicResource-only landmine on this branch is narrower than first assumed: it's specificto
DialogTheme.axaml's own Setters (grafted onto a view's.Stylesat runtime), notThemeDictionariescrossing in general.token-hygiene.ps1is whole-tree and zero-grandfathering, deliberately unlikecomment-hygiene.ps1. The scoped tree is new code with nothing to grandfather; currentdesign-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
-TokenHygienelocally, one slipped-in violation onmainfailsevery unrelated PR touching the tree until fixed — no ratchet/baseline valve exists yet.
GenerateTokenKeysis a custom MSBuild Task, not a Roslyn source generator. Matches thiscodebase'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
prerequisite for it. Investigated directly: every UI-language-change path in FieldWorks —
WinForms and the existing Avalonia port alike — deliberately sets
restartRequired = truerather 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.
discussing the gate's escape valve. Rejected once the actual existing exception (the
x:Staticcompiled-XAML limitation) turned out to already be resolved better byGenerateTokenKeysgenerating the value outright, backed byDuplicateTokenPairConsistencyTests.csas a regression guard — stronger than an unverifiedsuppression comment would have been.
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
fwGroupBoxsiblings.What this does NOT authorize
token-hygiene.ps1-style enforcement onto the WinFormssurface — 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
ThemeDictionariesstructure won't need a rearchitecture to add them later; none has beendesigned, built, or visually 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
were factually backwards:
SemiColorBorderwas described as "opaque, too heavy" for a 1pxdivider when the real value (verified against the pinned
v11.3.14tag's source) is8%-opacity and nearly invisible; Semi's
Text0-3were 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.axamlanddocs/adr/0001was wrong until corrected during review.git commit-treeratherthan 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.ps1 -BuildTests -SkipNative -CommentHygiene -TokenHygiene— 0 errors, bothhygiene gates clean (170 files scanned by
token-hygiene.ps1, 0 violations).FwAvaloniaTests647 passed / 1 skipped (pre-existing, unrelated);FwAvaloniaDialogsTests288 passed / 0 failed (284 baseline + 4 new fixture tests for thehardened
DialogLayoutAssertchecks).gitlint --commits origin/main..HEADclean.SemiColorBorder,SemiColorText0-3, thespacing/radius/height scale) were confirmed against the pinned
11.3.14tag's real source(
src/Semi.Avalonia/Tokens/Palette/Light.axaml) and, separately, a live headless resourcewalk under this repo's own
TestAppBuilder/FwAvaloniaApp— not assumed fromdocumentation or an earlier/different vendor version.
DialogLayoutAssertchecks are mutation-tested: a fixture with the real defectpresent (unreadable font size; a
Margin="0"override defeating the themed groupseparation) fails, and the compliant case passes, for both.
git diff --name-only origin/main...HEAD, entirely managed C#/XAML/PowerShell/docs.This change is