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,8 +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/helpers/constants/Colorconstants.dart';
import 'package:miler/views/helpers/constants/Font_constant.dart';
import 'package:miler/views/helpers/constants/miler_type.dart';
import 'package:miler/views/helpers/widgets/app_widgets.dart';
import 'package:miler/views/helpers/constants/design_constants.dart';
@@ -38,17 +39,18 @@ class SettingsCard extends StatelessWidget {
const SettingsCard({super.key, required this.child, this.padding});
/// ── No outline ──
///
/// The card carried a hairline border *and* a shadow, which is two
/// separations doing one job: a lift already says "this is in front of the
/// page", and the line drawn round it only added weight. Every surface in the
/// app that a rider scrolls past — the stop cards on Home, the job cards on
/// Deliveries — is a borderless fill under [DesignConstants.shadowGlass], and
/// this is now the same object in the section's own white.
static BoxDecoration decoration() => BoxDecoration(
color: ColorConstants.surfaceContainerLowest,
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
border: Border.all(color: ColorConstants.dividerColor),
boxShadow: [
BoxShadow(
color: ColorConstants.onSurface.withValues(alpha: 0.04),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
boxShadow: DesignConstants.shadowGlass,
);
@override
@@ -169,7 +171,15 @@ class SettingsRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Color fg = danger ? ColorConstants.primary : ColorConstants.onSurface;
// ── Destructive is red, not brand red ──
//
// The rule the rest of the app already follows: green advances the job at a
// door, brand maroon is navigation and identity, and red destroys. Logout
// was drawn in the brand colour, which made the one row that ends the
// session the same colour as the logo above it.
final Color fg = danger
? ColorConstants.errorRed
: ColorConstants.onSurface;
return PressScale(
onTap: onTap,
@@ -178,52 +188,135 @@ class SettingsRow extends StatelessWidget {
semanticLabel: subtitle == null || subtitle!.trim().isEmpty
? title
: '$title. $subtitle',
child: Padding(
// No horizontal inset: the group is no longer inside a card, so the row
// starts on the page's own gutter and the section heading above it
// lines up with the icon rather than floating inboard of it.
padding: EdgeInsets.symmetric(vertical: 12.h),
child: ConstrainedBox(
// A row is the tap target, so the row is never shorter than one.
constraints: BoxConstraints(minHeight: ButtonSizes.secondary.h),
child: Padding(
// No horizontal inset: the group is no longer inside a card, so the
// row starts on the page's own gutter and the section heading above
// it lines up with the icon rather than floating inboard of it.
padding: EdgeInsets.symmetric(vertical: 11.h),
child: Row(
children: [
// ── The icon does not outrank the title ──
//
// Every glyph on the page was drawn in `onSurface`, the same
// near-black as the words beside it, so a column of twelve icons
// read as loudly as the twelve things they were labelling. They
// are a column of markers, not the content; the title leads and
// the icon locates it.
Icon(
icon,
color:
iconColor ?? (danger ? fg : ColorConstants.secondaryText),
size: 21.sp,
),
SizedBox(width: 14.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: MilerType.body.on(fg)),
if (subtitle != null && subtitle!.trim().isNotEmpty) ...[
SizedBox(height: 2.h),
Text(
subtitle!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: MilerType.label,
),
],
],
),
),
if (trailing != null) ...[
SizedBox(width: 10.w),
trailing!,
] else if (!danger) ...[
SizedBox(width: 10.w),
// An affordance, not a mark. At `onSurfaceVariant` it was as
// dark as the subtitle it sat beside; it only has to say the
// row goes somewhere.
Icon(
LucideIcons.chevronRight,
color: ColorConstants.borderStrong,
size: 20.sp,
),
],
],
),
),
),
);
}
}
/// ─────────────────────────────────────────────────────────────────────────
/// THE ONE CONTROL IN THIS SECTION THAT IS EVER URGENT
///
/// `⚠ Emergency help 📞`
/// Calls 112 if you are in immediate danger
///
/// ── A wash, not an outline ──
///
/// Account and Help & Support each drew this as an `InfoBanner`: an 8% brand
/// fill inside a 16% brand *border*, with the whole sentence set in red. Three
/// ways of saying one thing — and, once the section's cards lost their edges,
/// the only outlined object left on either page. The loudest element on a
/// rider's account was a notice rather than anything he owns.
///
/// The wash stays, because this row genuinely is not a preference. The border
/// goes, the words go back to the page's near-black so they can be read at a
/// glance rather than decoded, and the red is spent on the two marks that
/// carry the meaning: the warning glyph and the handset.
///
/// One widget rather than two copies, because the two pages had already
/// drifted apart on the copy — and the sentence that tells a rider what will
/// happen when he presses it is not a thing to have two versions of.
class EmergencyRow extends StatelessWidget {
/// The number that will be dialled. Named in the subtitle, so the rider can
/// see it before he presses and dial it himself if the handset refuses.
final String number;
final VoidCallback onTap;
const EmergencyRow({super.key, required this.number, required this.onTap});
@override
Widget build(BuildContext context) {
return PressScale(
onTap: onTap,
semanticLabel: 'Emergency. Calls $number',
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h),
decoration: BoxDecoration(
color: ColorConstants.primary.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
),
child: Row(
children: [
Icon(icon, color: iconColor ?? fg, size: 22.sp),
Icon(
LucideIcons.triangleAlert,
color: ColorConstants.primary,
size: 22.sp,
),
SizedBox(width: 14.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Emergency help', style: MilerType.body.bold),
SizedBox(height: 2.h),
Text(
title,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: fg,
fontFamily: FontConstants.fontFamily,
),
'Calls $number if you are in immediate danger',
maxLines: 2,
style: MilerType.micro,
),
if (subtitle != null && subtitle!.trim().isNotEmpty) ...[
SizedBox(height: 2.h),
Text(
subtitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.sp,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
),
),
],
],
),
),
if (trailing != null)
trailing!
else if (!danger)
Icon(
Icons.chevron_right_rounded,
color: ColorConstants.onSurfaceVariant,
size: 20.sp,
),
SizedBox(width: 10.w),
Icon(LucideIcons.phone, color: ColorConstants.primary, size: 20.sp),
],
),
),
@@ -239,17 +332,21 @@ class SettingsLabel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: 4.h),
child: Text(
text.toUpperCase(),
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w800,
letterSpacing: 1.1,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
),
// ── Aligned to the titles, not to the icons ──
//
// The label used to start on the page gutter, level with the row icons
// below it. Text-to-glyph alignment is optical, never exact: an `Icon`
// does not paint flush to the left of its box, so an 11sp heading and a
// 21sp glyph starting at the same x read as eight points out of true —
// and a column of three headings all eight points out is what makes a
// settings page look assembled rather than laid out.
//
// Text-to-text is exact. The heading sits over the words it heads, and
// the icons become a clean margin rail to its left — the arrangement iOS
// Settings has used since it had groups. The inset is the row's own icon
// box plus its gap, read from the same numbers [SettingsRow] uses.
padding: EdgeInsetsDirectional.only(start: 21.sp + 14.w, bottom: 6.h),
child: Text(text.toUpperCase(), style: MilerType.eyebrow),
);
}
}
@@ -263,16 +360,8 @@ class SettingsHint extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(4.w, 8.h, 4.w, 0),
child: Text(
text,
style: TextStyle(
fontSize: 12.sp,
height: 1.45,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
),
),
padding: EdgeInsets.fromLTRB(2.w, 10.h, 2.w, 0),
child: Text(text, style: MilerType.micro.copyWith(height: 1.5)),
);
}
}