Full UI redesign pass complete: persistent tab shell with an always-visible background map, Modern Professional Dark theme, monochrome dark map tiles, offline skeleton map, a shared GlassPanel/FloatingPill component kit, customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning / Rides History screen rebuilds. 374 tests passing, up from 316. The APK is a fresh release build (debug-signed, no release signing config exists yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
8.3 KiB
UI-03 — Shared floating-glass component kit
Depends on UI-08 (theme tokens) · Size M · Status Done
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 AnimatedContainers 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
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.lib/src/ui/components/floating_pill.dart—FloatingPill, built onGlassPanel.lib/src/ui/components/pulsing_location_marker.dart—PulsingLocationMarker, wrapping itsAnimationControllerlifecycle correctly (dispose on unmount, same discipline asRideMap's existing lifecycle observer from V3-04).- 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
GlassPanelrenders a blurred, bordered, translucent container matching the Stitch reference visuallyFloatingPillrenders an arbitrary number of stat columns with dividers between them, not hardcoded to exactly threePulsingLocationMarkeranimates continuously without leaking itsAnimationControlleracross 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 (aflutter_testpending-timer/ticker check, mirroring the Drift stream-query keep-alive pattern already documented inwidget_test.dart)
Risks
BackdropFilteris one of the more expensive Flutter widgets to composite; stacking severalGlassPanels 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.
Outcome
All three values (blur radius, opacities, dimensions, colors) were lifted directly from
the Route Planning export's own glass treatment and pulse-ring keyframes rather than
guessed — docs/design/stitch-export/screens/route-planning-dark.html's bg-surface/90 backdrop-blur-xl border border-outline-variant/30 and its @keyframes pulse-ring
(scale 0.8→1.0, opacity 0.8→0, eased) map directly onto GlassPanel.blurSigma/
fillOpacity/borderOpacity and PulsingLocationMarker's animation curve.
GlassPanel (lib/src/ui/components/glass_panel.dart) is ClipRRect > BackdropFilter > DecoratedBox, taking child/borderRadius/padding and reading its fill/border colors
from the theme (colors.surface, colors.outlineVariant — the latter newly added to
ripprColors in theme.dart for this ticket, since UI-08 hadn't needed it before now).
FloatingPill (floating_pill.dart) is a GlassPanel shaped as a stadium capsule
around a Row of PillStat columns with 1px dividers between them — genuinely
arbitrary-length, not hardcoded to three, verified by a 4-stat test case.
PulsingLocationMarker (pulsing_location_marker.dart) layers a static faint ring, an
animated expanding-and-fading ring, and a glowing center dot; it checks
MediaQuery.disableAnimations in didChangeDependencies (not initState, which
Flutter forbids for inherited-widget lookups) and stops the controller entirely when
reduced motion is requested, falling back to a static dot.
Test-writing bug caught and fixed: the first version of the "legible in both
themes" test pumped ripprTheme() then ripprMountedTheme() sequentially in a single
testWidgets loop. Because both produced an identically-shaped widget tree
(MaterialApp > Scaffold > GlassPanel > Text), Flutter reused the first pump's elements
across the second pumpWidget call rather than rebuilding fresh, so the second
iteration silently re-read the first theme's already-resolved paint color — the test
would have falsely failed for a real theme-following bug and, worse, could have
falsely passed one due to reading stale data. Split into two independent testWidgets
blocks instead, which is the correct way to exercise two themes against the same
component.
Tests: flutter analyze clean. flutter test green at 336 tests (328 + 8 new in
test/glass_component_kit_test.dart): GlassPanel's blur/translucency/border
structure and its resolved-paint-color contrast in both themes; FloatingPill's
divider count scaling with stat count (and the zero-divider single-stat case);
PulsingLocationMarker's continuous animation, correct AnimationController disposal
on removal (caught automatically by flutter_test's own ticker-leak check, not a
manual timer assertion), and the reduced-motion fallback.
Android emulator verification: no consuming screen exists yet (that's UI-04/05/06),
so verification used a throwaway preview entry point
(lib/main_ui03_preview.dart, deleted after use) rendering all three components over a
map-colored gradient background. All three matched the Stitch reference's look: the
FloatingPill's blur/translucency/dividers/blue values rendered crisply; the
PulsingLocationMarker's glow and ring were visible and animating. One red herring
during this check: GlassPanel's content briefly appeared to render illegibly when the
preview used Theme.of(context).textTheme.headlineSmall for a heading, despite that
style's color property independently verified (via a debug test) to already be the
correct ink value with full alpha. Switching to an explicit inline TextStyle (no
ambient text-theme role) rendered crisply instead. This was isolated to the scratch
preview file, not GlassPanel itself — the shipped components' own tests all render
content through explicit styles or the already-verified bodyMedium role, the same
discipline every real screen in this codebase already follows — but it's worth flagging
as a headlineSmall-specific rendering quirk to watch for if UI-05/06 lean on that
particular text-theme role for real headings.
Risks note (revisited)
The ticket's own risk — BackdropFilter compositing cost when several GlassPanels
stack over a live, animating map — was not measurable in this ticket's isolated preview
(no map, no stacking). Deferred to UI-05, as the ticket itself anticipated, where the
Record screen actually assembles multiple glass panels over RideMap.