Skip to content

feat(user-delight) Phase 5 PR-T1: Modern LCD token alignment#166

Merged
jkeeley2073 merged 1 commit into
mainfrom
Dev-Phase5ModernLcdTokenAlignment
May 9, 2026
Merged

feat(user-delight) Phase 5 PR-T1: Modern LCD token alignment#166
jkeeley2073 merged 1 commit into
mainfrom
Dev-Phase5ModernLcdTokenAlignment

Conversation

@jkeeley2073
Copy link
Copy Markdown
Contributor

Summary

Closes 9 🔴 + 4 ⚠️ token-drift findings from docs/PHASE5-DRIFT-AUDIT.md § 1 by aligning Wave 1 visual tokens to the locked Modern LCD spec (docs/ui/themes/modern-lcd.md § Visual system). Foundation for Wave 2 delight surfaces — lands before they calcify against Material defaults.

  • PinballTheme.cs — MudPalette aligned to spec (warm near-black bg-base, warm off-white text, arcade amber, atomic green for grounded, saturated red for refusal). Typography H1–H6 → Barlow Condensed (700 / 500); Default + body → Inter; Roboto stays in the fallback chain.
  • wwwroot/app.css:root spec-mirror tokens (--pw-accent-*, --pw-bg-*, --pw-text-*, --pw-border-*, --pw-font-*, --pw-motion-*) for the slots MudPalette can't model and for stable Wave-2-consumable names. Adds font-feature-settings: "tnum" site-wide and a prefers-reduced-motion override that zeros motion durations.
  • App.razor — single Google Fonts load for Barlow Condensed + Inter + JetBrains Mono + Roboto with preconnect hints. (Self-hosting follow-up task spawned for GDPR / IP-leak posture.)
  • PinballThemeContractTests — new contract tests pinning 10 palette slots to spec hex values + H1–H6 family + weight + Default/Body1/Body2 family. Mechanically prevents palette drift on the next theme edit.

Scope boundaries

Out of scope (deferred per audit § 4 / § 6):

  • Header / footer / nav-link rework — PR-F-chrome-nav-rework and PR-F-chrome-footer follow with file-disjoint scope.
  • TiltErrorBoundary red-border refresh — --pw-accent-refusal token now exists; consuming CSS rule lands with that component's Wave 2 polish.
  • Theme-state service + theme picker — Wave 3 obligation.
  • Self-hosted fonts — separate follow-up; PR-T1 ships with Google Fonts CDN per the audit's recipe.

Test plan

  • dotnet build PinballWizard.slnx — 0 warnings / 0 errors
  • dotnet test tests/PinballWizard.Web.Tests — 55 / 55 passing (45 prior + 10 new contract tests)
  • git log -1 --format='%an <%ae>' shows 94459922+jkeeley2073@users.noreply.github.com
  • Visual smoke on running app (browser DevTools): bg/text/accent values match spec; tabular figures active on numerics; reduced-motion override disables animations

PR self-audit

Local review: 2 🔴 findings (1 fixed in this PR — added --pw-* spec token aliases including --pw-accent-refusal; 1 deferred — Google Fonts CDN → self-hosted, follow-up task spawned), 3 ⚠️ findings (all fixed: expanded contract test coverage to AppbarBackground/DrawerBackground + H1–H6 FontWeight, corrected misleading "warmer" comment).
Mechanical 10-item self-audit: clean (most items N/A for a pure-theming PR; identity verified personal-noreply).

Closes 9 🔴 + 4 ⚠️ token-drift findings from `docs/PHASE5-DRIFT-AUDIT.md`
§ 1 by aligning the shipped Wave 1 visual tokens to the locked Modern
LCD spec (`docs/ui/themes/modern-lcd.md` § Visual system).

The shipped Wave 1 chrome was structurally on-spec (MudBlazor strict +
custom-for-delight per ADR-0026 § 6) but value-drifted to Material
defaults: clinical greys instead of warm near-blacks, Material amber
instead of arcade amber, default Roboto everywhere instead of Barlow
Condensed display + Inter body + JetBrains Mono. PR-T1 lands the
foundation Wave 2 components will inherit before they calcify against
the wrong tokens.

Changes:

- PinballTheme.cs — MudPalette aligned to spec (Background #0c0b0e,
  Surface #161519, TextPrimary #f4f1ea, TextSecondary #9a9590, Primary
  #ff9a1f, Success #34d96a, Error #ff3b30, Divider #2a282d). Typography
  H1–H6 use Barlow Condensed (700 primary / 500 secondary); Default +
  Body1/2 + subtitles + caption use Inter; Roboto stays in the fallback
  chain.
- wwwroot/app.css — :root custom properties for the spec tokens
  MudPalette can't model (--pw-bg-surface-hi, --pw-accent-mode,
  --pw-border-glow-* alpha tints) plus stable --pw-* aliases for the
  rest of the spec palette so custom delight-surface CSS can reference
  spec names independent of MudBlazor internal classes. Adds
  --pw-font-display / --pw-font-body / --pw-font-mono stacks,
  --pw-motion-* duration tokens, font-feature-settings: "tnum"
  site-wide, and a prefers-reduced-motion override that zeros the
  motion durations and applies !important animation/transition
  duration:0ms.
- App.razor — replaces the Roboto-only Google Fonts <link> with a
  single combined load for Barlow Condensed (500;700), Inter
  (400;500;600;700), JetBrains Mono (400;500), and Roboto (300;400;
  500;700) with preconnect hints. CDN delivery is the audit's
  recipe; self-hosting follow-up tracked separately.
- PinballThemeContractTests — new contract test class pins ten
  PaletteLight slots to spec hex values, asserts Barlow Condensed +
  Roboto fallback for H1–H6 with FontWeight pinning (700 for H1–H4,
  500 for H5–H6), and asserts Inter + Roboto fallback for Default +
  Body1/2. Mechanically prevents palette drift the next time the
  theme is touched. MudColor's #RRGGBBAA normalization stripped via
  RGB-only slice (..7) to compare against the spec's 7-char hex.

Out of scope (deferred per audit § 4 / § 6):
- Header / footer / nav-link rework (PR-F-chrome-nav-rework +
  PR-F-chrome-footer follow with file-disjoint scope).
- TiltErrorBoundary red-border + display-typography refresh —
  --pw-accent-refusal token now exists; consuming CSS rule lands
  with that component's Wave 2 polish.
- Theme-state service + theme picker (Wave 3 obligation).
- Self-hosted fonts (follow-up task spawned for GDPR / IP-leak
  posture; PR-T1 ships with Google Fonts CDN per audit recipe).

Tests: 1127 → 1137 (+10 PinballThemeContractTests). Web build clean,
solution build clean (0 warnings, 0 errors).

Local review: 2 🔴 findings (1 fixed in this PR — added --pw-* spec
token aliases including --pw-accent-refusal; 1 deferred — Google Fonts
CDN → self-hosted, follow-up task spawned), 3 ⚠️ findings (all fixed:
expanded contract test coverage to AppbarBackground/DrawerBackground +
H1–H6 FontWeight, corrected misleading "warmer" comment).
Mechanical 10-item self-audit: clean (most items N/A for a pure-
theming PR; identity verified personal-noreply).
@jkeeley2073 jkeeley2073 added the claude-code Generated with Claude Code label May 9, 2026
@jkeeley2073 jkeeley2073 merged commit 2c4b330 into main May 9, 2026
5 checks passed
@jkeeley2073 jkeeley2073 deleted the Dev-Phase5ModernLcdTokenAlignment branch May 9, 2026 22:34
jkeeley2073 added a commit that referenced this pull request May 10, 2026
…o, Roboto) (#172)

Removes the Google Fonts CDN dependency from PinballWizard.Web and ships twelve woff2 weights as static web assets under wwwroot/fonts/, with @font-face declarations + font-display: swap in app.css.

Why: PinballWizard is a customer-facing showcase / reference app. The LG Munchen I ruling (Az. 3 O 17493/20, January 2022) classified Google Fonts CDN as a GDPR violation because it leaks visitor IPs to Google on every page load. Per CLAUDE.md section Showcase obligations, the demo must not exhibit patterns a privacy-aware prospect would flag. PR-T1's local review (#166) flagged this as a critical finding and deferred the fix to this scoped task because self-hosting expands scope beyond a token-alignment recipe.

Provenance: woff2 binaries taken verbatim from the @fontsource v5.2.5 npm packages on jsdelivr (which re-package the canonical upstream font files unmodified). Each family ships its upstream LICENSE.txt with the original copyright holder preserved (Inter Project Authors, Barlow Project Authors, JetBrains Mono Project Authors, Roboto Project Authors -- all OFL 1.1).

Coverage: latin subset only, twelve weights matching the families and weights loaded by App.razor (Inter 400/500/600/700, Barlow Condensed 500/700, JetBrains Mono 400/500, Roboto 300/400/500/700).

Tests: 1209 (Scraper) + 41 (Web) = 1250 passing, 0 warnings.

Local review: 0 critical / 2 minor / 11 ok. Mechanical 10-item self-audit: clean. Identity verified personal-noreply.
jkeeley2073 added a commit that referenced this pull request May 10, 2026
…ransparency surface (#171)

Closes the 1 🔴 finding from `docs/PHASE5-DRIFT-AUDIT.md` § 2 by adding
the chrome footer that was missing from Wave 1. Coverage-statement copy
is locked from `docs/ui/screens/empty-landing.md` § Section 4 and now
renders on every screen — not just the empty-landing surface — per the
answer-with-citations § Screen zones #5 spec.

This is the third (final) PR in the Phase 5 Wave 1 drift-audit fix
roadmap. PR-T1 (#166) shipped token alignment;
PR-F-chrome-nav-rework (#169) simplifies the header to a single "What
we cover" link and relocates Status to the footer added here.

Changes:

- BrandFooter.razor (NEW) — chrome surface in Components/Layout/.
  MudBlazor-strict (MudText + MudLink + native <footer> landmark only).
  Renders the locked coverage statement, the "What we cover →" link to
  /about styled with Color.Success (= --accent-grounded #34d96a per
  the PR-T1 spec alignment), the project name, the canonical GitHub
  link (Target=_blank + rel="noopener noreferrer" tabnabbing protection),
  and the Status link relocated from the prior 4-link header.
- MainLayout.razor — mounts <BrandFooter /> inside <MudLayout> after
  </MudMainContent>. Two-line edit.
- BrandFooterTests.cs (NEW) — 5 bUnit tests pin the footer landmark
  + aria-label, the locked coverage-statement copy, the "What we
  cover" link target + label, the GitHub link's tabnabbing-safe
  attributes, and the relocated Status link.
- MainLayoutTests.cs — extended with a drift guard that asserts
  BrandFooter mounts in the rendered tree (catches a future
  MainLayout edit silently dropping the footer).

The coverage-statement test is the load-bearing one — without it, the
next "let's tighten the footer copy" edit can quietly drop the
"first-party data on 8 active manufacturers" line that establishes
the project's coverage-transparency posture (ADR-0027 § 4).

Sequencing note: this branch was forked off origin/main *before*
PR #169 (chrome-nav-rework) merges, so the file-disjoint Status link
appears in BOTH the existing BrandHeader.razor (header) and the new
BrandFooter.razor (footer) until #169 lands. Merge order should be
#169 first, then this PR — at which point Status appears once, in the
footer, per ADR-0027 § 1.

Tests: 55 → 61 passing in Web.Tests (+5 BrandFooterTests + 1
MainLayoutTests drift-guard). Solution build clean (0 warnings /
0 errors).

Local review: 0 actionable 🔴 (the 1 🔴 flagged about MudLink
silently dropping rel was a false alarm — the bUnit test
mechanically verifies rel="noopener noreferrer" reaches the
rendered <a>, and that test passed; MudLink supports attribute
splatting in this version. The other 🔴 about Status duplication
is the merge-order note above, not a code-change concern). 1 ⚠️
(file-header comment was too long; tightened in this PR — kept
the spec authority + ADR citations only, dropped the rationale
prose). Mechanical 10-item self-audit: 0 🔴 / 0 ⚠️ / 5 ✅ / 5 N/A.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-code Generated with Claude Code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant