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/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -50,7 +51,7 @@ import 'package:miler/views/helpers/widgets/app_widgets.dart';
abstract final class AuthType {
static TextStyle get title => TextStyle(
fontSize: 27.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: -0.7,
height: 1.15,
color: ColorConstants.slateText,
@@ -67,7 +68,7 @@ abstract final class AuthType {
static TextStyle get fieldLabel => TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
@@ -75,7 +76,7 @@ abstract final class AuthType {
static TextStyle get meta => TextStyle(
fontSize: 12.5.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w500,
height: 1.45,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
@@ -168,7 +169,7 @@ class AuthSteps extends StatelessWidget {
'STEP $step OF $total',
style: TextStyle(
fontSize: 10.5.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
@@ -188,7 +189,9 @@ class AuthSteps extends StatelessWidget {
color: i <= step
? ColorConstants.primary
: ColorConstants.borderStrong,
borderRadius: BorderRadius.circular(DesignConstants.radiusFull),
borderRadius: BorderRadius.circular(
DesignConstants.radiusFull,
),
),
),
),
@@ -278,7 +281,7 @@ class AuthScaffold extends StatelessWidget {
onTap: onBack!,
semanticLabel: 'Back',
child: Icon(
Icons.arrow_back_rounded,
LucideIcons.arrowLeft,
size: 22.sp,
color: ColorConstants.slateText,
),
@@ -385,7 +388,7 @@ class AuthScaffold extends StatelessWidget {
onTap: onBack!,
semanticLabel: 'Back',
child: Icon(
Icons.arrow_back_rounded,
LucideIcons.arrowLeft,
size: 22.sp,
color: ColorConstants.onAccent,
),
@@ -513,7 +516,8 @@ class AuthBrandBanner extends StatelessWidget {
duration: DesignConstants.motionState,
curve: Curves.easeOutCubic,
width: double.infinity,
height: (typing ? _typingHeight : _restHeight).h +
height:
(typing ? _typingHeight : _restHeight).h +
MediaQuery.paddingOf(context).top,
color: ColorConstants.primary,
child: ClipRect(
@@ -526,8 +530,16 @@ class AuthBrandBanner extends StatelessWidget {
// ornament. Bled off both edges and held at 7% white: at that
// weight they read as texture on the brand colour and never as
// objects the rider might try to tap.
Positioned(left: -78.w, top: -34.h, child: const _BrandRing(size: 196)),
Positioned(right: -92.w, bottom: -78.h, child: const _BrandRing(size: 244)),
Positioned(
left: -78.w,
top: -34.h,
child: const _BrandRing(size: 196),
),
Positioned(
right: -92.w,
bottom: -78.h,
child: const _BrandRing(size: 244),
),
Center(
child: Padding(
@@ -566,7 +578,7 @@ class AuthBrandBanner extends StatelessWidget {
'MILER · RIDER APP',
style: TextStyle(
fontSize: 10.5.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: 1.6,
color: ColorConstants.onAccent.withValues(alpha: 0.72),
fontFamily: FontConstants.fontFamily,
@@ -667,7 +679,7 @@ class AuthPrimaryButton extends StatelessWidget {
const AuthPrimaryButton({
super.key,
required this.label,
this.icon = Icons.arrow_forward_rounded,
this.icon = LucideIcons.arrowRight,
this.loading = false,
required this.onPressed,
});
@@ -719,7 +731,7 @@ class AuthPrimaryButton extends StatelessWidget {
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
fontFamily: FontConstants.fontFamily,
),
@@ -774,7 +786,7 @@ class AuthTextAction extends StatelessWidget {
label,
style: TextStyle(
fontSize: 13.5.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: color,
fontFamily: FontConstants.fontFamily,
),
@@ -812,7 +824,7 @@ class AuthInlineError extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.error_outline_rounded,
LucideIcons.circleAlert,
size: 15.sp,
color: ColorConstants.errorRed,
),
@@ -823,7 +835,7 @@ class AuthInlineError extends StatelessWidget {
style: TextStyle(
color: ColorConstants.errorRed,
fontSize: 12.5.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
height: 1.35,
fontFamily: FontConstants.fontFamily,
),
@@ -865,31 +877,65 @@ class AuthCodeRow extends StatelessWidget {
this.hasError = false,
});
/// ── The boxes stopped stretching ──
///
/// Each box was `Expanded` inside an `AspectRatio`, so four of them divided
/// the *whole* content width between themselves: on a 390pt phone that is a
/// **79×83pt** box, four of them, filling the screen edge to edge. A PIN
/// field that size reads as a keypad rather than as an input — the digits
/// float in the middle of a large empty square, the group dominates a screen
/// whose only job is four taps, and it gets worse on a tablet, where the
/// boxes keep growing with the window.
///
/// Every app that does this well — the ones a rider already uses to bank and
/// to sign in — sizes the box to the *digit*, not to the screen, and centres
/// the group. 56pt is a comfortable target well above the 44pt floor, and
/// four of them plus their gaps come to 260 of the 390 available, so the
/// group reads as one control with air around it instead of a wall.
///
/// The size is a *preference*, not a constant: [LayoutBuilder] caps it at
/// whatever the width actually allows, so a 6-digit code on a 320pt phone
/// shrinks to fit rather than overflowing. It can get smaller than preferred;
/// it can never get bigger.
@override
Widget build(BuildContext context) {
final count = controllers.length;
// Taller boxes when there are fewer of them: a 4-digit PIN gets room to
// feel deliberate, a 6-digit code has to stay inside the width.
final ratio = count > 4 ? 1.20 : 1.05;
final double gap = MilerCodeBox.gap(context).w * (count > 4 ? 1 : 1.5);
return Row(
children: [
for (var i = 0; i < count; i++) ...[
if (i > 0) SizedBox(width: MilerCodeBox.gap(context).w),
Expanded(
child: AspectRatio(
aspectRatio: 1 / ratio,
child: _Box(
controller: controllers[i],
node: nodes[i],
obscure: obscure,
hasError: hasError,
onChanged: (v) => onChanged(v, i),
// A 4-digit PIN gets the full target; a 6-digit code starts smaller so it
// is not relying on the cap to be legible on a narrow phone.
final double preferred = (count > 4 ? 46 : 56).w;
return LayoutBuilder(
builder: (context, constraints) {
final double available = constraints.maxWidth - gap * (count - 1);
final double box = preferred < available / count
? preferred
: available / count;
// Barely taller than it is wide. A tall box puts the digit in the
// middle of empty space; this reads as a field.
final double height = box * 1.12;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < count; i++) ...[
if (i > 0) SizedBox(width: gap),
SizedBox(
width: box,
height: height,
child: _Box(
controller: controllers[i],
node: nodes[i],
obscure: obscure,
hasError: hasError,
onChanged: (v) => onChanged(v, i),
),
),
),
),
],
],
],
],
);
},
);
}
}