import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/constants/design_constants.dart'; import 'package:miler/views/helpers/constants/Font_constant.dart'; import 'package:miler/views/helpers/widgets/app_widgets.dart'; import 'package:miler/widget/Bottom_page.dart'; /// ───────────────────────────────────────────────────────────────────────── /// SELECTION BAR — the contextual action for a multi-stop pick /// /// Appears the moment the rider ticks a stop and leaves when he unticks the /// last one. While it is up it owns the bulk action: the inline "Accept all N /// stops" button in the trip's footer stands down, so there is never a choice /// of two bulk buttons pointing at different sets of stops. /// /// ── Why this floats instead of sitting in the list ── /// /// It used to be an inline button in the footer, below the stop rows. That put /// a *response to a direct action* at the bottom of a scroll: tick a stop near /// the top of a six-stop route and the button confirming what you had just done /// was off-screen. The rider had no feedback that the tick had registered /// beyond the tick itself, and no way to act on it without scrolling past every /// remaining stop. /// /// A selection is transient and it is *modal in spirit* — the screen is in a /// different mode while it is non-empty. Every app that does multi-select the /// way riders already know it, Gmail and Google Photos included, answers that /// with a contextual bar pinned to the edge of the viewport: it appears with /// the selection, it is always reachable, and it leaves without a trace. The /// count lives on it, so the rider never has to go looking for how many he has /// picked. /// /// ── Clearing ── /// /// The ✕ is on the left, where a dismiss belongs and where it cannot be hit by /// a thumb going for Accept. Without it the only way out of a selection was to /// untick each stop by hand, which on a six-stop route is six taps to undo one /// mistake. /// ───────────────────────────────────────────────────────────────────────── class SelectionBar extends StatelessWidget { /// How many stops are ticked. Zero renders nothing. final int count; /// Accepts exactly the ticked stops. final VoidCallback? onAccept; /// Declines exactly the ticked stops, after asking why. Null hides the /// action. final VoidCallback? onReject; /// Drops the whole selection. final VoidCallback onClear; /// True while the accept is in flight. final bool busy; const SelectionBar({ super.key, required this.count, required this.onAccept, required this.onClear, this.onReject, this.busy = false, }); @override Widget build(BuildContext context) { // Rides the floating nav bar: it is `Positioned` outside the Scaffold's // body, so `SafeArea` knows nothing about it and anything pinned to the // bottom lands underneath. See [BottomPage.bottomInset]. // // A fixed inset now. This used to follow a `navVisible` notifier up and // down, because the bar left the screen entirely on scroll. It compacts // instead of leaving, so there is no longer a state in which the room is // free — and chasing the 20pt difference re-laid this out every time the // rider changed scroll direction, for a gap he would never notice. return Padding( padding: EdgeInsets.only(bottom: BottomPage.bottomInset(context) + 10.h), child: Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: Material( color: Colors.white, borderRadius: BorderRadius.circular(DesignConstants.radiusXl), // A real lift, not the page's usual whisper: this is the one surface // on Home that is temporarily above everything else, and it has to // read that way against a list of white cards. elevation: 0, child: Container( padding: EdgeInsets.fromLTRB(6.w, 8.h, 10.w, 8.h), decoration: BoxDecoration( color: ColorConstants.pureSurface, borderRadius: BorderRadius.circular(DesignConstants.radiusXl), border: Border.all(color: ColorConstants.borderSubtle), boxShadow: DesignConstants.shadowLg, ), child: Row( children: [ _ClearButton(onTap: busy ? null : onClear), SizedBox(width: 2.w), // The running count, in words — but only when there is room for // it. With two decisions on the bar there is not: at 320pt the // three fixed controls plus this line overflowed by 94pt, and // "Accept 3" already carries the number. So the sentence is a // luxury for the one-button case and the space goes to the // buttons otherwise. if (onReject == null) Expanded( child: Text( '$count ${count == 1 ? 'stop' : 'stops'} selected', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13.5.sp, fontWeight: FontWeight.w800, letterSpacing: -0.2, color: ColorConstants.slateText, fontFamily: FontConstants.fontFamily, ), ), ) else const Spacer(), SizedBox(width: 6.w), // ── Both decisions, on the bar ── // // Reject used to live on every stop row and went when the rows // became tick-only. That left the rider able to say yes to a // selection and nothing else: declining a stop he could not do // — shop shut, address wrong — had no route at all, and the hub // learned about it by the stop simply never being accepted. // // So it comes back where Accept is, on the set he has picked. // Outlined and unlabelled-by-count against Accept's filled // green: they are not equal choices, and the one that ends work // should not look like the one that starts it. if (onReject != null) ...[ Flexible( child: MilerButton( // No icon, and flexible: the width an icon costs is width // the two labels need side by side on a 320pt phone, and // under real pressure the pair should give ground evenly // rather than run off the edge of the bar. label: 'Reject', variant: MilerButtonVariant.outlined, color: ColorConstants.errorRed, height: ButtonSizes.secondary, expand: false, onPressed: busy ? null : onReject, ), ), SizedBox(width: 8.w), ], // Content-hugging, so the count on the left keeps its room on a // narrow phone and the label never ellipsises — a bulk action // that reads "Accept 3 sto…" is not one the rider will trust. Flexible( child: MilerButton( label: 'Accept $count', icon: Icons.done_all_rounded, color: ColorConstants.acceptGreen, height: ButtonSizes.secondary, expand: false, loading: busy, onPressed: onAccept, ), ), ], ), ), ), ), ); } } class _ClearButton extends StatelessWidget { final VoidCallback? onTap; const _ClearButton({this.onTap}); @override Widget build(BuildContext context) { return Semantics( button: true, label: 'Clear selection', child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(DesignConstants.radiusFull), child: SizedBox( width: ButtonSizes.minTapTarget, height: ButtonSizes.minTapTarget, child: Icon( Icons.close_rounded, size: 20.sp, color: ColorConstants.secondaryText, ), ), ), ); } }