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.
101 lines
5.7 KiB
Markdown
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.
|