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:
@@ -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',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user