import 'package:flutter/material.dart'; import '../tokens.dart'; /// One accent, three intensities — the design's `.st`. enum DmPillTone { /// Something is happening right now and nobody has to do anything: searching /// for a Miler, out for delivery. Crimson field, white label, solid dot. live, /// Moving, but between beats — assigned, in transit. The crimson is held in /// an outline so a list of six orders is not six red blocks. neutral, /// Finished. Grey, with a filled dot: the journey happened. done, /// Never happened. Grey, hollow dot. off, } /// Status pill used on order rows and at the head of a tracking card. class DmPill extends StatelessWidget { const DmPill(this.label, {super.key, this.tone = DmPillTone.neutral}); final String label; final DmPillTone tone; @override Widget build(BuildContext context) { final (Color bg, Color fg, BoxBorder? edge) = switch (tone) { DmPillTone.live => (DmColors.brand, Colors.white, null), DmPillTone.neutral => ( DmColors.surface, DmColors.brand, Border.all(color: DmColors.brandLine), ), DmPillTone.done => (DmColors.surfaceAlt, DmColors.ink3, null), DmPillTone.off => (DmColors.surfaceAlt, DmColors.ink3, null), }; // The dot is the tone's tell at a glance, before the word is read: filled // for anything that happened or is happening, hollow for what never did. final hollow = tone == DmPillTone.off; return Container( padding: const EdgeInsets.fromLTRB(11, 5, 11, 5), decoration: BoxDecoration( color: bg, border: edge, borderRadius: DmRadius.all(5), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, margin: const EdgeInsets.only(right: 7), decoration: BoxDecoration( color: hollow ? Colors.transparent : fg, shape: BoxShape.circle, border: hollow ? Border.all(color: fg, width: 1.5) : null, ), ), Flexible( child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: DmText.label.copyWith(fontSize: 12.5, color: fg), ), ), ], ), ); } } /// The pulsing dot that marks a live booking. class DmLiveDot extends StatefulWidget { const DmLiveDot({super.key, this.size = 9, this.color = DmColors.brand}); final double size; final Color color; @override State createState() => _DmLiveDotState(); } class _DmLiveDotState extends State with SingleTickerProviderStateMixin { late final AnimationController _c = AnimationController( vsync: this, duration: const Duration(milliseconds: 1900), ); @override void initState() { super.initState(); // A ring that never stops is a battery cost and, for anyone who asked the // system not to animate, a distraction. Honour the setting: the dot still // reads as live because it is a filled brand dot on a quiet row. if (!WidgetsBinding.instance.platformDispatcher.accessibilityFeatures.disableAnimations) { _c.repeat(); } } @override void dispose() { _c.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final span = widget.size + 10; return SizedBox( width: span, height: span, child: AnimatedBuilder( animation: _c, builder: (context, child) { return Stack( alignment: Alignment.center, children: [ if (_c.isAnimating) Transform.scale( scale: 1 + _c.value * 1.6, child: Opacity( opacity: (1 - _c.value) * 0.5, child: Container( width: widget.size, height: widget.size, decoration: BoxDecoration( color: widget.color, shape: BoxShape.circle, ), ), ), ), child!, ], ); }, child: Container( width: widget.size, height: widget.size, decoration: BoxDecoration( color: widget.color, shape: BoxShape.circle, ), ), ), ); } } /// Initials avatar. Crimson-filled on a light surface, lightened white when it /// sits on the brand header. class DmAvatar extends StatelessWidget { const DmAvatar({ super.key, required this.initials, this.size = 42, this.onBrand = false, }); final String initials; final double size; final bool onBrand; @override Widget build(BuildContext context) { return Container( width: size, height: size, alignment: Alignment.center, decoration: BoxDecoration( color: onBrand ? DmColors.onBrandRaise : DmColors.brand, shape: BoxShape.circle, ), child: Text( initials, style: DmText.label.copyWith( fontSize: size < 40 ? 12 : 13, fontWeight: FontWeight.w800, fontVariations: const [FontVariation('wght', 800)], letterSpacing: 0.13, color: Colors.white, ), ), ); } } /// The Doormile logomark: a door inside a house silhouette. class DoormileMark extends StatelessWidget { const DoormileMark({super.key, this.size = 52, this.inverted = false}); final double size; /// White strokes on nothing, for use on the crimson field. final bool inverted; @override Widget build(BuildContext context) { return Container( width: size, height: size, decoration: BoxDecoration( color: inverted ? Colors.transparent : DmColors.brand, borderRadius: DmRadius.all(size * 0.24), ), child: CustomPaint( painter: const _MarkPainter(), size: Size.square(size), child: SizedBox.square(dimension: size), ), ); } } class _MarkPainter extends CustomPainter { const _MarkPainter(); @override void paint(Canvas canvas, Size size) { final s = size.width; final paint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = s * 0.072 ..strokeJoin = StrokeJoin.round ..strokeCap = StrokeCap.round; // Roof + walls, drawn on a 24x24 grid scaled to the mark. double u(double v) => v / 24 * s * 0.62 + s * 0.19; final house = Path() ..moveTo(u(3.6), u(9.2)) ..lineTo(u(12), u(4)) ..lineTo(u(20.4), u(9.2)) ..lineTo(u(20.4), u(20)) ..lineTo(u(3.6), u(20)) ..close(); final door = Path() ..moveTo(u(9.4), u(20)) ..lineTo(u(9.4), u(13)) ..lineTo(u(14.6), u(13)) ..lineTo(u(14.6), u(20)); canvas.drawPath(house, paint); canvas.drawPath(door, paint); } @override bool shouldRepaint(covariant _MarkPainter oldDelegate) => false; }