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';
@@ -24,18 +25,14 @@ void main() {
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
theme: AppTheme.light,
home: child,
),
builder: (_, _) => MaterialApp(theme: AppTheme.light, home: child),
),
);
await tester.pumpAndSettle();
}
Widget codeRow(int count, {bool error = false}) {
final controllers =
List.generate(count, (_) => TextEditingController());
final controllers = List.generate(count, (_) => TextEditingController());
final nodes = List.generate(count, (_) => FocusNode());
return Scaffold(
body: Padding(
@@ -56,8 +53,9 @@ void main() {
// width happens to divide that way; it has no headroom and cannot adapt.
for (final count in [4, 6]) {
for (final width in [320.0, 360.0, 390.0, 430.0]) {
testWidgets('$count boxes fit a ${width.toInt()}pt screen',
(tester) async {
testWidgets('$count boxes fit a ${width.toInt()}pt screen', (
tester,
) async {
await pump(tester, codeRow(count), screen: Size(width, 780));
expect(tester.takeException(), isNull);
expect(find.byType(TextField), findsNCount(count));
@@ -90,14 +88,18 @@ void main() {
});
group('AuthPrimaryButton', () {
testWidgets('the label is not squeezed by the theme form metrics',
(tester) async {
testWidgets('the label is not squeezed by the theme form metrics', (
tester,
) async {
await pump(
tester,
const Scaffold(
body: Padding(
padding: EdgeInsets.all(24),
child: AuthPrimaryButton(label: 'Verify & Continue', onPressed: _noop),
child: AuthPrimaryButton(
label: 'Verify & Continue',
onPressed: _noop,
),
),
),
);
@@ -152,8 +154,11 @@ void main() {
final body = tester.getRect(find.byType(SingleChildScrollView));
final footer = tester.getRect(find.byType(ElevatedButton));
expect(footer.top, greaterThanOrEqualTo(body.bottom - 0.01),
reason: 'the CTA sits below the scroll area, not inside it');
expect(
footer.top,
greaterThanOrEqualTo(body.bottom - 0.01),
reason: 'the CTA sits below the scroll area, not inside it',
);
});
testWidgets('the back target clears the 44dp floor', (tester) async {
@@ -166,19 +171,22 @@ void main() {
children: const [],
),
);
final back = tester.getRect(find.byIcon(Icons.arrow_back_rounded));
final back = tester.getRect(find.byIcon(LucideIcons.arrowLeft));
// The glyph is small; the tappable box around it must not be.
final target = tester.getRect(find.ancestor(
of: find.byIcon(Icons.arrow_back_rounded),
matching: find.byType(InkResponse),
));
final target = tester.getRect(
find.ancestor(
of: find.byIcon(LucideIcons.arrowLeft),
matching: find.byType(InkResponse),
),
);
expect(back.width, lessThan(target.width));
expect(target.width, greaterThanOrEqualTo(ButtonSizes.minTapTarget));
expect(target.height, greaterThanOrEqualTo(ButtonSizes.minTapTarget));
});
testWidgets('no step rail when the screen is outside the flow',
(tester) async {
testWidgets('no step rail when the screen is outside the flow', (
tester,
) async {
await pump(
tester,
AuthScaffold(
@@ -215,13 +223,13 @@ void main() {
/// layout change. Both are asserted rather than eyeballed.
group('AuthBrandBanner', () {
Widget bannered() => AuthScaffold(
step: 1,
banner: const AuthBrandBanner(),
title: 'Enter your number',
subtitle: 'We use it to sign you in.',
footer: const AuthPrimaryButton(label: 'Continue', onPressed: _noop),
children: [Container(height: 56, color: Colors.white)],
);
step: 1,
banner: const AuthBrandBanner(),
title: 'Enter your number',
subtitle: 'We use it to sign you in.',
footer: const AuthPrimaryButton(label: 'Continue', onPressed: _noop),
children: [Container(height: 56, color: Colors.white)],
);
for (final width in [320.0, 360.0, 390.0, 430.0]) {
testWidgets('lays out on a ${width.toInt()}pt screen', (tester) async {
@@ -233,23 +241,27 @@ void main() {
// The block must not run away with the screen. Half the height is the
// point at which the sheet stops being the subject of the page.
final banner = tester.getRect(find.byType(AuthBrandBanner));
expect(banner.height, lessThan(720 * 0.5),
reason: 'the brand block is a header, not the page');
expect(
banner.height,
lessThan(720 * 0.5),
reason: 'the brand block is a header, not the page',
);
});
}
testWidgets('the wordmark keeps its aspect ratio', (tester) async {
await pump(tester, bannered());
final mark = tester.getRect(find.image(
const AssetImage('assets/images/doormile-logo.png'),
));
final mark = tester.getRect(
find.image(const AssetImage('assets/images/doormile-logo.png')),
);
// Source is 748×100. A squashed mark is the classic symptom of a banner
// being re-laid-out without the ratio being held explicitly.
expect(mark.width / mark.height, closeTo(748 / 100, 0.05));
});
testWidgets('it gets out of the way when the keyboard opens',
(tester) async {
testWidgets('it gets out of the way when the keyboard opens', (
tester,
) async {
await pump(tester, bannered());
final atRest = tester.getRect(find.byType(AuthBrandBanner)).height;
@@ -265,8 +277,11 @@ void main() {
await tester.pumpAndSettle();
final typing = tester.getRect(find.byType(AuthBrandBanner)).height;
expect(typing, lessThan(atRest),
reason: 'the block halves itself so the field stays visible');
expect(
typing,
lessThan(atRest),
reason: 'the block halves itself so the field stays visible',
);
// And the mark is still there — it shrinks, it does not disappear.
expect(find.byType(AuthBrandBanner), findsOneWidget);
});
@@ -289,8 +304,11 @@ void main() {
addTearDown(tester.view.resetViewInsets);
await tester.pumpAndSettle();
expect(tester.getRect(find.byType(AuthBrandBanner)).height, first,
reason: 'a compact banner has nowhere to shrink to');
expect(
tester.getRect(find.byType(AuthBrandBanner)).height,
first,
reason: 'a compact banner has nowhere to shrink to',
);
});
// ── The banner used to eat the back button ──
@@ -309,12 +327,14 @@ void main() {
children: const [],
),
);
expect(find.byIcon(Icons.arrow_back_rounded), findsOneWidget);
expect(find.byIcon(LucideIcons.arrowLeft), findsOneWidget);
final target = tester.getRect(find.ancestor(
of: find.byIcon(Icons.arrow_back_rounded),
matching: find.byType(InkResponse),
));
final target = tester.getRect(
find.ancestor(
of: find.byIcon(LucideIcons.arrowLeft),
matching: find.byType(InkResponse),
),
);
expect(target.width, greaterThanOrEqualTo(ButtonSizes.minTapTarget));
expect(target.height, greaterThanOrEqualTo(ButtonSizes.minTapTarget));
@@ -341,8 +361,10 @@ void main() {
),
);
expect(find.text('That code is wrong.'), findsOneWidget);
expect(tester.getSize(find.byType(AuthInlineError)).height,
greaterThan(0));
expect(
tester.getSize(find.byType(AuthInlineError)).height,
greaterThan(0),
);
});
});
}