UI redesign: 8 tickets from the Stitch export analysis

Same shape as docs/v3/: Goal/Context/Design/Implementation/Acceptance/Tests/Risks/Out-of-scope, written before implementing.

UI-01: persistent 4-tab shell (StatefulShellRoute) replacing the current push/pop stack rooted at Record, with a single shared background map dimmed behind every tab and no header anywhere.
UI-02: animated skeleton map for the no-cache/no-network case, since the map is now a permanent background presence rather than opt-in per screen.
UI-03: shared GlassPanel/FloatingPill/PulsingLocationMarker component kit.
UI-04: draggable, resizable HUD telemetry widgets with a Settings visibility toggle -- split out from the screen redesign itself since it's a genuinely separate piece of engineering (a small widget-layout system, not a reskin).
UI-05: Map HUD/Record screen redesign -- the design north star every other screen ticket restyles toward.
UI-06: Plan & Route Planning -- correct in principle, explicitly restyled toward UI-05 rather than copying the Stitch export's drifted styling.
UI-07: Rides History -- live dimmed map background (not the mockup's static image), search/filter, real per-ride map thumbnails, and real distinct summary stats (fixing the export's 4-identical-cards generation artifact).
UI-08: theme migration to Modern Professional Dark, superseding V3-16's safety-orange direction.

README documents the dependency order, and two categories worth keeping separate: what was named explicitly by the person commissioning this (map-on-every-tab, no header, offline skeleton, customizable HUD) versus what the mockups themselves got inconsistent (nav icon count, duplicate summary cards, wrong active-tab highlight) and should not be copied as intentional.
This commit is contained in:
2026-08-23 18:25:35 -05:00
parent 95a88fa383
commit 822003ed38
9 changed files with 823 additions and 0 deletions

102
docs/ui-redesign/README.md Normal file
View File

@@ -0,0 +1,102 @@
# UI redesign tickets
Same shape as `docs/v3/`: one file per feature, Goal · Context · Design · Implementation ·
Acceptance criteria · Tests · Risks · Out of scope. Written before implementing.
Source material: `docs/design/stitch-export/` — four Stitch-generated screens (Map HUD,
Plan, Route Planning, Rides History) plus the "Modern Professional Dark" design system
actually used by all four. See that directory's `README.md` for how they were fetched and
a correction of an earlier mislabeling.
This is a **navigation-model change**, not a reskin: today's app is a stack rooted at
Record, with Trips/Settings/Routes as pushed children. The new design is a persistent
4-tab shell (Map / Rides / Plan / Settings) with the map visible, at some opacity, behind
every tab — not just the Map tab. Sequenced so the riskiest, highest-blast-radius piece
(the shell) lands first and `flutter test` stays green throughout, the same discipline
v3 held to.
## The tickets
| # | Ticket | Size | Depends on | Status |
|---|---|---|---|---|
| [UI-01](UI-01-tab-shell-background-map.md) | Persistent tab shell with an always-visible background map | L | — | Not started |
| [UI-02](UI-02-offline-skeleton-map.md) | Offline / no-connection skeleton map | S | UI-01 | Not started |
| [UI-03](UI-03-glass-component-kit.md) | Shared floating-glass component kit | M | UI-08 | Not started |
| [UI-04](UI-04-customizable-hud-widgets.md) | Customizable HUD telemetry widgets (drag, resize, visibility toggle) | L | UI-03 | Not started |
| [UI-05](UI-05-map-hud-record-screen.md) | Map HUD: Record screen redesign | M | UI-01, UI-03, UI-04 | Not started |
| [UI-06](UI-06-plan-and-route-planning.md) | Plan & Route Planning redesign | M | UI-01, UI-03 | Not started |
| [UI-07](UI-07-rides-history.md) | Rides History redesign | M | UI-01, UI-03 | Not started |
| [UI-08](UI-08-theme-modern-professional-dark.md) | Theme migration to Modern Professional Dark | S | — | Not started |
## Dependencies
```
UI-01 ──┬──► UI-02
├──► UI-05
├──► UI-06
└──► UI-07
UI-08 ──► UI-03 ──┬──► UI-04 ──► UI-05
├──► UI-06
└──► UI-07
no dependencies: UI-01 · UI-08
```
## Suggested order
1. **UI-01** — the shell. Highest blast radius (touches every screen's entry point), so
get it merged and green before any visual work starts.
2. **UI-08** — theme. Every screen ticket after this needs the palette settled once, not
patched per-screen. Supersedes V3-16's safety-orange direction — see that ticket's
note.
3. **UI-02** — skeleton map. Small, and exercises the background-map plumbing UI-01 just
built while it's fresh.
4. **UI-03** — the component kit (`GlassPanel`, `FloatingPill`, `PulsingLocationMarker`).
Build once, consumed by every screen ticket.
5. **UI-04** — the customizable HUD widget system (drag/resize/persist layout, a
visibility toggle in Settings). This is its own ticket, not folded into UI-05, because
drag-and-resize-with-persisted-layout is a genuinely separate piece of engineering
from "redesign the record screen" — a small widget-layout editor in miniature.
6. **UI-05 / UI-06 / UI-07** — the three screen redesigns, in any order once everything
above is done. Each is independently shippable.
## Things named explicitly by the person who commissioned this, not inferred from the mockups
- **The map is active on every tab**, including Rides History — blurred/dimmed behind
the content cards so it doesn't compete for attention, not hidden. This wasn't visible
in the Stitch export (Rides History's HTML uses a static blurred background image, not
a live map) — it's a deliberate product decision layered on top of the mockups.
- **No header, anywhere, ever** — the stated reason is screen real estate: the point of
the redesign is to show as much map as possible, and a persistent bottom nav plus zero
header chrome is what buys that space back.
- **A no-connection skeleton map** (UI-02), rather than a blank or broken map, since the
map is now a permanent background presence on every tab rather than something opened
deliberately.
- **The telemetry HUD widgets are user-customizable** (UI-04): draggable and resizable by
the rider, and which metrics show up live at all is a Settings toggle. The Stitch
mockup's tap-to-expand interaction is a fixed layout with one temporary focus state;
this goes further — the rider chooses the layout and which stats exist on screen at
all, and it persists.
## The Map screen is the design north star
The four Stitch exports don't agree with each other on styling — expected, since each
was a separate prompt and the AI generation drifted between them. **Map HUD is the
canonical reference** for color usage, icon choice/fill-state, spacing, and component
pattern across the whole redesign. Where another screen's mockup disagrees with Map
HUD on any of those, Map HUD wins.
Plan and Route Planning are correct **in principle** — fullscreen map, floating pill
summaries, pin-drop interaction, no header — but their specific styling (icon fill
states, exact nav bar treatment, some color choices) drifted from Map HUD across
prompts and should be restyled to match it, not copied as-is. UI-06 says this again at
the point where it matters.
## What the mockups got generated inconsistently — don't copy as intentional
- Bottom nav shows 4 icons on Map HUD and Rides History, only 3 (no Settings) on the Plan
screen. Build one nav bar with 4 destinations, always.
- Rides History's summary row is 4 copies of the same "Total Dist" card. Real, distinct
summary stats are UI-06's job to define.
- Route Planning's bottom nav highlights "Rides" as active while the screen shown is
Plan — a generation slip, not a deliberate cross-tab indicator.

View File

@@ -0,0 +1,100 @@
# UI-01 — Persistent tab shell with an always-visible background map
**Depends on** nothing · **Size** L · **Status** Not started
## Goal
Replace the current push/pop stack rooted at Record with a persistent 4-tab shell (Map,
Rides, Plan, Settings), and make the map visible — at reduced opacity where it isn't the
primary content — behind every one of those tabs, not just Map.
## Context
Today's navigation (`router.dart`) is a hub-and-spoke stack: Record is `/`, and
Trips/Settings/Routes are pushed children you back out of with a `TextButton`/`AppBar`
back arrow. The Stitch redesign is a different model entirely: four co-equal,
always-reachable destinations behind a persistent bottom nav bar, each with its own
independent navigation stack (Trip Detail pushes *within* the Rides tab; the route
planner pushes *within* the Plan tab).
Layered on top of the mockups, by explicit instruction: **the map is active on every
tab**, including Rides History, dimmed/blurred behind the tab's actual content so it
doesn't pull focus. None of the four Stitch exports show this for Rides History (its HTML
uses a static blurred background image) — this is a deliberate product decision, not
something to infer from the mockups alone.
**Also by explicit instruction: no header, on any tab, ever.** The stated reason is
screen real estate — the whole point of this redesign is showing as much map as
possible, and a header is exactly the chrome a persistent bottom nav is meant to let you
remove. Every Stitch export already has an empty `<!-- TopAppBar -->` comment confirming
this; treat it as intentional, not an oversight.
## Design
**Navigation:** `go_router`'s `StatefulShellRoute.indexedStack` (or `.builder`) with four
branches — Map (`/`), Rides (`/rides`), Plan (`/plan`), Settings (`/settings`) — each
branch keeps its own navigator so pushing Trip Detail from Rides, or the route planner
from Plan, doesn't disturb the other tabs' state or the active tab index.
**Background map:** one persistent, live `RideMap`/`FlutterMap` instance sitting behind
the `IndexedStack`'s current branch, not four separate map instances. Rebuilding a real
`FlutterMap` on every tab switch would refetch tiles and lose camera position; a single
shared instance, with only its *content* (opacity, overlay, current position) varying by
tab, is both cheaper and matches "the map is always there, tabs are what floats on top of
it."
- **Map tab:** full opacity, the actual recording/HUD content.
- **Rides / Plan / Settings tabs:** dimmed (roughly 30-40% overlay per the Stitch
export's `opacity-30`/`opacity-40` treatment) and non-interactive — taps pass through
to the tab's real content, the map is decoration, not a control surface, on these tabs.
**No header:** each tab's `Scaffold` has no `appBar`. Screen identity comes from content
(a title inside the tab's own top content, if needed) or from the active nav item, never
from a persistent bar.
## Implementation
1. `AppShell` widget: `StatefulShellRoute` with 4 branches, wrapping a persistent
`BottomNavBar` (see UI-03 for the styled version; a plain one is fine to start) and the
shared background map behind an `IndexedStack`.
2. Extract the background-map hosting into its own widget (`PersistentMapBackground` or
similar) that every tab's `Scaffold` composes against, rather than four independent
`RideMap` constructions.
3. Move `RecordScreen`'s existing map-in-a-card logic to *become* the Map tab's full-
opacity state of this shared background, rather than a separate widget tree — UI-04
does the visual rework, this ticket only has to make the plumbing possible.
4. Remove `AppBar`s from every top-level tab screen. Settings' existing back button goes
away too — Settings becomes a tab, not a pushed screen, so there's nothing to back out
of.
5. Update `router.dart`'s `Routes` constants and every `context.push`/`context.pop` call
site that assumed the old flat stack.
## Acceptance criteria
- [ ] Four tabs are reachable from a persistent bottom nav bar on every screen
- [ ] Switching tabs does not rebuild/refetch the map (camera position survives a tab
switch)
- [ ] The map is visible, dimmed, behind Rides, Plan, and Settings — not just Map
- [ ] No `AppBar`/header exists on any of the four top-level tab screens
- [ ] Trip Detail still pushes within the Rides tab (back returns to the Rides list, not
to the Map tab)
- [ ] The route planner still pushes within the Plan tab
- [ ] Existing widget tests are updated to pump the shell rather than a bare screen, and
still pass
## Tests
- Widget: all four tabs are reachable and their content renders
- Widget: pushing Trip Detail from Rides and popping returns to Rides, not to Map
- Widget: the background map widget instance is not recreated on a tab switch (e.g.
assert identity/key stability, or that camera state is preserved)
- Widget: no `AppBar` is found on any of the four tab roots
## Risks
- **This is the highest-blast-radius ticket in the set** — it touches every screen's
entry point. Land it alone, get `flutter test` fully green, before starting any visual
ticket on top of it.
- A shared background map instance behind an `IndexedStack` is easy to get wrong in a way
that either rebuilds the map on every tab switch (defeating the point) or keeps it
alive so aggressively that it never tears down while the app is backgrounded — revisit
V3-04's lifecycle-aware `TileLayer` teardown to make sure it still applies correctly
once the map is shared across tabs rather than owned by one screen.
## Out of scope
The skeleton/offline map state (UI-02). Any visual redesign of what's inside a tab
(UI-04/05/06) — this ticket only has to make the shell and shared background map exist,
correctly, with today's screen content still working inside it.

View File

@@ -0,0 +1,82 @@
# UI-02 — Offline / no-connection skeleton map
**Depends on** UI-01 · **Size** S · **Status** Not started
## Goal
When the map has no tiles to show — no cached tiles for the current view and no network
to fetch fresh ones — show an animated skeleton in place of a broken or blank map, on
every tab, since UI-01 makes the map a permanent background presence rather than
something a rider opts into per-screen.
## Context
By explicit instruction: *"If we do not have connection for the map, we should just show
an animated skeleton map so it doesn't look weird."* Once UI-01 ships, the map is always
on screen somewhere — a blank grey rectangle or a grid of broken-image icons behind every
tab, all the time, on a phone with no signal, is a much worse look than it was when the
map only appeared on the one screen a rider explicitly opened.
This composes with existing infrastructure rather than duplicating it:
`CachedTileProvider` (V3-11) already tries the offline tile cache before the network, so
"no connection" in practice means *both* the cache and the network failed for the tiles
currently in view.
## Design
Detect at the `CachedTileProvider`/`TileLayer` level, not by asking the OS for
connectivity state directly — a connectivity API can report "online" while the actual
tile fetch still times out (captive portal, degraded connection), and the tile fetch's
own success/failure is the only thing that actually matters here.
- Track a rolling failure count for in-flight tile fetches (cache miss **and** network
fetch failed) over a short window. Crossing a small threshold (e.g. 3 consecutive
failures) flips the map into skeleton mode; a single successful fetch flips it back.
- Skeleton mode replaces the `TileLayer` with a static, non-fetching placeholder — never
a widget that itself keeps trying and failing in a loop.
- Placeholder look: a shimmer/gradient-sweep animation over a neutral grid pattern
(reuse the "technical grid overlay" treatment already present in the Stitch exports'
`map-grid-overlay` CSS — 40px faint grid lines — as the static base under the shimmer),
not a spinner. A map-shaped thing that is clearly still loading, not an error state.
- Recheck periodically (not on every frame) so the map recovers automatically the moment
connectivity returns, without the rider having to do anything.
## Implementation
1. Wrap `CachedTileProvider` (or add a sibling) that reports fetch outcomes to a small
`MapConnectivityState` — rolling window, threshold, debounced flip.
2. `SkeletonMapLayer` widget: the grid + shimmer, sized to fill the same space a
`TileLayer` would.
3. `RideMap`/`PersistentMapBackground` (from UI-01) watches `MapConnectivityState` and
swaps `TileLayer` for `SkeletonMapLayer` when in skeleton mode — markers/polylines
(the rider's own path, planned route) keep rendering on top either way, since those
come from local data, not tiles.
4. A cap on retry frequency while in skeleton mode — this must not turn into a tile-
fetch retry loop that itself violates OSM's usage policy the way V3-11's design
explicitly guards against.
## Acceptance criteria
- [ ] With no cached tiles and no network, the map area shows an animated skeleton, not
blank space or broken-image icons
- [ ] Recovery is automatic: once tiles become fetchable again, the skeleton is replaced
without user action
- [ ] The rider's live position/path and any planned route still render on top of the
skeleton (only the base tiles are missing)
- [ ] Skeleton mode does not itself hammer the tile server with retries
- [ ] Behaves correctly across all four tabs (UI-01's shared background map), not just
the Map tab
## Tests
- Unit: the failure-count/threshold/debounce logic that decides skeleton vs. live,
exercised with fakes (no real network) — mirrors V3-11's `tile_downloader_test.dart`
pattern of injecting fake fetch outcomes
- Widget: `SkeletonMapLayer` renders when connectivity state says offline; `TileLayer`
renders when it says online
- Widget: markers/polylines still render in skeleton mode
## Risks
- Flapping between skeleton and live on a marginal connection would be worse than
either state alone — the debounce window is what prevents this; tune it based on real
behavior, not a guess (echoes V3-13's "measure, don't tune blind" discipline).
- Must not let skeleton-mode retries become the kind of bulk/rapid tile request V3-11's
design explicitly exists to avoid.
## Out of scope
General offline-mode UX beyond the map itself (e.g. graying out map-dependent buttons).
Manual retry controls — automatic recovery is the whole point.

View File

@@ -0,0 +1,72 @@
# UI-03 — Shared floating-glass component kit
**Depends on** UI-08 (theme tokens) · **Size** M · **Status** Not started
## Goal
Build once, use everywhere: the small set of visual primitives every Stitch screen
reuses, so UI-04/05/06/07 compose them instead of each reinventing blur-panel styling.
## Context
All four Stitch exports lean on the same handful of visual patterns repeatedly:
glassmorphic panels (`bg-surface/80 backdrop-blur-xl border border-white/10`), a floating
centered pill for a compact stat summary, and a pulsing/radar-style location marker.
Building these once means every consuming screen ticket is "compose these," not
"reimplement blur and border styling a fourth time."
## Design
Three widgets, all pure presentation (no data-fetching, no business logic):
**`GlassPanel`** — the workhorse. `BackdropFilter` + `ImageFilter.blur` inside a
`ClipRRect`, a translucent surface-color fill, a 1px low-opacity border. Takes a `child`
and behaves like a styled `Container`. Every floating card, tooltip, and control in the
redesign is one of these underneath.
**`FloatingPill`** — a `GlassPanel` shaped as a horizontally-centered, rounded-full
capsule holding a row of labeled stat columns (see Route Planning's Distance/Est. Time/
Pins header). Takes a list of `(label, value)` pairs and lays them out with vertical
dividers between them, matching the Stitch export exactly.
**`PulsingLocationMarker`** — the expanding-ring-plus-glow-dot from the Plan/Route
Planning exports. A `CustomPainter` or layered `AnimatedContainer`s driving an
`AnimationController` in a loop (scale 0.8→1.0, opacity 0.8→0, repeating) — respect
`MediaQuery.disableAnimations`/`prefers-reduced-motion` equivalent by falling back to a
static dot when animations are disabled system-wide.
## Implementation
1. `lib/src/ui/components/glass_panel.dart` — `GlassPanel`, with blur radius, fill
opacity, and border opacity as named constants (not magic numbers scattered per call
site), sourced from UI-08's theme tokens.
2. `lib/src/ui/components/floating_pill.dart` — `FloatingPill`, built on `GlassPanel`.
3. `lib/src/ui/components/pulsing_location_marker.dart` — `PulsingLocationMarker`,
wrapping its `AnimationController` lifecycle correctly (dispose on unmount, same
discipline as `RideMap`'s existing lifecycle observer from V3-04).
4. Widget tests render each in isolation against both themes (if UI-08 keeps a light
variant) to catch a black-on-black-style contrast regression early, the same
discipline V3-16 established.
## Acceptance criteria
- [ ] `GlassPanel` renders a blurred, bordered, translucent container matching the
Stitch reference visually
- [ ] `FloatingPill` renders an arbitrary number of stat columns with dividers between
them, not hardcoded to exactly three
- [ ] `PulsingLocationMarker` animates continuously without leaking its
`AnimationController` across widget rebuilds or disposal
- [ ] Reduced-motion setting is respected by `PulsingLocationMarker`
## Tests
- Widget: each component renders with representative content and takes a screenshot-
comparable snapshot of its structure (no golden files per V3-16's precedent — assert
structure/color, not pixels)
- Widget: `PulsingLocationMarker`'s animation controller is disposed when the widget is
removed from the tree (a `flutter_test` pending-timer/ticker check, mirroring the
Drift stream-query keep-alive pattern already documented in `widget_test.dart`)
## Risks
- `BackdropFilter` is one of the more expensive Flutter widgets to composite; stacking
several `GlassPanel`s over a live, animating map (per UI-01/UI-02) could visibly cost
frame time on lower-end devices. Worth a real-device check once UI-05 assembles them
together, not just in isolation.
## Out of scope
The customizable drag/resize telemetry widgets (UI-04) — those consume `GlassPanel` as
their visual shell but the interaction logic is a separate, larger ticket.

View File

@@ -0,0 +1,114 @@
# UI-04 — Customizable HUD telemetry widgets
**Depends on** UI-03 · **Size** L · **Status** Not started
## Goal
Let the rider drag each floating telemetry widget (Speed, Distance, etc.) to wherever
they want it on screen, resize it, and choose in Settings which metrics appear live at
all — a persisted, personal HUD layout rather than a fixed one.
## Context
The Stitch Map HUD mockup has a fixed horizontal row of four telemetry cards with one
interaction: tap a card to temporarily expand it, tap again (or tap another) to collapse
it back. That's a reasonable default, but by explicit instruction the actual goal is
further than that: **the rider owns the layout.** Hold-and-drag to reposition, resize by
dragging a handle, and a Settings screen listing every available metric with a toggle for
whether it's shown at all. This supersedes the mockup's tap-to-expand interaction rather
than coexisting with it — see Design.
## Design
**Data model.** A `HudWidgetLayout` per metric:
```
{ metricId, x, y, width, height, visible }
```
`x`/`y`/`width`/`height` stored as fractions of the available HUD area (0.0-1.0), not
absolute pixels, so a layout saved on one device/orientation still makes sense on
another. `metricId` is a stable enum (`speed`, `avgSpeed`, `distance`, `elapsedTime`,
`maxSpeed`, `elevationGain`, ...) — the same set `RecordUiState`/`Trip` already expose,
not a new data source.
**Available metrics vs. visible metrics.** Every metric the app already tracks is a
candidate; `visible` (toggled in Settings, see below) controls whether it currently has a
HUD widget at all. A metric toggled off entirely has no position/size to speak of until
turned back on, at which point it gets a sane default slot (not wherever it happened to
be last, unless a position was already saved).
**Interaction:**
- **Move:** long-press-and-drag anywhere on a widget's `GlassPanel` body. A brief haptic
and a subtle scale-up on press-start signal "this is now grabbable," matching the
weight of a real physical action rather than an accidental tap.
- **Resize:** a small drag handle in one corner, visible only while a widget is in "edit
mode" (see below), not in every-day use — a resize handle sitting on screen permanently
during a live ride is visual noise the rider doesn't need mid-ride.
- **Edit mode:** entered explicitly (e.g. a long-press anywhere on the HUD area that
isn't a specific widget, or a dedicated "Edit HUD" toggle from Settings/a long-press on
empty HUD space) rather than every telemetry widget always being draggable — an
always-draggable widget risks an accidental drag mid-ride when the rider meant to tap
it for something else. Exiting edit mode (tap "Done", or tap empty space) persists the
layout.
- **Constraints:** every widget clamps to stay fully within the safe HUD area on drag/
resize end — never under the bottom nav bar, never off-screen, never smaller than a
legibility floor (the number must stay readable) or larger than some sane ceiling.
**Settings integration.** A new "Live HUD stats" section: a checkbox/switch per available
metric, controlling `visible`. Order in the list is stable (doesn't reflect current HUD
position) so it's easy to scan.
**Persistence.** One `Config` field (JSON-encoded map of `metricId` → `HudWidgetLayout`),
same pattern as every other `Config` preference in this app — see `mountedMode`,
`crashReportingEnabled` for the shape to follow.
## Implementation
1. `HudWidgetLayout` model + JSON encode/decode, with defaults for every known metric
(a sensible starting grid, e.g. the Stitch mockup's horizontal row) so a fresh install
has a working, if plain, HUD before the rider customizes anything.
2. `Config.hudLayout` getter/setter, following the established `Config` pattern.
3. `HudLayoutController` (or a `Riverpod` `StateNotifier`) holding the current layout in
memory, seeded from `Config`, written back to `Config` on every edit-mode exit — not
on every drag frame, to avoid hammering `SharedPreferences` mid-drag.
4. `DraggableResizableHudWidget`: wraps a telemetry `GlassPanel`, handles the long-press-
to-grab gesture, the resize handle, and the clamp-on-release logic.
5. `HudEditOverlay`: the edit-mode chrome (resize handles, a "Done" affordance, maybe a
subtle grid/snap guide) shown only while editing.
6. Settings section: "Live HUD stats," one row per metric with a switch bound to
`visible`.
## Acceptance criteria
- [ ] A telemetry widget can be dragged to a new position and the new position survives
an app restart
- [ ] A telemetry widget can be resized within sane min/max bounds
- [ ] Dragging or resizing never leaves a widget partially off-screen or behind the nav
bar
- [ ] Toggling a metric off in Settings removes its HUD widget immediately; toggling it
back on restores it (at its last saved position if one exists, otherwise a default)
- [ ] Outside of edit mode, a normal tap on a telemetry widget does not move it
(no accidental drags during a ride)
- [ ] Layout is per-install (`Config`), not per-ride
## Tests
- Unit: `HudWidgetLayout` JSON round-trips exactly; unknown/missing metric ids on load
fall back to defaults rather than crashing
- Unit: clamp logic — a drag/resize ending outside allowed bounds is corrected to the
nearest valid position/size, exercised with fixed geometry inputs (no real gestures
needed to test the math)
- Widget: drag gesture moves a widget and the moved position is what gets persisted
(simulate via `TestGesture`, matching how other drag interactions in this codebase are
tested)
- Widget: toggling a metric's Settings switch adds/removes its HUD widget
- Widget: a tap outside edit mode does not trigger a move
## Risks
- **Scope creep toward a general-purpose layout editor.** Keep the interaction minimal:
drag, resize, toggle visibility. No z-ordering, no custom widget shapes, no per-metric
color customization — those are separate future tickets if wanted, not this one.
- Persisting on every drag frame would thrash `SharedPreferences`; persist only on
edit-mode exit, keep in-memory state authoritative during an active drag.
- This ticket removes the Stitch mockup's tap-to-expand interaction rather than layering
on top of it — a widget the rider has manually resized should not also silently resize
itself on tap, which would fight the rider's own choice.
## Out of scope
Z-ordering/overlap resolution between widgets (widgets simply clamp to stay on-screen;
overlapping each other is the rider's own choice to avoid). Per-metric colour
customization. Sharing/exporting a HUD layout between devices.

View File

@@ -0,0 +1,82 @@
# UI-05 — Map HUD: Record screen redesign
**Depends on** UI-01, UI-03, UI-04 · **Size** M · **Status** Not started
## 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.

View File

@@ -0,0 +1,91 @@
# UI-06 — Plan & Route Planning redesign
**Depends on** UI-01, UI-03 · **Size** M · **Status** Not started
## Goal
Rebuild the Plan tab (empty state) and the route planner (pins dropped) as fullscreen
map canvases with floating controls — correct in principle per the Stitch mockups, but
**restyled to match Map HUD (UI-05), not copied as-is** — see `README.md`'s note on why
these two mockups drifted from the design north star.
## Context
Today's `RoutePlannerScreen` is a `Scaffold` with an `AppBar` (title, rename/delete/
download actions) and the map beneath it. The mockups drop the header entirely (per
UI-01) and float everything — a pin-drop tooltip, a stat summary pill, a Start Route
button — over a fullscreen map, with a bottom nav bar instead of an app bar's back
button (the planner becomes a screen reached *within* the Plan tab, not a separately
titled pushed screen).
**What to take from the mockups as-is:** the fullscreen-map-plus-floating-controls
pattern, the pin-drop ripple micro-interaction, `PulsingLocationMarker` for the rider's
own position, the marching-ants animated route line, the floating stat pill
(Distance/Est. Time/Pins) replacing today's pre-download confirmation dialog as a
persistent readout instead of a one-time modal.
**What to restyle rather than copy:** icon fill-states, the nav bar's exact background/
blur treatment, and any color choice that doesn't match UI-08's actual palette as
established on the Map HUD screen. The Route Planning mockup specifically shows the
"Rides" nav icon active while viewing Plan — a generation error, not a real cross-tab
indicator; the shipped nav bar must correctly highlight whichever tab is actually active,
matching Map HUD's nav bar exactly (same widget, in fact — see UI-01).
## Design
- **Plan tab (no pins yet):** fullscreen map, technical grid overlay, floating "Tap to
drop a pin" tooltip (gentle float animation), `PulsingLocationMarker`.
- **Route Planning (pins dropped):** same canvas, plus the floating stat pill (`FloatingPill`
from UI-03) showing Distance / Est. Time / Pin count, updating live as pins are added/
moved/removed (already true of the underlying `RoutePlanRepository` data — this ticket
is presentation only). "Start Route" as a floating pill-shaped button above the nav
bar, replacing today's app-bar-driven actions.
- **Existing actions (rename, delete, download offline tiles)** need a new home now that
there's no app bar to hang icon buttons off of — a small floating overflow/glass menu
button is the natural fit, styled per Map HUD's `GlassPanel` language, not invented
fresh.
- **Drag-to-move / tap-to-delete pins:** unchanged interaction from today's
`RoutePlannerScreen`; only the chrome around it changes.
## Implementation
1. Rebuild `RoutePlannerScreen`'s `Scaffold`/`AppBar` as a headerless canvas within the
Plan tab's branch navigator (per UI-01), map at full opacity/interactivity here
(unlike the dimmed treatment other tabs get for the *background* map — this screen's
map *is* the primary content, same as Map HUD's).
2. Replace the pre-download confirmation dialog (V3-11) with the persistent `FloatingPill`
stat summary; keep the actual download flow (count/size check, cancellable progress)
as-is underneath — presentation change only.
3. Add the pin-drop ripple micro-interaction and marching-ants route line as new,
currently-nonexistent polish.
4. Rebuild the Plan tab's empty state (no `RoutePlan` selected/created yet) as the
fullscreen tooltip-and-map view, wired to whatever "create a new route" entry point
replaces today's `RoutesListScreen` FAB (see UI-07 — Rides History's tab likely
absorbs or sits alongside a route list; confirm final IA when that ticket lands, since
"Rides" and "Plan" may end up sharing more list/history UI than the current
`RoutesListScreen`/`TripsScreen` split assumes).
5. Move rename/delete/download actions to a floating glass menu, styled per Map HUD.
## Acceptance criteria
- [ ] Both Plan states (empty, pins dropped) render as fullscreen map canvases with no
header
- [ ] The nav bar on this screen is the exact same widget/styling as Map HUD's, with the
Plan tab correctly shown active (not the generation-error "Rides active" state
from the mockup)
- [ ] Existing pin add/move/delete, rename, delete-route, and offline-download behavior
all still work, restyled only
- [ ] Icon fill-states and colors match Map HUD, not the Stitch export's drifted values
## Tests
- All existing `route_planner_screen_test.dart` / `route_plan_repository_test.dart`
behavior tests still pass against the new presentation layer
- Widget: nav bar active-tab indicator is correct on this screen
- Widget: the floating stat pill updates live as pins are added/removed (already covered
conceptually by existing repository tests; add a presentation-level assertion that the
pill reflects it)
## Risks
- The IA question flagged in Implementation step 4 (how "create a new route" is reached
without a `RoutesListScreen`-style list-with-FAB) may turn out to need its own small
design decision once UI-07 is underway — don't block this ticket on it if the existing
entry point can be preserved with new styling in the meantime.
## Out of scope
Road-snapped routing (V3-08/V3-09, deferred). Any change to the underlying route-planning
data model or repository — this is presentation only.

View File

@@ -0,0 +1,84 @@
# UI-07 — Rides History redesign
**Depends on** UI-01, UI-03 · **Size** M · **Status** Not started
## Goal
Rebuild the Trips list as "Rides History": a dimmed live map background (per UI-01, not
a static image), a search bar, and richer ride cards with a real route-thumbnail map —
restyled to Map HUD's (UI-05) established colors/icons, not the Stitch export's mockup
styling.
## Context
Today's `TripsScreen` is a plain `ListView` of `_TripTile`s (icon, name, one-line
subtitle) over a solid background, reached by pushing off Record. The mockup adds a
search bar, a filter button, a summary-stats row, and cards with an actual map-thumbnail
image per ride. By explicit instruction, the background here is the same live map every
other tab has (dimmed), not the mockup's static blurred screenshot — see UI-01.
**Generation artifact to fix, not copy:** the mockup's summary row is four identical
"Total Dist: 482 KM" cards. This ticket defines real, distinct summary stats.
## Design
- **Background:** UI-01's shared dimmed map, same as Settings and Plan get when not the
active-content tab.
- **Search + filter:** a text field over ride name/date, plus a filter affordance (by
activity type — V3-01 already has this data — and/or date range). Filtering logic is
new; nothing today searches or filters the trips list.
- **Summary row:** real distinct stats — total distance, total moving time, ride count,
and one more genuinely useful figure (e.g. average speed across all rides, or longest
ride) rather than repeating one number four times. Computed from existing `Trip`
aggregate columns already stored on every completed ride — no new data needed, only a
query across all of them.
- **Ride cards:** title, date, and a distance/time/avg-speed stat row (matching the
mockup), plus a **real map thumbnail** — a small, non-interactive `RideMap` (or a
lightweight static rendering of the stored path) rather than a stock image, since we
actually have the ride's own path data, unlike the mockup's placeholder photos.
- **Colors/icons:** Map HUD's palette and icon fill-states, per the design north star —
not the mockup's card-specific styling choices (e.g. the third card's grayscale/
reduced-opacity treatment, which reads as an arbitrary "older ride" decoration with no
defined rule — drop it unless a real rule for it is specified).
## Implementation
1. Rebuild `TripsScreen`'s background to sit over UI-01's dimmed shared map instead of a
plain scaffold background.
2. Add search (text query against `Trip.name`/date) and filter (activity type at
minimum, reusing V3-01's `Activity` enum and `activityIcon`/`activityLabel` helpers).
3. Add a summary-stats query/provider computing totals across `watchCompletedTrips()`
(or a dedicated aggregate query if computing it client-side over a large ride history
becomes a real cost — measure before optimizing).
4. Rebuild `_TripTile` as a card with a small live/static `RideMap` thumbnail (reusing
the existing `RideMap` widget at a small size and non-interactive, rather than
building a second map-rendering path) plus the distance/time/avg-speed row.
5. Confirm this screen's tab identity/name in the nav bar — "Rides" (mockup's label) vs.
"History" vs. keeping "Rides" as used elsewhere in the codebase already (V3-07's
Routes screen already uses "Rides" as the trips-tab label on the record screen) —
match whatever UI-01's nav bar ships with.
## Acceptance criteria
- [ ] Background is the shared live map (dimmed), not a static image
- [ ] Search narrows the list by name/date
- [ ] Filter narrows the list by activity type
- [ ] Summary row shows four distinct, real statistics, not one number repeated
- [ ] Each ride card shows an actual thumbnail of that ride's own path, not a stock photo
- [ ] Merge/delete/rename actions from today's `TripsScreen` still work
- [ ] Colors and icon fill-states match Map HUD, not the Stitch export's card styling
## Tests
- Widget: search filters the visible list correctly
- Widget: filter-by-activity narrows correctly
- Unit: summary-stats aggregation matches a hand-computed total over a fixed set of
seeded trips
- All existing `trips list` widget tests (empty state, newest-first ordering, active-ride
exclusion, merge-at-exactly-two-selections, delete) updated to the new layout and still
passing
## Risks
- Rendering a small `RideMap` thumbnail per card in a long list could be a real
performance cost (many simultaneous `FlutterMap` instances) — consider a lightweight
static polyline-on-canvas rendering instead of a full interactive map per thumbnail if
this proves too expensive; measure with a real ride history of realistic length before
deciding.
## Out of scope
Any change to trip data, merge, or split logic (V3-10) — presentation and search/filter
only.

View File

@@ -0,0 +1,96 @@
# UI-08 — Theme migration to Modern Professional Dark
**Depends on** nothing · **Size** S · **Status** Not started
## Goal
Replace `theme.dart`'s current safety-orange palette with "Modern Professional Dark" —
the design system actually backing every fetched Stitch screen — so every screen ticket
in this set has one settled palette to build against instead of guessing or patching
colors per screen.
## Context
V3-16 (last shipped) deliberately kept and refined the original safety-orange-on-warm-
charcoal identity, with an instrument-blue tertiary for reference readings. That work is
good and tested, but it is **not** the palette the Stitch mockups use — confirmed by
hex-matching every color in the four fetched screens' HTML against all three design
systems in the Stitch project (`docs/design/stitch-export/README.md`). If this redesign
ships, V3-16's direction is superseded, not extended.
Full spec: `docs/design/stitch-export/design-system-modern-professional-dark.md`.
## Design
- **Ground:** deep dark gray `#0a0a0c` (not pure black — "a true premium feel without
the harshness of pure black," per the design system's own doc), stepped up through
`#131313` (surface) and `#16161a` (cards) for elevation.
- **Primary:** Professional Blue — rendered as `#4090fe` (container) / `#aac7ff`
(on-dark-surface primary) / `#002f64` (on-primary text). This replaces safety-orange as
*the* accent everywhere: live telemetry, active nav indicator, primary buttons,
the user's own location dot.
- **Secondary/tertiary:** muted slate-blue (`#aec7f6`/`#2e476f`) and a warm orange
(`#ffb68c`/`#e3711f`) reserved for tertiary accents (the design system's own spec
doesn't define a strict "reference vs. live" split the way V3-16's tertiary did —
decide during implementation whether to keep that instinct using this palette's
secondary color, or drop it; either is defensible, but pick one and apply it
consistently rather than leaving it ambiguous per screen).
- **Typography:** Inter, exclusively, for every role (headline/body/label) — this drops
the multi-font split V3-16 and the original theme used (monospace for numbers via
`monoDigits` is a separate, orthogonal decision from V3-16 worth keeping regardless of
which color palette wins, since "digits must not jitter" is a real constraint, not a
branding choice — retain `monoDigits` layered under Inter-family theming).
- **Shape:** 8px rounding (`ROUND_EIGHT`), up from the current 4px.
- **Depth:** tonal layering + a soft blue glow on elevated/active elements, no shadows —
matches V3-16's existing "no shadows, translucency for depth" instinct, just with a
different accent color for the glow.
- **Contrast:** the design system's own doc claims AAA-level contrast for functional
text against the dark backdrop — verify this the same way V3-16 did (a
`contrastRatio()` helper and real assertions), don't take the claim on faith.
## Implementation
1. Update `theme.dart`'s `ripprColors`/`ColorScheme` to the Modern Professional Dark
values (ground, surface, cards, primary/secondary/tertiary, outline).
2. Update `bodyFont`/`headlineFont`/`labelFont` to Inter throughout; keep `monoDigits` as
a distinct style applied specifically to numeric displays, unchanged in spirit from
today.
3. Update `roundness` usage (button/card corner radii) from 4px to 8px.
4. Re-run V3-16's `contrastRatio()` assertions against the new palette; adjust any color
that fails AA before shipping, exactly as V3-16 did for the palette it replaces.
5. Decide and document the reference-vs-live color convention (see Design) rather than
leaving V3-16's tertiary-for-reference-readings instinct undecided under the new
palette.
6. **Mounted mode (V3-05) needs its own pass**, not an automatic inheritance — it's a
separate high-contrast daylight theme with its own palette, tuned against a real
sunlight/visor constraint. Confirm it still holds up in spirit (still legible, still
AA-compliant) under the new brand direction; V3-13's real-ride verification is still
the only way to confirm the *actual* sunlight legibility claim, same caveat V3-16
already recorded.
## Acceptance criteria
- [ ] `ripprColors` matches Modern Professional Dark's token values
- [ ] Every screen using `Theme.of(context).colorScheme` picks up the new palette with
no per-screen hardcoded color left over from the old theme
- [ ] `contrastRatio()` assertions pass against the new palette (AA normal/large, same
thresholds V3-16 established)
- [ ] `monoDigits` still applies to every numeric display
- [ ] Mounted mode re-verified against the new brand direction
## Tests
- All of V3-16's `theme_test.dart` contrast assertions, re-pointed at the new color
values, still pass
- The existing black-on-black regression guard (`text is legible against the dark
ground`) still passes
- Widget: a representative sample of screens render with the new palette (no leftover
hardcoded orange/old-tertiary-blue literals)
## Risks
- **Regressing the black-on-black guard is the named risk V3-16 itself called out** —
this ticket touches the same `textTheme`/`bodyColor`/`displayColor` wiring that bug
came from originally. Do not remove the explicit color-naming discipline while
restyling.
- Grep for hardcoded hex literals from the old palette (`0xFFFF5722` and friends) across
the UI layer before considering this done — a few call sites (e.g. `RideMap`'s speed
gradient) reference specific hex values directly rather than through the theme, and
those need a deliberate decision, not an accidental miss.
## Out of scope
Any screen-specific redesign (UI-05/06/07) — this ticket only changes the token layer
those screens then build against.