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>
197 lines
10 KiB
Dart
197 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// Miler Professional Design System Constants
|
||
/// Centralized design tokens for spacing, radius, shadows, and layout
|
||
/// ─────────────────────────────────────────────────────────────────────────
|
||
/// SHARED RADII AND ELEVATION
|
||
///
|
||
/// ── Why this used to be much bigger ──
|
||
///
|
||
/// This class shipped 43 members: a nine-step spacing scale, eleven `EdgeInsets`
|
||
/// presets, six radii, five shadows, a `cardDecoration` helper and three named
|
||
/// border radii. An audit found **six of them used, 17 times in total.** The
|
||
/// spacing scale had never been referenced once; every screen writes `16.w` or
|
||
/// `12.h` inline, which is both more readable at the call site and correct for
|
||
/// this app, because those need ScreenUtil's per-device scaling and a `const
|
||
/// double` cannot have it.
|
||
///
|
||
/// The 37 dead members were not harmless. A token nobody uses still has to be
|
||
/// read, still looks authoritative, and is the reason someone reaches for
|
||
/// `Colors.grey.shade100` instead — a design system that is mostly aspirational
|
||
/// teaches people to ignore it. So the unused members are gone and what is left
|
||
/// is only what the app actually reaches for.
|
||
///
|
||
/// The audit metric that flagged this file was itself misleading, which is worth
|
||
/// recording: 13 files import `design_constants.dart` and only 5 mention
|
||
/// `DesignConstants`. The other 8 import it for [ButtonSizes], which lives in
|
||
/// this same file and is used 72 times. Colocating a thriving class with a dead
|
||
/// one hid both facts.
|
||
///
|
||
/// **Adding to this class:** don't, until a second call site needs the value.
|
||
/// Two uses make a token; one use is a local constant.
|
||
/// ─────────────────────────────────────────────────────────────────────────
|
||
class DesignConstants {
|
||
DesignConstants._();
|
||
|
||
// ── Motion ────────────────────────────────────────────────────────────
|
||
//
|
||
// The app was using 35 distinct durations. No shared vocabulary means every
|
||
// transition is a fresh guess, and some of the guesses sat directly in the
|
||
// rider's path between two stops — an 800ms entrance and several 500ms
|
||
// cross-fades on screens he passes through 40 times a shift.
|
||
//
|
||
// Motion on a driver app is a tax paid all day. It earns its place only when
|
||
// it explains a state change; anything else is time spent standing still.
|
||
|
||
/// Press and release. Fast enough to feel like the surface responded rather
|
||
/// than animated.
|
||
static const Duration motionPress = Duration(milliseconds: 120);
|
||
|
||
/// A state change in place: a chip selecting, a tick landing, a row opening.
|
||
static const Duration motionState = Duration(milliseconds: 200);
|
||
|
||
/// A page or a sheet arriving. The ceiling for anything on the critical path.
|
||
static const Duration motionPage = Duration(milliseconds: 300);
|
||
|
||
/// Celebration, and only celebration — once per completed route, never on a
|
||
/// screen the rider is trying to get through.
|
||
static const Duration motionCelebrate = Duration(milliseconds: 600);
|
||
|
||
// ── Border radius ─────────────────────────────────────────────────────
|
||
//
|
||
// ── Three, because 26 communicated nothing ──
|
||
//
|
||
// An audit counted 26 distinct radii in use against the four declared here,
|
||
// and found the most common value in the app was 14 — `ButtonSizes.radius`,
|
||
// a *button* token, being applied to cards and sheets. Radius is one of the
|
||
// strongest cues that two things belong to the same family; at 26 values it
|
||
// says nothing, and two elements side by side at 12 and 14 read as a
|
||
// rendering bug rather than a decision.
|
||
//
|
||
// `radius2xl` (20) is retired: it was the third-most-used value and had no
|
||
// job the container radius could not do.
|
||
|
||
/// **Inner** — inputs, chips, small tiles, panels *inside* a container.
|
||
static const double radiusLg = 12.0;
|
||
|
||
/// **Container** — cards, sheets, and anything that is a surface in its own
|
||
/// right. Buttons use this too, so a button and the card it sits on agree.
|
||
static const double radiusXl = 16.0;
|
||
|
||
/// **Pill** — fully rounded: pills, circular tracks, avatars.
|
||
static const double radiusFull = 999.0;
|
||
|
||
// ── Elevation ─────────────────────────────────────────────────────────
|
||
//
|
||
// Only two survive, and both are used sparingly. Most cards in the app are
|
||
// now borderless fills with no shadow at all — see
|
||
// `ColorConstants.cardSurface` — so elevation is the exception, not the
|
||
// default it once was.
|
||
|
||
/// A card lifted just off the page.
|
||
static const List<BoxShadow> shadowSm = [
|
||
BoxShadow(color: Color(0x1A000000), blurRadius: 4, offset: Offset(0, 2)),
|
||
];
|
||
|
||
/// Something floating over content: a menu, a dragged element.
|
||
static const List<BoxShadow> shadowLg = [
|
||
BoxShadow(color: Color(0x2A000000), blurRadius: 12, offset: Offset(0, 8)),
|
||
];
|
||
|
||
/// A surface that floats **over** the list rather than in it: the selection
|
||
/// bar, a snackbar. Wider and deeper than [shadowGlass] because it has to
|
||
/// read as being on a different plane from the cards it covers — but still
|
||
/// slate-tinted and still soft, because a hard drop shadow under a white bar
|
||
/// reads as a grey stripe, which is exactly what it looked like.
|
||
static const List<BoxShadow> shadowFloat = [
|
||
BoxShadow(color: Color(0x1F0F172A), blurRadius: 28, offset: Offset(0, 12)),
|
||
BoxShadow(color: Color(0x140F172A), blurRadius: 4, offset: Offset(0, 2)),
|
||
];
|
||
|
||
/// ── What holds a borderless card off the page ──
|
||
///
|
||
/// Two shadows, not one, and that is the whole trick: a wide soft pool gives
|
||
/// the card its lift, and a tight near-opaque line under the bottom edge
|
||
/// gives it a *defined* edge — without it a single blurred shadow reads as a
|
||
/// smudge and the card looks unfinished rather than raised.
|
||
///
|
||
/// Tinted with the page's slate rather than pure black: a neutral-black
|
||
/// shadow on a blue-grey ground goes muddy, which is what makes a card look
|
||
/// cheap next to the same card in a design file.
|
||
static const List<BoxShadow> shadowGlass = [
|
||
BoxShadow(color: Color(0x0F0F172A), blurRadius: 18, offset: Offset(0, 6)),
|
||
BoxShadow(color: Color(0x0A0F172A), blurRadius: 2, offset: Offset(0, 1)),
|
||
];
|
||
}
|
||
|
||
/// ─────────────────────────────────────────────────────────────────────────
|
||
/// BUTTON SIZING — one scale for the whole app.
|
||
///
|
||
/// Buttons had drifted to a dozen different heights (44, 48, 52, 54, 56, 58)
|
||
/// across the pickup flow, Home, auth and the sheets. On a screen the rider
|
||
/// uses 30–50 times a day that reads as sloppiness, and worse, it breaks the
|
||
/// muscle memory that lets him hit a control without looking: if "the green
|
||
/// one at the bottom" is a different size on every screen, he has to aim.
|
||
///
|
||
/// Three sizes, and only three. Every button in the app must pick one.
|
||
/// ─────────────────────────────────────────────────────────────────────────
|
||
class ButtonSizes {
|
||
ButtonSizes._();
|
||
|
||
/// **Primary** — the one action a screen exists for. Full-width CTAs:
|
||
/// Accept Trip, Start Pickup, Confirm, Navigate.
|
||
static const double primary = 56.0;
|
||
|
||
/// **Secondary** — supporting actions that sit beside or below a primary:
|
||
/// Reject, Skip, Details, filter chips with a tap target.
|
||
static const double secondary = 48.0;
|
||
|
||
/// **Compact** — dense rows where several actions share a line, e.g. the
|
||
/// per-stop Accept/Reject pair inside a trip card. Still above the 44dp
|
||
/// accessibility floor.
|
||
static const double compact = 44.0;
|
||
|
||
/// Square icon-only buttons (call, overflow) match [primary] so they line up
|
||
/// with the CTA they sit next to.
|
||
static const double icon = 56.0;
|
||
|
||
/// Corner radius, shared by every button size so they read as one family.
|
||
///
|
||
/// 14 → 16 so it matches [DesignConstants.radiusXl]. At 14 a button sitting
|
||
/// on a 16pt card had a visibly tighter corner than the card holding it,
|
||
/// which is the kind of two-point difference nobody can name and everybody
|
||
/// sees.
|
||
static const double radius = DesignConstants.radiusXl;
|
||
|
||
/// Absolute floor for any tappable target, per accessibility guidance.
|
||
static const double minTapTarget = 44.0;
|
||
}
|
||
|
||
/// ─────────────────────────────────────────────────────────────────────────
|
||
/// DESIGN TRIAL — full addresses on stop cards
|
||
///
|
||
/// Turn on to see every stop card carrying its **complete** address rather than
|
||
/// the trimmed one, over [kFullAddressMaxLines] lines instead of two.
|
||
///
|
||
/// Two separate things are suppressed normally, and both are lifted together
|
||
/// here or the trial would not show what it claims to:
|
||
///
|
||
/// • **Home shortens the text.** `Trip.shortAddress` strips the tail every stop
|
||
/// on the route shares — the city, the state, the pincode — because repeating
|
||
/// "Coimbatore, Tamil Nadu 641006" on all six stops tells the rider nothing
|
||
/// about which one he is looking at. With the trial on, the raw address is
|
||
/// used instead. Bookings already shows the raw value.
|
||
///
|
||
/// • **Both screens cap the lines.** Two, so a card's height does not depend on
|
||
/// how verbose a customer was — a list of cards that step unevenly down the
|
||
/// screen is harder to scan and fits fewer stops.
|
||
///
|
||
/// Both caps exist for measured reasons, so this is a switch rather than a
|
||
/// deletion. **Set to `false` to restore the shipped behaviour.**
|
||
const bool kFullAddressTrial = true;
|
||
|
||
/// Line cap while [kFullAddressTrial] is on. Four fits a full Indian address at
|
||
/// 13sp in a stop card's column without letting one pathological record run the
|
||
/// card off the screen.
|
||
const int kFullAddressMaxLines = 4;
|