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
161 lines
10 KiB
Markdown
161 lines
10 KiB
Markdown
# 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
|
|
`monoDigits` is 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 — retain `monoDigits` layered 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
|
|
1. Update `theme.dart`'s `ripprColors`/`ColorScheme` to the Modern Professional Dark
|
|
values (ground, surface, cards, primary/secondary/tertiary, outline).
|
|
2. Update `bodyFont`/`headlineFont`/`labelFont` to Inter throughout; keep `monoDigits` as
|
|
a distinct style applied specifically to numeric displays, unchanged in spirit from
|
|
today.
|
|
3. Update `roundness` usage (button/card corner radii) from 4px to 8px.
|
|
4. 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.
|
|
5. 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.
|
|
6. **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
|
|
- [ ] `ripprColors` matches Modern Professional Dark's token values
|
|
- [ ] Every screen using `Theme.of(context).colorScheme` picks 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)
|
|
- [ ] `monoDigits` still applies to every numeric display
|
|
- [ ] Mounted mode re-verified against the new brand direction
|
|
|
|
## Tests
|
|
- All of V3-16's `theme_test.dart` contrast 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`/`displayColor` wiring that bug
|
|
came from originally. Do not remove the explicit color-naming discipline while
|
|
restyling.
|
|
- Grep for hardcoded hex literals from the old palette (`0xFFFF5722` and 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.
|