import 'package:flutter/material.dart'; import '../tokens.dart'; /// Dark pill toast, matching the prototype's transient confirmations. class DmToast { DmToast._(); static OverlayEntry? _entry; static void show(BuildContext context, String message) { final overlay = Overlay.maybeOf(context); if (overlay == null) return; _entry?.remove(); final entry = OverlayEntry( builder: (context) => _ToastBody(message: message), ); _entry = entry; overlay.insert(entry); Future.delayed(const Duration(milliseconds: 2400), () { if (_entry == entry) { entry.remove(); _entry = null; } }); } /// Called when the app is torn down in tests. static void dismiss() { _entry?.remove(); _entry = null; } } class _ToastBody extends StatefulWidget { const _ToastBody({required this.message}); final String message; @override State<_ToastBody> createState() => _ToastBodyState(); } class _ToastBodyState extends State<_ToastBody> with SingleTickerProviderStateMixin { late final AnimationController _c = AnimationController( vsync: this, duration: const Duration(milliseconds: 240), )..forward(); @override void dispose() { _c.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final bottom = MediaQuery.of(context).viewInsets.bottom; return Positioned( left: 20, right: 20, bottom: 96 + bottom, child: FadeTransition( opacity: _c, child: SlideTransition( position: Tween( begin: const Offset(0, 0.3), end: Offset.zero, ).animate(CurvedAnimation(parent: _c, curve: Curves.easeOutCubic)), child: Center( child: Material( color: Colors.transparent, child: Container( padding: const EdgeInsets.symmetric( horizontal: 18, vertical: 11, ), decoration: BoxDecoration( color: const Color(0xF0191716), borderRadius: DmRadius.all(DmRadius.sm), ), child: Text( widget.message, textAlign: TextAlign.center, style: DmText.label.copyWith( fontSize: 13.5, color: Colors.white, ), ), ), ), ), ), ), ); } } /// Bottom sheet with the prototype's grip, radius and padding. Future showDmSheet({ required BuildContext context, required WidgetBuilder builder, bool isScrollControlled = true, }) { return showModalBottomSheet( context: context, isScrollControlled: isScrollControlled, backgroundColor: DmColors.canvas, barrierColor: DmColors.scrim, elevation: 0, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(DmRadius.xl)), ), clipBehavior: Clip.antiAlias, builder: (context) => Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: SafeArea( top: false, child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.86, ), child: Padding( padding: const EdgeInsets.fromLTRB( DmSpace.pad, 10, DmSpace.pad, 20, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _SheetGrip(), Flexible(child: builder(context)), ], ), ), ), ), ), ); } class _SheetGrip extends StatelessWidget { const _SheetGrip(); @override Widget build(BuildContext context) { return Container( width: 36, height: 4, margin: const EdgeInsets.only(top: 2, bottom: 18), decoration: BoxDecoration( color: DmColors.border, borderRadius: DmRadius.all(2), ), ); } } /// Sheet heading + supporting line. class DmSheetHeader extends StatelessWidget { const DmSheetHeader({super.key, required this.title, this.subtitle}); final String title; final String? subtitle; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: DmText.sheetTitle), if (subtitle != null) ...[ const SizedBox(height: 3), Text(subtitle!, style: DmText.tiny), ], const SizedBox(height: 18), ], ); } }