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
110 lines
3.4 KiB
Dart
110 lines
3.4 KiB
Dart
/// UI-03: the expanding-ring-plus-glow-dot user-location marker from the Plan/Route
|
|
/// Planning exports. Values (sizes, colours, the scale/opacity curve) are lifted from
|
|
/// the exports' own `pulse-ring` keyframes, not guessed -- see
|
|
/// `docs/design/stitch-export/screens/route-planning-dark.html`.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
class PulsingLocationMarker extends StatefulWidget {
|
|
const PulsingLocationMarker({super.key});
|
|
|
|
@override
|
|
State<PulsingLocationMarker> createState() => _PulsingLocationMarkerState();
|
|
}
|
|
|
|
class _PulsingLocationMarkerState extends State<PulsingLocationMarker>
|
|
with SingleTickerProviderStateMixin {
|
|
late final _controller = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_syncWithReducedMotion();
|
|
}
|
|
|
|
/// A static dot -- the ring never expands -- when the platform has reduced motion
|
|
/// enabled, the same accessibility setting `prefers-reduced-motion` maps to on the
|
|
/// web this component's design came from.
|
|
void _syncWithReducedMotion() {
|
|
final reduceMotion = MediaQuery.maybeOf(context)?.disableAnimations ?? false;
|
|
if (reduceMotion) {
|
|
_controller.stop();
|
|
} else if (!_controller.isAnimating) {
|
|
_controller.repeat();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = Theme.of(context).colorScheme;
|
|
return SizedBox(
|
|
width: 40,
|
|
height: 40,
|
|
child: AnimatedBuilder(
|
|
animation: _controller,
|
|
builder: (context, child) {
|
|
// The keyframes' own 0%/70%/100% easing: scale 0.8 -> 1.0 and opacity
|
|
// 0.8 -> 0, both eased out rather than linear, so the ring visibly slows as
|
|
// it fades rather than looking like it's cut off mid-motion.
|
|
final t = Curves.easeOut.transform(_controller.value);
|
|
final scale = 0.8 + (0.2 * t);
|
|
final opacity = 0.8 * (1 - t);
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
// The faint, static outer ring -- always present, pulse or no pulse.
|
|
Container(
|
|
width: 16,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: colors.primary.withValues(alpha: 0.3),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
Transform.scale(
|
|
scale: scale,
|
|
child: Opacity(
|
|
opacity: opacity,
|
|
child: Container(
|
|
width: 12,
|
|
height: 12,
|
|
decoration: BoxDecoration(
|
|
color: colors.primary,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
color: colors.primary,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: colors.primary.withValues(alpha: 0.5),
|
|
blurRadius: 10,
|
|
spreadRadius: 1,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|