diff --git a/docs/ui-redesign/README.md b/docs/ui-redesign/README.md new file mode 100644 index 0000000..686331c --- /dev/null +++ b/docs/ui-redesign/README.md @@ -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. diff --git a/docs/ui-redesign/UI-01-tab-shell-background-map.md b/docs/ui-redesign/UI-01-tab-shell-background-map.md new file mode 100644 index 0000000..d66577e --- /dev/null +++ b/docs/ui-redesign/UI-01-tab-shell-background-map.md @@ -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 `` 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. diff --git a/docs/ui-redesign/UI-02-offline-skeleton-map.md b/docs/ui-redesign/UI-02-offline-skeleton-map.md new file mode 100644 index 0000000..6af9df0 --- /dev/null +++ b/docs/ui-redesign/UI-02-offline-skeleton-map.md @@ -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. diff --git a/docs/ui-redesign/UI-03-glass-component-kit.md b/docs/ui-redesign/UI-03-glass-component-kit.md new file mode 100644 index 0000000..dbe5369 --- /dev/null +++ b/docs/ui-redesign/UI-03-glass-component-kit.md @@ -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. diff --git a/docs/ui-redesign/UI-04-customizable-hud-widgets.md b/docs/ui-redesign/UI-04-customizable-hud-widgets.md new file mode 100644 index 0000000..182e0b0 --- /dev/null +++ b/docs/ui-redesign/UI-04-customizable-hud-widgets.md @@ -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. diff --git a/docs/ui-redesign/UI-05-map-hud-record-screen.md b/docs/ui-redesign/UI-05-map-hud-record-screen.md new file mode 100644 index 0000000..7c62011 --- /dev/null +++ b/docs/ui-redesign/UI-05-map-hud-record-screen.md @@ -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. diff --git a/docs/ui-redesign/UI-06-plan-and-route-planning.md b/docs/ui-redesign/UI-06-plan-and-route-planning.md new file mode 100644 index 0000000..f5fd7f0 --- /dev/null +++ b/docs/ui-redesign/UI-06-plan-and-route-planning.md @@ -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. diff --git a/docs/ui-redesign/UI-07-rides-history.md b/docs/ui-redesign/UI-07-rides-history.md new file mode 100644 index 0000000..240d116 --- /dev/null +++ b/docs/ui-redesign/UI-07-rides-history.md @@ -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. diff --git a/docs/ui-redesign/UI-08-theme-modern-professional-dark.md b/docs/ui-redesign/UI-08-theme-modern-professional-dark.md new file mode 100644 index 0000000..0d18d49 --- /dev/null +++ b/docs/ui-redesign/UI-08-theme-modern-professional-dark.md @@ -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.