import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../tokens.dart'; /// The selectable row used for serviceable states, districts and pickup slots. /// /// Selection **fills**: the chosen slot becomes a crimson card with a glow /// under it, and every other option stays a white card with a hairline. The /// earlier design tinted the selected tile instead, which on a list of five /// slots left the customer hunting for which one they had picked. A filled row /// is unmistakable from across the room, and the radio on the right keeps the /// control legible for anyone who does not read the colour. /// /// Unavailable options stay visible but recede, with the backend's reason /// attached — coverage should be legible, not hidden. class DmOptionTile extends StatefulWidget { const DmOptionTile({ super.key, required this.title, this.subtitle, this.selected = false, this.enabled = true, this.onTap, this.leading, this.tags, this.trailing, this.titleStyle, }); final String title; final String? subtitle; final bool selected; final bool enabled; final VoidCallback? onTap; /// Optional leading glyph. final IconData? leading; /// Chips above the title — "Fastest", "4 Milers nearby". final List? tags; /// Replaces the radio — "Notify me" on a full slot. final Widget? trailing; /// Slots set their time in the display face; states and districts do not. final TextStyle? titleStyle; @override State createState() => _DmOptionTileState(); } class _DmOptionTileState extends State { bool _down = false; @override Widget build(BuildContext context) { final selected = widget.selected; final enabled = widget.enabled; // ── Selection is a tint and an edge, not a crimson slab ── // // A chosen slot used to fill solid brand and carry [DmShadow.lift] — the // 80%-crimson glow written for the one committing button on a screen. In // a list of six windows that made the choice the loudest object in the // sheet, louder than the button that confirms it, and the white text // inside it broke the type colour every other row was set in. // // A wash with a crimson edge says *chosen* just as unambiguously, keeps // the row's type where it was, and leaves the crimson fill to the footer // button — which is the thing the customer presses next. final Color background = selected ? DmColors.brandSoft : (enabled ? DmColors.surface : DmColors.surfaceAlt); final titleColor = selected ? DmColors.brandPress : (enabled ? DmColors.ink : DmColors.ink3); final subColor = selected ? DmColors.brandPress : (enabled ? DmColors.ink2 : DmColors.ink3); return Semantics( button: true, selected: selected, enabled: enabled, child: GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: enabled ? (_) => setState(() => _down = true) : null, onTapUp: enabled ? (_) => setState(() => _down = false) : null, onTapCancel: enabled ? () => setState(() => _down = false) : null, onTap: enabled ? () { HapticFeedback.selectionClick(); widget.onTap?.call(); } : null, child: AnimatedSlide( offset: _down ? const Offset(0, 0.012) : Offset.zero, duration: DmMotion.fast, curve: DmMotion.ease, child: AnimatedContainer( duration: DmMotion.base, curve: DmMotion.ease, constraints: const BoxConstraints(minHeight: 62), padding: EdgeInsets.fromLTRB( widget.leading == null ? 16 : 14, 14, 16, 14, ), decoration: BoxDecoration( color: background, borderRadius: DmRadius.all(DmRadius.md), border: selected ? Border.all(color: DmColors.brand, width: 1.5) : null, // An unchosen, available option is a card like any other: lifted // off the sheet rather than outlined on it. A disabled one is // flat, because it is not an object you can pick up. boxShadow: enabled && !selected ? DmShadow.card : null, ), child: Row( children: [ if (widget.leading != null) ...[ Icon( widget.leading, size: 20, color: selected ? DmColors.brand : DmColors.ink3, ), const SizedBox(width: 13), ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (widget.tags != null && widget.tags!.isNotEmpty) ...[ Wrap( spacing: 7, runSpacing: 6, children: widget.tags!, ), const SizedBox(height: 9), ], Text( widget.title, style: (widget.titleStyle ?? DmText.cardTitle) .copyWith(color: titleColor), ), if (widget.subtitle != null) ...[ const SizedBox(height: 3), Text( widget.subtitle!, style: DmText.small.copyWith(color: subColor), ), ], ], ), ), const SizedBox(width: 12), widget.trailing ?? _Radio(selected: selected, enabled: enabled), ], ), ), ), ), ); } } /// A radio, not a check: these lists are single-choice, and a tick reads as /// "done" where a radio reads as "this one, of these". class _Radio extends StatelessWidget { const _Radio({required this.selected, required this.enabled}); final bool selected; final bool enabled; @override Widget build(BuildContext context) { return AnimatedContainer( duration: DmMotion.base, curve: DmMotion.ease, width: 22, height: 22, alignment: Alignment.center, decoration: BoxDecoration( shape: BoxShape.circle, // The mark takes the brand now that the tile behind it is a wash // rather than a crimson field — white on `brandSoft` is invisible. border: Border.all( color: selected ? DmColors.brand : (enabled ? DmColors.ink4 : DmColors.border), width: selected ? 2 : 1.5, ), ), child: selected ? Container( width: 10, height: 10, decoration: const BoxDecoration( color: DmColors.brand, shape: BoxShape.circle, ), ) : null, ); } } /// A tappable row with a leading tile — "Drop a map pin", "Saved addresses". class DmActionRow extends StatelessWidget { const DmActionRow({ super.key, required this.icon, required this.title, required this.subtitle, this.onTap, this.subtitleColor, this.active = false, }); final IconData icon; final String title; final String subtitle; final VoidCallback? onTap; final Color? subtitleColor; /// Tints the tile once the action has been completed. final bool active; @override Widget build(BuildContext context) { return Material( color: DmColors.surface, borderRadius: DmRadius.all(DmRadius.md), child: InkWell( onTap: onTap, borderRadius: DmRadius.all(DmRadius.md), child: Ink( decoration: BoxDecoration( color: DmColors.surface, border: Border.all(color: DmColors.border), borderRadius: DmRadius.all(DmRadius.md), ), child: Container( constraints: const BoxConstraints(minHeight: 64), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row( children: [ Container( width: 38, height: 38, alignment: Alignment.center, decoration: BoxDecoration( color: active ? DmColors.okSoft : DmColors.brandSoft, borderRadius: DmRadius.all(DmRadius.xs), ), child: Icon( icon, size: 18, color: active ? DmColors.ok : DmColors.brand, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(title, style: DmText.cardTitle), const SizedBox(height: 2), Text( subtitle, style: DmText.small.copyWith( color: subtitleColor ?? DmColors.ink2, ), ), ], ), ), const Icon( LucideIcons.chevronRight, size: 20, color: DmColors.ink4, ), ], ), ), ), ), ); } }