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,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -40,8 +41,9 @@ void main() {
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context)
.copyWith(textScaler: TextScaler.linear(textScale)),
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.linear(textScale)),
child: child!,
),
home: page,
@@ -52,39 +54,59 @@ void main() {
}
group('the shared pushed-page bar', () {
testWidgets('is a glass surface with a slate title', (tester) async {
// ── The wash gave way to the brand, everywhere at once ──
//
// This bar wore a frosted 7% brand wash flattened against white while Home
// and the four tabs are solid brand. So pushing Edit Profile, Saved
// Addresses, Alert Sound or Notifications changed the colour of the top of
// the phone AND the colour of the status-bar glyphs — which reads as
// leaving the app rather than opening a page inside it.
//
// One ground now, and everything on it inverts. The contract this test
// still holds is the one that matters: whatever the bar's surface is, the
// type on it has to clear contrast against it, and every bar in the app has
// to agree on the answer.
testWidgets('is solid brand with a white title', (tester) async {
await pump(
tester,
Scaffold(appBar: milerPageBar('Saved Addresses', onBack: () {})),
);
// The bar is transparent and wears `milerGlassSurface` — a frosted 7%
// brand wash — rather than a solid slab of either white or red. The rule
// it is holding to is unchanged: the surface stays light enough for
// near-black type, so the loudest colour in the palette is not spent on
// the one element that never changes.
final bar = tester.widget<AppBar>(find.byType(AppBar));
expect(bar.backgroundColor, Colors.transparent);
expect(bar.flexibleSpace, isNotNull,
reason: 'the glass wash is what colours the bar now');
expect(bar.backgroundColor, ColorConstants.primary);
expect(
bar.flexibleSpace,
isNull,
reason: 'a solid ground needs no surface layered over it',
);
expect(
bar.systemOverlayStyle?.statusBarIconBrightness,
Brightness.light,
reason:
'dark glyphs on maroon is the one contrast failure a rider '
'cannot work around by tilting the phone',
);
// ── Slate, not maroon ──
//
// This was the only heading in the app in the brand colour, so an Account
// page's title read as a link while the same heading on a tab was
// near-black. One title colour across every bar; the brand still owns the
// glass wash behind it.
final title = tester.widget<Text>(find.text('Saved Addresses'));
expect(title.style!.color, ColorConstants.slateText);
expect(title.style!.fontSize, MilerType.pageTitleFixed.fontSize,
reason: 'one page-title size across tabs and pushed screens');
expect(title.style!.color, Colors.white);
expect(
title.style!.fontSize,
MilerType.pageTitleFixed.fontSize,
reason: 'one page-title size across tabs and pushed screens',
);
// The back control inverts with it, or it disappears into the ground.
final back = tester.widget<Icon>(find.byIcon(LucideIcons.arrowLeft));
expect(back.color, Colors.white);
});
testWidgets('carries a back affordance when given one', (tester) async {
var popped = false;
await pump(
tester,
Scaffold(appBar: milerPageBar('Notifications', onBack: () => popped = true)),
Scaffold(
appBar: milerPageBar('Notifications', onBack: () => popped = true),
),
);
// ── An arrow, because these pages are pushed ──
@@ -92,18 +114,18 @@ void main() {
// The cross promised dismissal on nine screens you actually go *back*
// from, and it was one of three back glyphs in the app. `close_rounded`
// survives behind `isModal: true`, for a sheet that genuinely dismisses.
expect(find.byIcon(Icons.arrow_back_rounded), findsOneWidget);
expect(find.byIcon(Icons.close_rounded), findsNothing);
expect(find.byIcon(LucideIcons.arrowLeft), findsOneWidget);
expect(find.byIcon(LucideIcons.x), findsNothing);
await tester.tap(find.byIcon(Icons.arrow_back_rounded));
await tester.tap(find.byIcon(LucideIcons.arrowLeft));
await tester.pumpAndSettle();
expect(popped, isTrue);
});
testWidgets('and none when it is not', (tester) async {
await pump(tester, Scaffold(appBar: milerPageBar('Terms')));
expect(find.byIcon(Icons.arrow_back_rounded), findsNothing);
expect(find.byIcon(Icons.close_rounded), findsNothing);
expect(find.byIcon(LucideIcons.arrowLeft), findsNothing);
expect(find.byIcon(LucideIcons.x), findsNothing);
});
testWidgets('a modal presentation still gets a cross', (tester) async {
@@ -113,8 +135,8 @@ void main() {
appBar: milerPageBar('Skip stop', onBack: () {}, isModal: true),
),
);
expect(find.byIcon(Icons.close_rounded), findsOneWidget);
expect(find.byIcon(Icons.arrow_back_rounded), findsNothing);
expect(find.byIcon(LucideIcons.x), findsOneWidget);
expect(find.byIcon(LucideIcons.arrowLeft), findsNothing);
});
testWidgets('survives a long title and a large font', (tester) async {
@@ -158,7 +180,7 @@ void main() {
Scaffold(appBar: milerPageBar('Edit Profile', onBack: () {})),
);
final close = tester.getRect(find.byIcon(Icons.arrow_back_rounded));
final close = tester.getRect(find.byIcon(LucideIcons.arrowLeft));
final title = tester.getRect(find.text('Edit Profile'));
expect(
@@ -181,10 +203,12 @@ void main() {
Scaffold(appBar: milerPageBar('Edit Profile', onBack: () {})),
);
final box = tester.getSize(
find.ancestor(
of: find.byIcon(Icons.arrow_back_rounded),
matching: find.byType(SizedBox),
).first,
find
.ancestor(
of: find.byIcon(LucideIcons.arrowLeft),
matching: find.byType(SizedBox),
)
.first,
);
// 48, not the 44pt accessibility floor: this is the control a rider hits
// one-handed, and the glyph inside stays 24 so only the target grew.
@@ -201,19 +225,25 @@ void main() {
for (final width in [320.0, 360.0, 390.0, 430.0]) {
for (final scale in [1.0, 1.3, 1.5, 2.0]) {
testWidgets('at ${width.toInt()}pt / ${scale}x', (tester) async {
await pump(tester, const FaqPage(),
width: width, textScale: scale);
expect(tester.takeException(), isNull,
reason: 'FAQ overflowed at ${width.toInt()}pt / ${scale}x');
await pump(tester, const FaqPage(), width: width, textScale: scale);
expect(
tester.takeException(),
isNull,
reason: 'FAQ overflowed at ${width.toInt()}pt / ${scale}x',
);
// Opening the accordions is where the page grows the most.
final tiles = find.byType(ExpansionTile);
for (var i = 0; i < tiles.evaluate().length; i++) {
await tester.tap(tiles.at(i), warnIfMissed: false);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull,
reason: 'FAQ overflowed opening accordion $i at '
'${width.toInt()}pt / ${scale}x');
expect(
tester.takeException(),
isNull,
reason:
'FAQ overflowed opening accordion $i at '
'${width.toInt()}pt / ${scale}x',
);
}
});
}