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

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:miler/views/Dashboard/home/trip.dart';
import 'package:miler/views/helpers/constants/miler_surface.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'package:miler/views/helpers/constants/Font_constant.dart';
import 'package:miler/views/helpers/constants/design_constants.dart';
@@ -159,7 +161,12 @@ class _TripTabsState extends State<TripTabs>
/// The colour a segment settles on once the pill is over it.
Color _litColourOf(int tripIndex) {
if (tripIndex >= widget.trips.length) return ColorConstants.secondaryText;
if (widget.trips.tripAt(tripIndex) == null) {
// Selected-but-empty keeps a readable voice — the rider deliberately
// looked at this slot, so it answers — but never the brand: there is
// nothing here to be mission about.
return ColorConstants.secondaryText;
}
return _percentOf(tripIndex) >= 100
? ColorConstants.acceptGreen
: ColorConstants.primary;
@@ -171,16 +178,37 @@ class _TripTabsState extends State<TripTabs>
/// Done" is worth knowing from Trip 2 — so for those the two colours are the
/// same and nothing changes as the pill goes by.
Color _restColourOf(int tripIndex) {
if (tripIndex >= widget.trips.length) return ColorConstants.secondaryText;
if (widget.trips.tripAt(tripIndex) == null) {
// ── An empty slot is furniture, not a peer ──
//
// At `secondaryText` an unassigned tab was indistinguishable from a
// real unselected trip, so a morning screen read as three equal trips
// when two of them were placeholders. The disabled token is what the
// app already means by "nothing here to act on", and it is exactly one
// voice quieter — the slot is still legible, it has simply stopped
// competing.
return ColorConstants.disabledFill;
}
return _percentOf(tripIndex) >= 100
? ColorConstants.acceptGreen
: ColorConstants.secondaryText;
}
int _percentOf(int tripIndex) => widget.trips[tripIndex].completionPercent(
acceptedIds: widget.acceptedIds,
rejectedIds: widget.rejectedIds,
);
/// ── Slot, not position ──
///
/// Every lookup here goes through [TripSlots.tripAt]. Tab 1 is the morning
/// whether or not the rider has morning work, so `trips[i]` — which meant
/// "the i-th trip he happens to have" — would put an afternoon run under the
/// Trip 1 tab on a day with no morning stops, and the rider's Trip 2 would
/// not be the hub's.
int _percentOf(int tripIndex) =>
widget.trips
.tripAt(tripIndex)
?.completionPercent(
acceptedIds: widget.acceptedIds,
rejectedIds: widget.rejectedIds,
) ??
0;
/// True when this trip has a stop in progress — the rider is on it now.
///
@@ -231,7 +259,10 @@ class _TripTabsState extends State<TripTabs>
margin: EdgeInsets.fromLTRB(16.w, 2.h, 16.w, 4.h),
padding: EdgeInsets.all(3.w),
decoration: BoxDecoration(
color: ColorConstants.neutralLight,
// Layer 1 on the canvas, same as every working surface on Home.
// `neutralLight` separated from the old cream backing by 1.02:1 —
// a track you had to be told about.
color: MilerSurface.working,
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
),
// LayoutBuilder OUTSIDE IntrinsicHeight, not inside it. LayoutBuilder
@@ -286,7 +317,9 @@ class _TripTabsState extends State<TripTabs>
// tint, so the two controls a rider uses most looked
// like they came from different apps.
color: ColorConstants.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
borderRadius: BorderRadius.circular(
DesignConstants.radiusLg,
),
),
child: Align(
alignment: Alignment.bottomCenter,
@@ -304,7 +337,9 @@ class _TripTabsState extends State<TripTabs>
),
decoration: BoxDecoration(
color: markColour,
borderRadius: BorderRadius.circular(DesignConstants.radiusFull),
borderRadius: BorderRadius.circular(
DesignConstants.radiusFull,
),
),
),
),
@@ -334,14 +369,13 @@ class _TripTabsState extends State<TripTabs>
return Expanded(
child: _Tab(
label: Trip.tabLabel(i),
percent: i < widget.trips.length
? _percentOf(i)
: 0,
assigned: i < widget.trips.length,
percent: _percentOf(i),
assigned: widget.trips.tripAt(i) != null,
selected: i == widget.selectedIndex,
active:
i < widget.trips.length &&
_isRunning(widget.trips[i]),
active: switch (widget.trips.tripAt(i)) {
final t? => _isRunning(t),
_ => false,
},
lit: lit,
restColour: _restColourOf(i),
litColour: _litColourOf(i),
@@ -479,8 +513,14 @@ class _PinnedSurface extends StatelessWidget {
return AnimatedContainer(
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
// Opaque, or the stop cards show through the bar as they pass under it.
color: ColorConstants.surface,
// Opaque, or the stop cards show through the bar as they pass under
// it. And it is the CANVAS, not `ColorConstants.surface` — that token
// is the warm near-white the surface audit flagged as "warm, unlike
// everything else", and here it painted a cream band across the top
// of a cool grey page. The bar is the page holding its position, so
// it wears the page's own ground; the white segmented track stands on
// it at the full canvas step.
color: MilerSurface.canvas,
border: Border(
bottom: BorderSide(
color: floating ? ColorConstants.borderStrong : Colors.transparent,
@@ -552,7 +592,10 @@ class _Tab extends StatelessWidget {
final Color accent = Color.lerp(restColour, litColour, lit)!;
final String status = !assigned
? 'Not set'
// "Not set" read as a chore waiting for the rider. The hub fills
// these slots; all this tab can honestly say is that nothing has
// arrived for it yet.
? 'None yet'
: complete
? 'Done'
: active
@@ -581,7 +624,9 @@ class _Tab extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.5.sp,
fontWeight: FontWeight.w800,
// A placeholder slot also gives up the headline weight —
// colour alone was not enough separation on a sunlit LCD.
fontWeight: assigned ? FontWeight.w700 : FontWeight.w600,
letterSpacing: -0.3,
color: accent,
fontFamily: FontConstants.fontFamily,
@@ -598,7 +643,7 @@ class _Tab extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10.5.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
color: accent,
fontFamily: FontConstants.fontFamily,
),
@@ -658,17 +703,17 @@ class _OverflowMenu extends StatelessWidget {
Trip.tabLabel(i),
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(width: 8.w),
Text(
i < trips.length ? trips[i].slotLabel : 'Not assigned',
trips.tripAt(i)?.slotLabel ?? 'Not assigned',
style: TextStyle(
fontSize: 11.5.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w500,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
@@ -684,7 +729,7 @@ class _OverflowMenu extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.expand_more_rounded,
LucideIcons.chevronDown,
size: 22.sp,
color: ColorConstants.slateText,
),
@@ -692,7 +737,7 @@ class _OverflowMenu extends StatelessWidget {
'+${hidden.length}',
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),