UI-03: shared floating-glass component kit

Adds GlassPanel (BackdropFilter + translucent fill + subtle border, values
lifted directly from the Stitch exports' own bg-surface/90 backdrop-blur-xl
border-outline-variant/30 treatment), FloatingPill (a GlassPanel shaped as
a capsule around an arbitrary number of labeled stat columns with
dividers), and PulsingLocationMarker (expanding-ring-plus-glow-dot,
matching the exports' pulse-ring keyframes exactly, with a static-dot
fallback when reduced motion is requested). Pure presentation -- every
UI-04/05/06/07 screen composes these rather than reimplementing blur/
border styling per screen.

Adds outlineVariant to ripprColors, needed for the panel border and pill
dividers and not previously wired up by UI-08.

Verified with a throwaway preview entry point (deleted after use, no
consuming screen exists yet) plus 8 new widget tests covering blur/
translucency/border structure, contrast in both themes, arbitrary stat
counts, and the pulsing marker's animation lifecycle and reduced-motion
fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
2026-08-23 20:22:19 -05:00
parent 10040e6985
commit 24f4e17e81
7 changed files with 494 additions and 2 deletions

View File

@@ -21,7 +21,7 @@ v3 held to.
|---|---|---|---|---|
| [UI-01](UI-01-tab-shell-background-map.md) | Persistent tab shell with an always-visible background map | L | — | Done |
| [UI-02](UI-02-offline-skeleton-map.md) | Offline / no-connection skeleton map | S | UI-01 | Done |
| [UI-03](UI-03-glass-component-kit.md) | Shared floating-glass component kit | M | UI-08 | Not started |
| [UI-03](UI-03-glass-component-kit.md) | Shared floating-glass component kit | M | UI-08 | Done |
| [UI-04](UI-04-customizable-hud-widgets.md) | Customizable HUD telemetry widgets (drag, resize, visibility toggle) | L | UI-03 | Not started |
| [UI-05](UI-05-map-hud-record-screen.md) | Map HUD: Record screen redesign | M | UI-01, UI-03, UI-04 | Not started |
| [UI-06](UI-06-plan-and-route-planning.md) | Plan & Route Planning redesign | M | UI-01, UI-03 | Not started |

View File

@@ -1,6 +1,6 @@
# UI-03 — Shared floating-glass component kit
**Depends on** UI-08 (theme tokens) · **Size** M · **Status** Not started
**Depends on** UI-08 (theme tokens) · **Size** M · **Status** Done
## Goal
Build once, use everywhere: the small set of visual primitives every Stitch screen
@@ -70,3 +70,68 @@ static dot when animations are disabled system-wide.
## 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 `GlassPanel`s
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`.