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

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