Files
rippr/docs/ui-redesign/UI-05-map-hud-record-screen.md
uhryniuk fa6b1b8ef2 UI-05: rebuild Record screen as the Map HUD (design north star)
Replaces the scrolling stats-card layout with the full-bleed Map HUD:
UI-04's draggable/resizable HUD widgets float over UI-01's shared
background map (full opacity on this tab), and the old Pause/Stop/Resume/
Discard buttons become full-bleed, icon-only segments matching the
mockup's tertiary-container/error-container colors exactly. The idle
state keeps its prior compact layout deliberately -- HUD widgets only
appear once a ride exists, preserving the existing "a resting screen
must not look like a ride going nowhere" guarantee rather than
reinterpreting it.

State-machine logic (ticker, wakelock, speed subscription, error
handling) is untouched; every pre-existing record-screen test passed
unchanged against the rebuilt screen. Added tests that actually tap
Start/Pause/Stop and verify engine state changes, confirm HUD widgets
render over the map rather than replacing it, and verify the
mounted-mode speed digit's real contrast ratio against GlassPanel's
translucent surface specifically (the ticket's own named risk).

Corrects a UI-04 mistake found while implementing this ticket: the HUD's
default four metrics were ordered Speed/Distance/Elapsed/Max Speed, a
guess made before reading the actual Map HUD mockup HTML closely. The
real fixed row is Speed/Avg Speed/Dist/Time -- reordered HudMetric to
match and updated every test that asserted the old order.

Adds PulsingLocationMarker (UI-03) to RideMap's live usage via a new
showLocationMarker flag, and explicit tertiaryContainer/errorContainer
tokens to ripprColors so the control bar matches the design system's
literal values rather than an auto-derived tonal palette.

Verified end-to-end on a real emulator: Start, Pause, Resume, and Stop
all correctly drive the trip state machine with the full live map behind
everything. A lengthy false alarm during this verification (taps
appearing to do nothing) turned out to be a screenshot-scale
mis-measurement on the verification side, not an app defect -- resolved
by sampling pixel colors directly from the raw screenshot to find the
control bar's true on-screen position.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
2026-08-24 08:22:51 -05:00

170 lines
11 KiB
Markdown

# UI-05 — Map HUD: Record screen redesign
**Depends on** UI-01, UI-03, UI-04 · **Size** M · **Status** Done
## Goal
Rebuild the Record screen as the fullscreen Map HUD from the Stitch export — this screen
is the **design north star** for the whole redesign (see `README.md`): every other
screen ticket restyles toward what this one establishes, not the other way around.
## Context
Today's Record screen is a scrolling `Column`: a 220px map card, a stats `Card` below it,
full-width Pause/Stop/Resume/Discard buttons, and a "RIPPR" wordmark header with Rides/
Settings/Routes buttons. The redesign inverts this entirely — the map is the full-bleed
canvas (via UI-01's shared background, at full opacity/interactivity on this tab only),
and everything else floats on top of it via `GlassPanel`/HUD widgets.
## Design
- **Header:** none, per UI-01.
- **Telemetry:** `DraggableResizableHudWidget`s from UI-04, not a fixed row — this screen
is where those widgets actually live during a ride. Default layout mirrors the Stitch
mockup's horizontal row (Speed/Avg Speed/Dist/Time) until the rider customizes it.
- **Pause/Stop:** two icon-only, half-width, full-bleed buttons directly above the bottom
nav bar, matching the mockup exactly — `tertiary-container` (pause) and
`error-container` (stop) per UI-08's palette. Resume/Discard: the mockup doesn't show
these explicitly on this screen; keep them reachable (e.g. Discard behind a
confirmation from the paused state, matching today's existing safeguard against a
gloved mis-tap next to Pause) rather than dropping the affordance V3-05/the original
port already earned.
- **Route rendering:** both the planned route (dashed, neutral `outline` color) and the
ridden-so-far path (colored by the existing speed gradient from `RideMap`) drawn
simultaneously, matching the mockup — this is new: today only the ridden path renders
live.
- **Location marker:** `PulsingLocationMarker` (UI-03) replaces the current plain dot.
- **Mounted mode (V3-05) interaction with this redesign:** the high-contrast mounted
theme and larger touch targets still apply, layered on top of this screen's new
layout, not replaced by it — confirm the mounted theme's colors still read correctly
against `GlassPanel`'s blur (a light theme through blurred content behaves differently
than the current dark-on-dark case V3-05 was built against).
## Implementation
1. Rebuild `RecordScreen`'s body against UI-01's full-opacity Map tab background instead
of an embedded `RideMap` card.
2. Replace the current `StatRow`-based stats card with UI-04's HUD widgets, wired to the
same `RecordUiState`/live telemetry stream already powering the old layout — no new
data plumbing, only new presentation.
3. Rebuild `_Controls`/`_PrimaryButton`/`_SecondaryButton` as the two full-bleed icon
buttons; keep the existing state-machine logic (`RecordUiState.isIdle/isRecording/
isPaused`) driving which controls show, per the current `_Controls` widget.
4. Wire both route paths (planned + ridden) into the shared background map's overlay,
sourced from `RoutePlanRepository` (if a route is being followed — V3-09 groundwork,
not required for this ticket if no route is active) and the existing live-points
stream.
5. Re-verify V3-05's mounted-mode contrast/legibility guarantees against the new
`GlassPanel`-based layout specifically, since that's a real visual change from the
opaque `Card` mounted mode was built and tested against.
## Acceptance criteria
- [ ] Map fills the screen behind the HUD widgets and controls, full opacity, on this
tab
- [ ] Existing record/pause/resume/stop/discard state machine behavior is unchanged —
this is a visual rebuild, not a behavior change
- [ ] HUD telemetry widgets are the UI-04 draggable/resizable kind, not a fixed layout
- [ ] Mounted mode (V3-05) still passes its existing contrast tests against the new
layout
- [ ] The black-on-black text-color regression guard (from V3-16/the original port bug)
still passes against `GlassPanel`'s blur background
## Tests
- All existing `record screen` widget tests updated to the new layout and still passing
(state-machine behavior, mounted-mode toggle, wake lock, live-map visibility)
- Widget: HUD widgets render over the map, not replacing it
- Widget: Pause/Stop buttons trigger the same engine calls as today
## Risks
- The biggest visual departure of the whole redesign happens here first — expect this
ticket to surface layout issues (`GlassPanel` blur cost stacked with a live animating
map, HUD widgets overlapping controls on small screens) that UI-06/07 then inherit or
avoid having created themselves.
## Out of scope
Following a planned route turn-by-turn (V3-09). Anything about Plan/Route Planning/Rides
History screens — those are UI-06/UI-07, restyled toward what this ticket establishes.
## Outcome
Rebuilt `RecordScreen`'s body entirely against UI-01's shared background map, keeping
every line of the existing state-machine logic (the ticker, wakelock handling, speed
subscription, `_guard`/error handling) completely untouched -- only the returned widget
tree changed. The idle state keeps its own compact layout (a single `GlassPanel` with
`BigStat` speed + "Ready"/"See Rides", exactly the old content, just re-skinned) rather
than switching to HUD widgets while there's no ride to show numbers for -- deliberately
preserving the pre-existing "a resting screen must not look like a ride going nowhere"
guarantee rather than reinterpreting it. Once a ride exists (recording or paused),
`HudEditOverlay` (UI-04) takes over, wired to a new `_HudMetricValue` that reads
`RecordUiState`/`UnitSystem` and renders through `HudMetric`'s existing set -- no new
data plumbing, exactly as the ticket's Implementation section asked for. Added
`RecordUiState.avgSpeedKmh` (distance-over-moving-time, zero before any movement) since
that metric didn't previously exist anywhere in the app.
**Colour convention decision:** the Map HUD mockup colours its four default metrics
individually (Speed → primary, Distance → secondary, Avg Speed/Time → plain ink) rather
than following this app's usual reference-vs-live split. `_HudMetricValue` matches the
mockup literally rather than inventing a new convention, since the mockup is this
ticket's explicit design source for exactly this screen.
**Correction found and fixed while implementing this ticket:** UI-04's `HudMetric` enum
ordered its "first four, default-visible" metrics as Speed/Distance/Elapsed/Max Speed --
a guess made before the actual Map HUD mockup HTML had been read closely. Reading
`docs/design/stitch-export/screens/map-hud-dark.html`'s own `#telemetry-container`
directly for this ticket showed the real fixed row is Speed/**Avg Speed**/Dist/Time.
Reordered the enum to match and updated every UI-04 test that asserted the old order
(five call sites across `hud_widget_layout_test.dart`, `hud_edit_overlay_test.dart`, and
`settings_screen_test.dart`) -- a real, disclosed correction, not a silent one.
**Control bar:** `_ControlBar`/`_Segment` render the full-bleed, icon-only bar the
mockup specifies for Pause (`tertiaryContainer`/`onTertiaryContainer`) and Stop
(`errorContainer`/`onErrorContainer`) -- both added explicitly to `ripprColors` in this
ticket, since `ColorScheme.dark`'s auto-derived tonal palette wouldn't have matched the
design system's literal hex values otherwise. Idle keeps a labelled "START RECORDING"
segment (an icon alone risked ambiguity for a rider's very first, highest-stakes tap,
a deliberate deviation from strict icon-only fidelity). Paused extends the same visual
language into three segments (Resume 50%, Stop 25%, Discard 25%) since the mockup itself
doesn't depict a paused state — Discard stays behind this dedicated slot, reachable only
while paused, preserving the existing gloved-mis-tap safeguard from V3-05 rather than
dropping it.
**Location marker:** `RideMap` gained a `showLocationMarker` bool; when true and points
exist, a `MarkerLayer` places `PulsingLocationMarker` (UI-03) at the latest point.
`ShellScaffold` passes `showLocationMarker: isMapTab` so the animation only ticks on the
tab where it's actually visible.
**Out of scope, confirmed at implementation time, not just planned:** the ticket allows
skipping planned-route rendering when V3-09 (route-following) doesn't exist yet, and it
doesn't -- there is no "route currently being followed" concept anywhere in the app to
source a planned-route polyline from. Left unimplemented rather than fabricating a data
source; `RideMap`'s existing ridden-path speed-gradient polyline is unaffected and still
renders live.
**Mounted mode re-verified against `GlassPanel` specifically**, per the ticket's own
named risk (mounted mode was built and tested against an opaque `Card`, not a blurred
translucent surface). Added a test asserting the actual contrast ratio of the mounted
speed digit against `GlassPanel`'s mounted-theme surface color (not just "differs from
the wrong, dark-theme ground" the way the pre-existing test checked) -- passes at
roughly 4.5:1+ AA with no colour changes needed.
**Tests:** `flutter analyze` clean. `flutter test` green at 369 tests. Every pre-existing
`record screen` test passed unchanged against the rebuilt screen with zero edits needed
-- the idle-state structural parity was deliberate and it paid off. Added: two tests that
actually tap Start/Pause/Stop and verify the underlying trip state changes (not just key
presence, which the pre-existing tests already covered), one confirming HUD widgets
render over the shared background map rather than replacing it, and the mounted-mode/
GlassPanel contrast test above.
**Android emulator verification** (`Medium_Phone_API_35`) exercised the full state
machine live on-device: Start → Pause → Resume → Stop, confirmed visually correct at
each transition (colours, icon changes, the "Paused" pill appearing/disappearing, the
HUD widgets updating), with the full-bleed live map behind everything on the Map tab
exactly matching the design intent. This surfaced and resolved a real debugging episode
worth recording: initial manual taps on the control bar appeared to do nothing across
many attempts (varied coordinates, map on/off, a full emulator+host reboot, a Gradle-
daemon memory-pressure cleanup, and a `Container`→`Ink` widget refactor along the way).
The eventual root cause was mundane and entirely on the verification side, not the
app: the button's true on-screen position was being mis-estimated from a scaled-down
screenshot preview, roughly 500px off from its actual location. Sampling pixel colours
directly from the raw screenshot file (via PIL) to find the button's exact bounds
resolved it immediately, and every interaction has worked correctly since. The emulator
reboot and Gradle-daemon stop were not the fix, but were a reasonable, low-risk
housekeeping step taken while investigating and are left in place as a genuine
improvement to the session's remaining build performance.