Files
rippr/docs/ui-redesign/UI-01-tab-shell-background-map.md
uhryniuk 822003ed38 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.
2026-08-23 18:25:35 -05:00

101 lines
5.7 KiB
Markdown

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