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

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) 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.