V3-16: visual identity (token-level pass)

Direction written before code, per the ticket's own step 1: safety orange stays primary (authentic to the domain, not decorative), a new instrument-blue tertiary is reserved for reference readings (max/average) vs the live figure, ground warmed fractionally, no new font, no motion -- both argued for directly rather than left undecided.

Instrument blue reuses RideMap's existing slow-speed gradient color rather than inventing a new one, so 'same colour, same meaning' holds between the map and stat rows. contrastRatio() implements WCAG 2.x directly and is asserted (11 tests, both themes, AA normal/large) rather than eyeballed. Applied to StatRow's new 'reference' flag on record screen and trip detail's Max/Avg speed rows -- a token-level + targeted pass, not an exhaustive re-skin, named as a deliberate scope reduction alongside the two criteria (outdoor sunlight verification, golden tests) this environment genuinely cannot satisfy.
This commit is contained in:
2026-08-17 19:50:08 -05:00
parent e4ffafe9e0
commit 1bcc6f1c5a
7 changed files with 209 additions and 10 deletions

View File

@@ -8,21 +8,31 @@ library;
import 'package:flutter/material.dart';
// Matches the launcher icon: near-black tarmac with a safety-orange accent.
// Matches the launcher icon: near-black tarmac with a safety-orange accent. V3-16
// warmed the ground fractionally (asphalt, not a generic dark-mode blue-black) and added
// an instrument-blue tertiary reserved for reference readings -- see the ticket's
// Direction section for why.
const _accent = Color(0xFFFF5722);
const _ground = Color(0xFF101418);
const _surface = Color(0xFF181D23);
const _ground = Color(0xFF120F0D);
const _surface = Color(0xFF1C1917);
const _ink = Color(0xFFF2F5F8);
const _outline = Color(0xFF66727E);
const _outline = Color(0xFF6E7681);
const _danger = Color(0xFFE53935);
/// Reference readings only (a max, an average) -- never the live figure, which stays
/// [_accent]. The same colour [RideMap] already uses for its slowest speed bucket,
/// reused rather than invented, so "instrument blue" means one thing everywhere.
const _instrumentBlue = Color(0xFF4FC3F7);
const ripprColors = ColorScheme.dark(
primary: _accent,
onPrimary: Color(0xFF100A06),
secondary: _outline,
tertiary: _instrumentBlue,
onTertiary: Color(0xFF042433),
surface: _surface,
onSurface: _ink,
surfaceContainerHighest: Color(0xFF212832),
surfaceContainerHighest: Color(0xFF262220),
onSurfaceVariant: _ink,
outline: _outline,
error: _danger,
@@ -53,7 +63,7 @@ ThemeData ripprTheme() {
foregroundColor: _ink,
elevation: 0,
),
dividerTheme: const DividerThemeData(color: Color(0xFF2A323C), space: 1),
dividerTheme: const DividerThemeData(color: Color(0xFF322C28), space: 1),
);
}
@@ -64,10 +74,17 @@ const _mountedGround = Color(0xFFF5F3EF);
const _mountedInk = Color(0xFF12100D);
const _mountedAccent = Color(0xFFD84315);
// V3-16: the same instrument/reference split as the pocketed theme, darkened enough to
// hold WCAG AA against a near-white ground -- the mounted theme's own reason to exist
// (sunlight, a visor) is not touched by this, only extended.
const _mountedInstrumentBlue = Color(0xFF01579B);
const ripprMountedColors = ColorScheme.light(
primary: _mountedAccent,
onPrimary: Color(0xFFFFFFFF),
secondary: Color(0xFF4A4034),
tertiary: _mountedInstrumentBlue,
onTertiary: Color(0xFFFFFFFF),
surface: Color(0xFFFFFFFF),
onSurface: _mountedInk,
surfaceContainerHighest: Color(0xFFE7E2D9),
@@ -107,6 +124,18 @@ ThemeData ripprMountedTheme() {
);
}
/// WCAG 2.x's contrast ratio: `(L1 + 0.05) / (L2 + 0.05)`, `L1` the lighter of the two
/// relative luminances. AA requires 4.5:1 for normal text, 3:1 for large text (18pt, or
/// 14pt bold) -- see the tests in `theme_test.dart` for V3-16's acceptance criterion,
/// which asserts this directly rather than trusting a palette pick by eye.
double contrastRatio(Color a, Color b) {
final la = a.computeLuminance();
final lb = b.computeLuminance();
final lighter = la > lb ? la : lb;
final darker = la > lb ? lb : la;
return (lighter + 0.05) / (darker + 0.05);
}
/// Monospace for anything numeric.
///
/// Digits that change every second must not make the layout jitter, which proportional