Miler rider app: surface system, visible design language, backend lifecycle

Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 05:40:35 +05:30
parent c8350563d9
commit d7348e253f
387 changed files with 80693 additions and 12272 deletions

View File

@@ -491,15 +491,24 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen>
}
void _showCustomerDetailsSheet(Map<String, dynamic> pickup, int stepNumber) {
showModalBottomSheet(
context: context,
sheetAnimationStyle: kMilerSheetStyle,
isScrollControlled: true,
isDismissible: true,
enableDrag: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
// ── Through the kit ──
//
// The furthest-drifted sheet in the app, and every difference was a
// decision re-made rather than inherited:
//
// • its own `RoundedRectangleBorder(20)` — a radius nothing else uses;
// • **no glass at all**, so it arrived opaque white over a live map while
// every other sheet is frosted;
// • `SafeArea(bottom: true)` **and** `Padding(bottom: viewInsets.bottom)`
// — the two summed, which is the double-inset bug written out in full:
// with the keyboard up the content floats a navigation bar's height
// above it;
// • a 24 gutter against the app's 20;
// • a 22sp/w600 title in **brand red**, where the system's header is
// 18/w700 slate — the accent belongs on the badge, not on the sentence;
// • and a close ✕, which the handle and the barrier already do.
showMilerSheet<void>(
context,
builder: (BuildContext context) {
final customerName = (pickup['pickupcustomer'] ?? 'Customer')
.toString();
@@ -511,267 +520,226 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen>
.trim();
final isSkipped = status == 'skipped';
return SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
return MilerSheetScaffold(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const MilerSheetHeader(
title: 'Customer details',
icon: LucideIcons.mapPinned,
),
SizedBox(height: 4.h),
// Step number and status
Row(
children: [
// Header with title and close icon
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Customer Details',
style: TextStyle(
fontSize: 22.sp,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.primary,
),
),
),
InkWell(
onTap: () => Navigator.of(context).pop(),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: ColorConstants.dividerColor,
shape: BoxShape.circle,
),
// A close affordance, not a destructive action.
// This was a 34px red `Icons.cancel` — the loudest
// thing in the sheet, competing with the call button
// and reading as "delete this stop".
child: Icon(
Icons.close_rounded,
size: 20.sp,
color: ColorConstants.slateText,
),
),
),
],
),
const Divider(thickness: 1.5),
// Step number and status
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: isSkipped
? ColorConstants.warning
: ColorConstants.primary,
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
),
child: Text(
'Step $stepNumber',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14.sp,
),
),
),
if (isSkipped) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: ColorConstants.warning,
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
),
child: Text(
'SKIPPED',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12.sp,
),
),
),
],
],
),
const SizedBox(height: 16),
// Customer name
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.person,
size: 20,
color: ColorConstants.secondaryText,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Customer Name',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 16.sp,
color: ColorConstants.slateText,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
customerName,
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: ColorConstants.secondaryText,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Pickup address
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.location_on,
size: 20,
color: ColorConstants.secondaryText,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recipient Address',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 16.sp,
color: ColorConstants.slateText,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
address,
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: ColorConstants.secondaryText,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Phone number
if (phone.isNotEmpty)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.phone,
size: 20,
color: ColorConstants.secondaryText,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Contact Number',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 16.sp,
color: ColorConstants.slateText,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
phone,
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: ColorConstants.secondaryText,
),
),
],
),
),
],
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
const SizedBox(height: 20),
// Booking ID
Text(
'Booking ID: $orderId',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 16.sp,
color: ColorConstants.slateText,
fontStyle: FontStyle.italic,
decoration: BoxDecoration(
color: isSkipped
? ColorConstants.warning
: ColorConstants.primary,
borderRadius: BorderRadius.circular(
DesignConstants.radiusLg,
),
),
child: Text(
'Step $stepNumber',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 14.sp,
),
),
),
const SizedBox(height: 20),
// Call button
MilerButton(
label: 'Call',
icon: Icons.phone,
color: ColorConstants.acceptGreen,
onPressed: () async {
// Close bottom sheet first
Navigator.of(context).pop();
// Small delay to ensure bottom sheet is closed
await Future.delayed(const Duration(milliseconds: 300));
// Then make the call
final bool success = await launchPhoneDialer(phone);
if (!success && context.mounted) {
AppFeedback.error(context, 'Could not open the dialer');
}
},
),
const SizedBox(height: 8),
if (isSkipped) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: ColorConstants.warning,
borderRadius: BorderRadius.circular(
DesignConstants.radiusLg,
),
),
child: Text(
'SKIPPED',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 12.sp,
),
),
),
],
],
),
),
const SizedBox(height: 16),
// Customer name
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
LucideIcons.user,
size: 20,
color: ColorConstants.secondaryText,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Customer Name',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 16.sp,
color: ColorConstants.slateText,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
customerName,
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: ColorConstants.secondaryText,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Pickup address
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
LucideIcons.mapPin,
size: 20,
color: ColorConstants.secondaryText,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recipient Address',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 16.sp,
color: ColorConstants.slateText,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
address,
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: ColorConstants.secondaryText,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Phone number
if (phone.isNotEmpty)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
LucideIcons.phone,
size: 20,
color: ColorConstants.secondaryText,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Contact Number',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 16.sp,
color: ColorConstants.slateText,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
phone,
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: ColorConstants.secondaryText,
),
),
],
),
),
],
),
const SizedBox(height: 20),
// Booking ID
Text(
'Booking ID: $orderId',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 16.sp,
color: ColorConstants.slateText,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 20),
// Call button
MilerButton(
label: 'Call',
icon: LucideIcons.phone,
color: ColorConstants.acceptGreen,
onPressed: () async {
// Close bottom sheet first
Navigator.of(context).pop();
// Small delay to ensure bottom sheet is closed
await Future.delayed(const Duration(milliseconds: 300));
// Then make the call
final bool success = await launchPhoneDialer(phone);
if (!success && context.mounted) {
AppFeedback.error(context, 'Could not open the dialer');
}
},
),
const SizedBox(height: 8),
],
),
),
);
@@ -823,35 +791,17 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen>
// Inverted, and rebuilt as an ordinary AppBar: white surface, brand red on
// the title where it costs nothing, a real `leading` slot, and the same
// hairline the rest of the app uses to close a bar.
appBar: AppBar(
// The app-wide frosted brand wash — see [milerGlassSurface].
backgroundColor: Colors.transparent,
flexibleSpace: milerGlassSurface(),
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
titleSpacing: 0,
leading: IconButton(
tooltip: 'Back',
icon: Icon(
Icons.arrow_back_rounded,
color: ColorConstants.slateText,
size: 22.sp,
),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Route map',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: 19.sp,
color: ColorConstants.primary,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
),
),
centerTitle: false,
),
// ── The sixth app-bar implementation, folded into the one ──
//
// This was a hand-rolled `AppBar`: a frosted wash, a maroon 19sp title
// and its own back button — so the map page's heading was a different
// colour and a different size from every other pushed screen. The audit
// note on [milerPageBar] says that bar is the only one a pushed screen
// gets; this one had simply never been converted.
//
// Now it inherits the brand ground, the white title and the shared back
// control along with everything else, and cannot drift again.
appBar: milerPageBar('Route map', onBack: () => Navigator.pop(context)),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: AfterEntrance(