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.
5.7 KiB
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-40treatment) 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
AppShellwidget:StatefulShellRoutewith 4 branches, wrapping a persistentBottomNavBar(see UI-03 for the styled version; a plain one is fine to start) and the shared background map behind anIndexedStack.- Extract the background-map hosting into its own widget (
PersistentMapBackgroundor similar) that every tab'sScaffoldcomposes against, rather than four independentRideMapconstructions. - 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. - Remove
AppBars 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. - Update
router.dart'sRoutesconstants and everycontext.push/context.popcall 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
AppBaris 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 testfully green, before starting any visual ticket on top of it. - A shared background map instance behind an
IndexedStackis 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-awareTileLayerteardown 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.