import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:miler/data/service_profile.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/widget/Bottom_page.dart'; /// ───────────────────────────────────────────────────────────────────────── /// ACCEPTED PILL — how many stops you have taken, and the way to them /// /// Home is where the rider *decides*; Bookings is where he *works*. The bridge /// between the two used to be a "Continue trip" button sitting under the last /// hub row of a trip — which meant it only existed once that particular trip /// had nothing left to decide, it moved as the route grew, and it was below the /// fold on any route longer than about four stops. A rider who had accepted /// three stops and scrolled back to the top had nothing on screen telling him /// so, and no way through. /// /// This says both things in one control, in one fixed p~lace: **how many /// bookings he is holding**, and one tap to go work them. It is the counterpart /// to [SelectionBar] — that one is about the stops he is choosing, this one /// about the stops he has already taken — so they share the floating slot at /// the foot of the page and never appear together. A selection in progress is /// the more urgent of the two and wins the slot. /// /// The count comes from the accepted store, which drops a booking once it is /// picked, so the number is "waiting for you on Bookings" rather than "accepted /// at some point today". /// ───────────────────────────────────────────────────────────────────────── class AcceptedPill extends StatelessWidget { /// The tallest this pill gets, excluding the device inset. See /// [SelectionBar.maxHeight] for why the surface declares its own. static double get maxHeight => ButtonSizes.compact + 24.h; /// Accepted bookings still waiting to be worked. Zero renders nothing. final int count; /// Jumps to the Bookings tab. final VoidCallback onTap; const AcceptedPill({super.key, required this.count, required this.onTap}); @override Widget build(BuildContext context) { if (count <= 0) return const SizedBox.shrink(); final profile = ServiceProfile.active; final radius = BorderRadius.circular(DesignConstants.radiusFull); // 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: Align( // Right-aligned and sized to its content: this is an ambient way // through, not the action the screen is for. A full-width green bar // here would out-rank the accept the rider is actually on Home to // make. alignment: Alignment.centerRight, child: Semantics( button: true, // The noun follows the rider's line of work — "booking" on a // parcel route, "delivery" on a meal run. See [ServiceProfile]. label: count == 1 ? '1 ${profile.jobNoun} ${_verb(profile)}. ' 'Go to ${profile.workTabLabel}.' : '$count ${profile.jobNounPlural} ${_verb(profile)}. ' 'Go to ${profile.workTabLabel}.', // ── Red is for the action, not for the notice ── // // This was a solid brand-red slab with a white disc in it, floating // over the foot of the route. On a page whose one accent moment is // supposed to be the journey button, it was the largest and // reddest object on the screen — and it is not an action the screen // exists for at all. It is a *notice with a way through*: "you are // holding nine of these, they are over there." // // So the surface goes quiet — the same lifted white the selection // bar uses, which is the app's one "floating over the list" // material — and the brand is spent on the two things that carry // the meaning: the count and the arrow out. Same height, same // wording, a third of the ink. child: Material( color: ColorConstants.pureSurface, borderRadius: radius, elevation: 0, child: InkWell( onTap: onTap, borderRadius: radius, child: Container( // 48 clears the Material touch-target floor without the pill // reading as a primary CTA. height: 48.h, padding: EdgeInsets.fromLTRB(7.w, 0, 10.w, 0), decoration: BoxDecoration( color: ColorConstants.pureSurface, borderRadius: radius, boxShadow: DesignConstants.shadowFloat, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ // The count as a badge, not as a word in a sentence: it // is the thing the rider is checking from across the // handlebars, and it changes while the words do not. Container( width: 30.w, height: 30.w, alignment: Alignment.center, decoration: BoxDecoration( color: ColorConstants.primary, shape: BoxShape.circle, ), child: Text( '$count', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w700, height: 1, letterSpacing: -0.3, color: ColorConstants.onAccent, fontFamily: FontConstants.fontFamily, ), ), ), SizedBox(width: 9.w), Text( // ── The word has to match the boundary ── // // It said "accepted" on both lines. On a round the // count is `WorkBoundary.deliveryQueue` — work already // **collected** and waiting to be delivered — so a // rider holding twenty-one bags read "21 deliveries // accepted", which is the wrong verb for the wrong // rung and understates what he is carrying. // // Logistics hands over at acceptance, so there the word // is right. Same knob the boundary itself reads. _waiting(profile, count), style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w700, letterSpacing: -0.1, color: ColorConstants.slateText, fontFamily: FontConstants.fontFamily, ), ), SizedBox(width: 2.w), Icon( LucideIcons.chevronRight, size: 20.sp, color: ColorConstants.primary, ), ], ), ), ), ), ), ), ), ); } /// The verb for the rung this count actually sits on. /// /// The pill counts `WorkBoundary.deliveryQueue`, and where that boundary sits /// is a property of the line — [ServiceProfile.handoffAt]. On a round it is /// **pickup-complete**, so the work is in the rider's hands; on logistics it /// is acceptance, so "accepted" is exactly right. One knob, read here rather /// than a line-name check. static String _verb(ServiceProfile profile) => profile.handsOffAtCollection ? 'in hand' : 'accepted'; /// `deliveries in hand` · `bookings accepted`. static String _waiting(ServiceProfile profile, int count) => '${count == 1 ? profile.jobNoun : profile.jobNounPlural} ' '${_verb(profile)}'; }