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
This commit is contained in:
@@ -20,6 +20,7 @@ import 'package:latlong2/latlong.dart' as ll;
|
||||
|
||||
import '../../domain/models.dart';
|
||||
import '../../geo/geo.dart' as geo;
|
||||
import '../theme.dart' show ripprRadiusLarge;
|
||||
|
||||
/// Metres. Render-only: a 3-hour ride is ~21,600 points and would jank undecimated.
|
||||
const double simplifyEpsilonM = 5.0;
|
||||
@@ -176,7 +177,8 @@ class _RideMapState extends State<RideMap> with WidgetsBindingObserver {
|
||||
final map = ClipRRect(
|
||||
// The map draws to the edge of its box; clipping keeps it from painting over
|
||||
// adjacent controls, which osmdroid did until it was explicitly bounded.
|
||||
borderRadius: widget.fill ? BorderRadius.zero : BorderRadius.circular(12),
|
||||
borderRadius:
|
||||
widget.fill ? BorderRadius.zero : BorderRadius.circular(ripprRadiusLarge),
|
||||
child: FlutterMap(
|
||||
mapController: _controller,
|
||||
options: MapOptions(
|
||||
@@ -305,9 +307,16 @@ class _RideMapState extends State<RideMap> with WidgetsBindingObserver {
|
||||
|
||||
/// Cool for slow, accent for fast. A single hue ramp reads better through a visor than
|
||||
/// a rainbow.
|
||||
///
|
||||
/// UI-08: was a hardcoded instrument-blue lerped to `colors.primary`. Under the old
|
||||
/// safety-orange theme that gave a clean blue-to-orange ramp; under Modern
|
||||
/// Professional Dark, `colors.primary` is itself blue, so lerping from another
|
||||
/// hardcoded blue would wash the whole gradient into one indistinct hue. Lerping
|
||||
/// from `colors.secondary` (the theme's own cool slate-blue) to `colors.primary`
|
||||
/// (Professional Blue) keeps a real, theme-driven cool-to-accent ramp instead.
|
||||
Color _colorFor(int bucket, double maxSpeed, ColorScheme colors) {
|
||||
if (bucket < 0 || maxSpeed <= 0) return colors.primary;
|
||||
final t = ((bucket * speedBucketKmh) / maxSpeed).clamp(0.0, 1.0);
|
||||
return Color.lerp(const Color(0xFF4FC3F7), colors.primary, t)!;
|
||||
return Color.lerp(colors.secondary, colors.primary, t)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,10 @@ class StatRow extends StatelessWidget {
|
||||
final String value;
|
||||
|
||||
/// V3-16: true for a figure you compare the live reading against (a max, an average)
|
||||
/// rather than the live reading itself. Colours the value with the instrument-blue
|
||||
/// tertiary instead of the default ink, so the same colour means the same kind of
|
||||
/// number everywhere it appears. See the ticket's Direction section.
|
||||
/// rather than the live reading itself. Colours the value with the theme's tertiary
|
||||
/// (UI-08: a warm orange, chosen specifically to read as distinct from the new
|
||||
/// Professional Blue primary) instead of the default ink, so the same colour means
|
||||
/// the same kind of number everywhere it appears. See the ticket's Direction section.
|
||||
final bool reference;
|
||||
|
||||
@override
|
||||
|
||||
@@ -8,40 +8,68 @@ library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// 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(0xFF120F0D);
|
||||
const _surface = Color(0xFF1C1917);
|
||||
const _ink = Color(0xFFF2F5F8);
|
||||
const _outline = Color(0xFF6E7681);
|
||||
const _danger = Color(0xFFE53935);
|
||||
// UI-08: "Modern Professional Dark", the design system actually backing every fetched
|
||||
// Stitch mockup (confirmed by hex-matching, not by trusting the project's declared
|
||||
// default theme — see docs/design/stitch-export/README.md). This supersedes V3-16's
|
||||
// safety-orange identity outright, not an extension of it. Full spec:
|
||||
// docs/design/stitch-export/design-system-modern-professional-dark.md.
|
||||
//
|
||||
// `surface-main` (#0a0a0c), not the spec's plain `background`/`surface` (#131313), is
|
||||
// used as the app ground: the spec's own prose calls out #0a0a0c as *the* background,
|
||||
// with #131313 one step up for a mid-level surface and #16161a another step up for cards
|
||||
// — a three-step stack, not two.
|
||||
const _ground = Color(0xFF0A0A0C);
|
||||
const _surfaceMid = Color(0xFF131313);
|
||||
const _ink = Color(0xFFE5E2E1);
|
||||
const _outline = Color(0xFF8B919F);
|
||||
const _danger = Color(0xFFFFB4AB);
|
||||
|
||||
/// 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);
|
||||
/// Professional Blue. The soul of the interface per the design system's own doc —
|
||||
/// primary buttons, the active nav indicator, the user's own location dot, and (via
|
||||
/// `RideMap`'s speed gradient) the accent end of the live-path colouring. Replaces
|
||||
/// safety-orange as *the* accent everywhere; the app's existing convention of leaving
|
||||
/// headline figures (`BigStat`) in plain ink and reserving colour for `StatRow`'s
|
||||
/// `reference` values is unchanged by this ticket.
|
||||
const _primary = Color(0xFF4090FE);
|
||||
|
||||
/// UI-08 decision (the design system doesn't prescribe this split itself): reference
|
||||
/// readings (a max, an average) get the warm tertiary orange, never the live figure,
|
||||
/// which stays [_primary]. Inverted from V3-16 (blue was the reference colour there)
|
||||
/// because blue is now the primary/live colour — a same-hue reference tone would have
|
||||
/// been visually indistinguishable from the live one it exists to be told apart from.
|
||||
const _referenceOrange = Color(0xFFFFB68C);
|
||||
|
||||
const ripprColors = ColorScheme.dark(
|
||||
primary: _accent,
|
||||
onPrimary: Color(0xFF100A06),
|
||||
secondary: _outline,
|
||||
tertiary: _instrumentBlue,
|
||||
onTertiary: Color(0xFF042433),
|
||||
surface: _surface,
|
||||
primary: _primary,
|
||||
onPrimary: Color(0xFF002F64),
|
||||
secondary: Color(0xFFAEC7F6),
|
||||
tertiary: _referenceOrange,
|
||||
onTertiary: Color(0xFF532200),
|
||||
surface: _surfaceMid,
|
||||
onSurface: _ink,
|
||||
surfaceContainerHighest: Color(0xFF262220),
|
||||
onSurfaceVariant: _ink,
|
||||
surfaceContainerHighest: Color(0xFF353534),
|
||||
onSurfaceVariant: Color(0xFFC1C6D5),
|
||||
outline: _outline,
|
||||
error: _danger,
|
||||
onError: _ink,
|
||||
onError: Color(0xFF690005),
|
||||
);
|
||||
|
||||
/// The near-black ground the whole app sits on.
|
||||
const ripprBackground = _ground;
|
||||
|
||||
/// Cards and modals — one step up from [ripprColors]'s `surface`, per the design
|
||||
/// system's three-level stack ("Surface Strategy": base darkest, cards a step lighter).
|
||||
/// Exposed separately because `ColorScheme` has no fourth surface slot for it that isn't
|
||||
/// already claimed by `surfaceContainerHighest` (reserved here for the selected/
|
||||
/// highlighted state, not the resting card colour).
|
||||
const ripprCardColor = Color(0xFF16161A);
|
||||
|
||||
/// Shape spec: "Rounded (Level 2)" — small components (buttons, inputs) get 8px, large
|
||||
/// containers (cards, modals) get 16px. Two constants, not a single "roundness" figure,
|
||||
/// because the spec explicitly gives them different values.
|
||||
const double ripprRadiusSmall = 8.0;
|
||||
const double ripprRadiusLarge = 16.0;
|
||||
|
||||
ThemeData ripprTheme() {
|
||||
final base = ThemeData(
|
||||
colorScheme: ripprColors,
|
||||
@@ -57,13 +85,25 @@ ThemeData ripprTheme() {
|
||||
// Flutter's equivalent exposure is the default text colour. Setting it explicitly
|
||||
// here, rather than relying on a wrapping widget, means the failure cannot recur by
|
||||
// someone removing a container.
|
||||
// UI-08: the design system specifies Inter exclusively. Not switched to here --
|
||||
// naming a fontFamily with no matching asset bundled would silently fall back to
|
||||
// the platform default (Roboto), the exact class of invisible failure the bodyColor
|
||||
// fix below exists to prevent. Bundling the actual font files (or a vetted offline
|
||||
// build of the google_fonts package) is left as a deliberate follow-up rather than
|
||||
// shipping a fontFamily string that doesn't resolve to anything.
|
||||
textTheme: base.textTheme.apply(bodyColor: _ink, displayColor: _ink),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: _ground,
|
||||
foregroundColor: _ink,
|
||||
elevation: 0,
|
||||
),
|
||||
dividerTheme: const DividerThemeData(color: Color(0xFF322C28), space: 1),
|
||||
cardTheme: const CardThemeData(
|
||||
color: ripprCardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(ripprRadiusLarge)),
|
||||
),
|
||||
),
|
||||
dividerTheme: const DividerThemeData(color: Color(0xFF2D3037), space: 1),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user