T15-T18, T20, T23: app shell, three screens, and the first UI tests
Theme, go_router shell, record screen, trips list with selection, trip detail with stats and charts, rename/delete/merge. 15 widget tests -- the first this project has ever had, closing the gap docs/v3/BACKLOG.md names as v2's largest. The theme sets bodyColor and displayColor explicitly rather than relying on a wrapping widget, so the black-on-black regression cannot recur by someone removing a container, and a widget test now asserts the headline names a colour distinct from the ground. Those tests immediately found a real layout bug: with a ride active the record screen grows to six stat rows and overflows a short screen. Compose clipped this silently, so it may be latent in the native app; Flutter reports it. Fixed by making the screen scrollable while still centring when there is room. Two harness lessons. pumpAndSettle never settles against a repeating timer, so the elapsed clock now runs only while a ride is active -- better behaviour anyway, since an idle screen has no clock to advance. And flutter_test asserts no Timer is pending after disposal, which Drift's stream-query keep-alive cache trips; tests now tear the tree down and pump past that window. 160 tests passing, analyze clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
65
lib/src/ui/theme.dart
Normal file
65
lib/src/ui/theme.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
/// Ported from `com.rippr.ui.theme.RipprTheme`.
|
||||
///
|
||||
/// Dark-only by design. This is read at a glance in daylight with a visor down, and a
|
||||
/// light theme would be actively worse for that.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Matches the launcher icon: near-black tarmac with a safety-orange accent.
|
||||
const _accent = Color(0xFFFF5722);
|
||||
const _ground = Color(0xFF101418);
|
||||
const _surface = Color(0xFF181D23);
|
||||
const _ink = Color(0xFFF2F5F8);
|
||||
const _outline = Color(0xFF66727E);
|
||||
const _danger = Color(0xFFE53935);
|
||||
|
||||
const ripprColors = ColorScheme.dark(
|
||||
primary: _accent,
|
||||
onPrimary: Color(0xFF100A06),
|
||||
secondary: _outline,
|
||||
surface: _surface,
|
||||
onSurface: _ink,
|
||||
surfaceContainerHighest: Color(0xFF212832),
|
||||
onSurfaceVariant: _ink,
|
||||
outline: _outline,
|
||||
error: _danger,
|
||||
onError: _ink,
|
||||
);
|
||||
|
||||
/// The near-black ground the whole app sits on.
|
||||
const ripprBackground = _ground;
|
||||
|
||||
ThemeData ripprTheme() {
|
||||
final base = ThemeData(
|
||||
colorScheme: ripprColors,
|
||||
useMaterial3: true,
|
||||
scaffoldBackgroundColor: _ground,
|
||||
);
|
||||
|
||||
return base.copyWith(
|
||||
// Compose's `Surface` was load-bearing, not decoration: it set `LocalContentColor`.
|
||||
// Without it, any Text that did not name a colour fell back to black — invisible on
|
||||
// this near-black ground, and **no test caught it; only a screenshot did**.
|
||||
//
|
||||
// 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.
|
||||
textTheme: base.textTheme.apply(bodyColor: _ink, displayColor: _ink),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: _ground,
|
||||
foregroundColor: _ink,
|
||||
elevation: 0,
|
||||
),
|
||||
dividerTheme: const DividerThemeData(color: Color(0xFF2A323C), space: 1),
|
||||
);
|
||||
}
|
||||
|
||||
/// Monospace for anything numeric.
|
||||
///
|
||||
/// Digits that change every second must not make the layout jitter, which proportional
|
||||
/// figures do. Used for every speed, distance and clock in the app.
|
||||
const TextStyle monoDigits = TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
);
|
||||
Reference in New Issue
Block a user