Refresh the Flutter snapshot: UI-01 through UI-09, plus a fresh installable APK

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
This commit is contained in:
2026-08-24 14:41:35 -05:00
parent 4c634354bd
commit 587f75eab4
42 changed files with 4401 additions and 89 deletions

View File

@@ -0,0 +1,160 @@
# 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.