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
11 KiB
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:
DraggableResizableHudWidgets 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) anderror-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
outlinecolor) and the ridden-so-far path (colored by the existing speed gradient fromRideMap) 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
- Rebuild
RecordScreen's body against UI-01's full-opacity Map tab background instead of an embeddedRideMapcard. - Replace the current
StatRow-based stats card with UI-04's HUD widgets, wired to the sameRecordUiState/live telemetry stream already powering the old layout — no new data plumbing, only new presentation. - Rebuild
_Controls/_PrimaryButton/_SecondaryButtonas the two full-bleed icon buttons; keep the existing state-machine logic (RecordUiState.isIdle/isRecording/ isPaused) driving which controls show, per the current_Controlswidget. - 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. - 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 opaqueCardmounted 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 screenwidget 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 (
GlassPanelblur 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.