Full UI redesign pass complete: persistent tab shell with an always-visible background map, Modern Professional Dark theme, monochrome dark map tiles, offline skeleton map, a shared GlassPanel/FloatingPill component kit, customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning / Rides History screen rebuilds. 374 tests passing, up from 316. The APK is a fresh release build (debug-signed, no release signing config exists yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
10 KiB
UI-08 — Theme migration to Modern Professional Dark
Depends on nothing · Size S · Status Done
Goal
Replace theme.dart's current safety-orange palette with "Modern Professional Dark" —
the design system actually backing every fetched Stitch screen — so every screen ticket
in this set has one settled palette to build against instead of guessing or patching
colors per screen.
Context
V3-16 (last shipped) deliberately kept and refined the original safety-orange-on-warm-
charcoal identity, with an instrument-blue tertiary for reference readings. That work is
good and tested, but it is not the palette the Stitch mockups use — confirmed by
hex-matching every color in the four fetched screens' HTML against all three design
systems in the Stitch project (docs/design/stitch-export/README.md). If this redesign
ships, V3-16's direction is superseded, not extended.
Full spec: docs/design/stitch-export/design-system-modern-professional-dark.md.
Design
- Ground: deep dark gray
#0a0a0c(not pure black — "a true premium feel without the harshness of pure black," per the design system's own doc), stepped up through#131313(surface) and#16161a(cards) for elevation. - Primary: Professional Blue — rendered as
#4090fe(container) /#aac7ff(on-dark-surface primary) /#002f64(on-primary text). This replaces safety-orange as the accent everywhere: live telemetry, active nav indicator, primary buttons, the user's own location dot. - Secondary/tertiary: muted slate-blue (
#aec7f6/#2e476f) and a warm orange (#ffb68c/#e3711f) reserved for tertiary accents (the design system's own spec doesn't define a strict "reference vs. live" split the way V3-16's tertiary did — decide during implementation whether to keep that instinct using this palette's secondary color, or drop it; either is defensible, but pick one and apply it consistently rather than leaving it ambiguous per screen). - Typography: Inter, exclusively, for every role (headline/body/label) — this drops
the multi-font split V3-16 and the original theme used (monospace for numbers via
monoDigitsis a separate, orthogonal decision from V3-16 worth keeping regardless of which color palette wins, since "digits must not jitter" is a real constraint, not a branding choice — retainmonoDigitslayered under Inter-family theming). - Shape: 8px rounding (
ROUND_EIGHT), up from the current 4px. - Depth: tonal layering + a soft blue glow on elevated/active elements, no shadows — matches V3-16's existing "no shadows, translucency for depth" instinct, just with a different accent color for the glow.
- Contrast: the design system's own doc claims AAA-level contrast for functional
text against the dark backdrop — verify this the same way V3-16 did (a
contrastRatio()helper and real assertions), don't take the claim on faith.
Implementation
- Update
theme.dart'sripprColors/ColorSchemeto the Modern Professional Dark values (ground, surface, cards, primary/secondary/tertiary, outline). - Update
bodyFont/headlineFont/labelFontto Inter throughout; keepmonoDigitsas a distinct style applied specifically to numeric displays, unchanged in spirit from today. - Update
roundnessusage (button/card corner radii) from 4px to 8px. - Re-run V3-16's
contrastRatio()assertions against the new palette; adjust any color that fails AA before shipping, exactly as V3-16 did for the palette it replaces. - Decide and document the reference-vs-live color convention (see Design) rather than leaving V3-16's tertiary-for-reference-readings instinct undecided under the new palette.
- Mounted mode (V3-05) needs its own pass, not an automatic inheritance — it's a separate high-contrast daylight theme with its own palette, tuned against a real sunlight/visor constraint. Confirm it still holds up in spirit (still legible, still AA-compliant) under the new brand direction; V3-13's real-ride verification is still the only way to confirm the actual sunlight legibility claim, same caveat V3-16 already recorded.
Acceptance criteria
ripprColorsmatches Modern Professional Dark's token values- Every screen using
Theme.of(context).colorSchemepicks up the new palette with no per-screen hardcoded color left over from the old theme contrastRatio()assertions pass against the new palette (AA normal/large, same thresholds V3-16 established)monoDigitsstill applies to every numeric display- Mounted mode re-verified against the new brand direction
Tests
- All of V3-16's
theme_test.dartcontrast assertions, re-pointed at the new color values, still pass - The existing black-on-black regression guard (
text is legible against the dark ground) still passes - Widget: a representative sample of screens render with the new palette (no leftover hardcoded orange/old-tertiary-blue literals)
Risks
- Regressing the black-on-black guard is the named risk V3-16 itself called out —
this ticket touches the same
textTheme/bodyColor/displayColorwiring that bug came from originally. Do not remove the explicit color-naming discipline while restyling. - Grep for hardcoded hex literals from the old palette (
0xFFFF5722and friends) across the UI layer before considering this done — a few call sites (e.g.RideMap's speed gradient) reference specific hex values directly rather than through the theme, and those need a deliberate decision, not an accidental miss.
Out of scope
Any screen-specific redesign (UI-05/06/07) — this ticket only changes the token layer those screens then build against.
Outcome
lib/src/ui/theme.dart's ripprColors now carries Modern Professional Dark's values:
ground #0a0a0c (the design system's own surface-main, not its plain background
token — the spec's prose is explicit that #0a0a0c is the background, with #131313
one step up and #16161a a second step up for cards, a three-level stack the old
two-level _ground/_surface pair didn't have room for). Added ripprCardColor
(#16161a) to carry the third level, since ColorScheme has no free surface slot for it
once surfaceContainerHighest is spent on the selected/highlighted state. Primary is
Professional Blue (#4090fe); tertiary is a warm orange (#ffb68c).
Decision recorded per the ticket's own prompt: the design system doesn't specify a
reference-vs-live split itself, so one was chosen and applied everywhere consistently —
tertiary orange for StatRow's reference values (a max, an average), inverted from
V3-16 where blue held that role, because blue is now the primary colour and reusing it
for reference readings would have made the two indistinguishable. RideMap's speed
polyline gradient — flagged explicitly in the ticket's Risks as a hardcoded hex literal —
now lerps colors.secondary (the theme's own cool slate-blue) to colors.primary
instead of a hardcoded 0xFF4FC3F7, for the same reason: the old literal was itself
blue, and lerping blue-to-blue under the new primary would have washed the gradient into
one hue.
Shape: added ripprRadiusSmall (8px) and ripprRadiusLarge (16px) constants
matching the spec's small-component/large-container split, and a CardThemeData using
the large radius and ripprCardColor. RideMap's card-mode ClipRRect (previously a
hardcoded 12px) now uses ripprRadiusLarge. Deliberately did not add a global
ElevatedButtonTheme/OutlinedButtonTheme shape override — RecordScreen's
Start/Pause/Stop/Discard buttons rely on Material 3's default StadiumBorder for their
pill shape, which already matches the Stitch mockups' own pill buttons; forcing an 8px
rectangle there would have been an unrequested regression, not a spec application.
Inter was not wired in. The design system calls for it exclusively, but the codebase
has no bundled font asset and no existing bodyFont/headlineFont abstraction to retarget
— setting TextTheme.apply(fontFamily: 'Inter') with nothing registered under that name
would silently fall back to the platform default (Roboto), which is exactly the class of
invisible failure ripprTheme()'s own bodyColor/displayColor fix exists to prevent
(the "Compose Surface" comment in the file). Left as a documented, deliberate follow-up
rather than shipping a fontFamily string that resolves to nothing. monoDigits is
untouched and still applies to every numeric display, as required.
Contrast: re-ran theme_test.dart's contrastRatio() assertions against the new
palette (no values needed adjusting — primary-on-ground, tertiary-on-ground, and
onSurface-on-ground all clear their AA thresholds with considerable margin: roughly
6.3:1, 11.6:1, and 15.3:1 respectively, well past the 4.5:1/3:1 bars). Mounted mode
(V3-05) was left unchanged — its own separate high-contrast daylight palette isn't
covered by the Modern Professional Dark spec, its orange accent isn't a brand clash the
way the pocketed theme's safety-orange was, and V3-13's real-ride sunlight-legibility
claim depends on the specific values already tuned there. Its existing contrast tests
were re-run, unchanged, and still pass.
Tests: flutter analyze clean. flutter test green at 315 tests, no count change —
this ticket touched no test files, only re-pointed ripprColors's literals and reused
the same theme_test.dart/ride_map_test.dart assertions the diff didn't need to alter
because they read ripprTheme().colorScheme rather than hardcoding old hex values.
Android emulator verification (Medium_Phone_API_35): checked Map/Settings/Rides
tabs. Buttons, switches, the segmented Metric/Imperial control, section labels, and the
Save/Clear text actions all render in Professional Blue; the reference "Max speed"
figure renders in the new tertiary orange, clearly distinct from the live "0" headline
figure (unchanged ink) and from the Pause button's blue; the bottom nav's active-tab
indicator picked up a blue-tinted pill automatically (Material 3 derives it from the
ColorScheme, not something this ticket configured directly) rather than the old
neutral grey. No leftover orange/old-instrument-blue literals were visible anywhere.