# UI-03 — Shared floating-glass component kit **Depends on** UI-08 (theme tokens) · **Size** M · **Status** Not started ## 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 `AnimatedContainer`s 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 1. `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. 2. `lib/src/ui/components/floating_pill.dart` — `FloatingPill`, built on `GlassPanel`. 3. `lib/src/ui/components/pulsing_location_marker.dart` — `PulsingLocationMarker`, wrapping its `AnimationController` lifecycle correctly (dispose on unmount, same discipline as `RideMap`'s existing lifecycle observer from V3-04). 4. 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 - [ ] `GlassPanel` renders a blurred, bordered, translucent container matching the Stitch reference visually - [ ] `FloatingPill` renders an arbitrary number of stat columns with dividers between them, not hardcoded to exactly three - [ ] `PulsingLocationMarker` animates continuously without leaking its `AnimationController` across 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 (a `flutter_test` pending-timer/ticker check, mirroring the Drift stream-query keep-alive pattern already documented in `widget_test.dart`) ## Risks - `BackdropFilter` is one of the more expensive Flutter widgets to composite; stacking several `GlassPanel`s 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.