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