Files
rippr/docs/ui-redesign/UI-08-theme-modern-professional-dark.md
uhryniuk 13017b8ee8 UI-08: migrate theme to Modern Professional Dark
Replaces V3-16's safety-orange identity with the palette actually backing
every fetched Stitch mockup (confirmed by hex-matching against all three
design systems in the Stitch project, not by trusting its declared default).
Ground drops to #0a0a0c with a three-level surface/card stack, primary
becomes Professional Blue (#4090fe), and reference readings move to a new
tertiary orange since blue is no longer free for that role. Re-verified
theme_test.dart's AA contrast assertions against the new values (all pass
with wide margin) and left mounted mode's separate daylight palette alone,
per the ticket's own reasoning.

Also resolves the ticket's named risk: RideMap's speed-gradient hardcoded
hex literal, which would have washed to a single hue now that primary is
blue, now lerps from the theme's own secondary token instead.

Inter is not wired in -- no bundled font asset exists to back the family
name, and setting one without an asset would silently fall back to Roboto.
Documented as a deliberate follow-up rather than a silent gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
2026-08-23 19:37:25 -05:00

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 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.