// FaiStatusDot — small pulsing dot used as a "live" indicator. // Goes still when [pulsing] is false, breathes gently when true. import 'package:flutter/material.dart'; import '../theme/tokens.dart'; class FaiStatusDot extends StatefulWidget { final Color color; /// When true, the dot fades between full and 30% opacity. /// When false, it stays at full opacity (steady-state). final bool pulsing; final double size; const FaiStatusDot({ super.key, required this.color, this.pulsing = false, this.size = 8, }); @override State createState() => _FaiStatusDotState(); } class _FaiStatusDotState extends State with SingleTickerProviderStateMixin { late final AnimationController _ctrl; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 1400), ); if (widget.pulsing) _ctrl.repeat(reverse: true); } @override void didUpdateWidget(covariant FaiStatusDot old) { super.didUpdateWidget(old); if (widget.pulsing && !_ctrl.isAnimating) { _ctrl.repeat(reverse: true); } else if (!widget.pulsing && _ctrl.isAnimating) { _ctrl.stop(); _ctrl.value = 0; } } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _ctrl, builder: (_, _) { final t = widget.pulsing ? _ctrl.value : 0.0; final alpha = 1.0 - (t * 0.6); return Container( width: widget.size, height: widget.size, decoration: BoxDecoration( color: widget.color.withValues(alpha: alpha), shape: BoxShape.circle, boxShadow: widget.pulsing ? [ BoxShadow( color: widget.color.withValues(alpha: 0.4 * alpha), blurRadius: 6 * (1 + t), spreadRadius: 1 * t, ), ] : const [], ), ); }, ); } } /// Convenience presets. class FaiStatusDots { FaiStatusDots._(); static Widget live() => const FaiStatusDot( color: FaiColors.success, pulsing: true, ); static Widget idle() => const FaiStatusDot( color: FaiColors.muted, ); static Widget down() => const FaiStatusDot( color: FaiColors.danger, ); }