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

202
DESIGN_SYSTEM.md Normal file
View File

@@ -0,0 +1,202 @@
# Miler — Surface & Elevation System
Why the app looked flat, what replaced it, and the rules that keep it fixed.
**Scope.** `lib/` only. `lib/xpress` is the ported Xpress-rider lane and is not
part of this system.
---
## 1. The diagnosis, with numbers
The app did not look flat because it lacked decoration. It looked flat because
**the ground did nothing**, and every screen compensated privately.
Measured against the page ground:
| Token | Hex | Contrast vs canvas | Verdict |
|---|---|---|---|
| `neutralLight` | `#F2F4F8` | **1.021 : 1** | indistinguishable |
| `cardSurface` | `#EDEFF4` | **1.023 : 1** | indistinguishable |
| `surface` | `#FCF9F8` | **1.073 : 1** | invisible, and *warm* unlike everything else |
| `pureSurface` | `#FFFFFF` | **1.124 : 1** | the one that mattered — and too close |
A tonal difference does not begin to read as a *different surface* until roughly
**1.25 : 1**, and on the budget LCD this app ships to it needs the top of that
band. So a white card on a white page was not a card; it was a rectangle you had
to be told about. Every screen that wanted its content to separate therefore grew
its own border or its own shadow — and once one screen does that, they all do,
each in its own way. That is the mechanism that turns one product into a
collection of independently-built Flutter screens.
Five near-identical light tokens were also five names for about two decisions.
### The second half of the diagnosis
`MilerSheet` — the shared body under Home, Deliveries and Activity — painted
**white**. The `daylightSurface` canvas token existed but only two pages used it.
So the app's three main screens were a white page carrying white surfaces: a
1.0 : 1 step, which is not a step at all.
---
## 2. The fix: four layers
```
0 canvas the page itself. Never white.
1 working a region the rider reads or works in. White, no border.
2 raised something that acts. Same fill, plus lift.
3 floating over everything, with a scrim: sheets, dialogs.
```
Declared once in `lib/views/helpers/constants/miler_surface.dart` as
`MilerSurface`. Read that ladder when choosing a ground — not the raw colour
constants, which is what let five of them drift into meaning the same thing.
### Canvas — `#EEF2F7` → `#DEE3EA`
White now separates at **1.290 : 1** instead of 1.124. Surfaces separate on
their own, so the borders and shadows screens grew to compensate can come off
rather than being piled higher.
Nothing was traded for it: body text still clears **13.8 : 1**, and every
operational ink clears AA body on both grounds (see §4).
### Which screen stands on which rung
The rung is chosen by **what the screen actually draws**, not by preference:
| Screen | Ground | Because |
|---|---|---|
| Home | working (white) | rows on a spine — **no cards by design** |
| Activity | working (white) | same — a ruled record, not a stack of objects |
| Deliveries | **canvas** | its content genuinely *is* raised objects: the NOW card is a layer-2 command surface and needs a ground to sit on |
| Record / details | canvas | one white sheet plus folded groups |
| All modal sheets | floating | scrim + glass |
> **The trap, recorded because it was walked into.** Painting Home with the
> canvas produced a uniformly grey screen — it has nothing white on it to
> separate *from*, so the same flatness came back in a different colour. A
> darker ground only helps where something white actually stands on it.
---
## 3. Elevation
| Layer | Fill | Lift |
|---|---|---|
| working | `pureSurface` | none — the canvas step does the work |
| raised | `pureSurface` | `MilerSurface.raisedShadow` (`shadowSm`) |
| floating | `glassCard` | `MilerSurface.floatingShadow` + **scrim** |
`MilerSurface.scrim` (`0x66000000`) replaced Flutter's default `black54` on
every sheet. **The scrim is the separation; the shadow is not.** A sheet that
needs a heavier shadow to be legible is a sheet whose scrim is too weak — the
scrim is the knob.
### The one nesting the system allows
`MilerSurface.inset` — a quiet tonal block *inside* layer 1, for a fact grid or
a folded group. Only when the block is a different **kind** of thing from what
surrounds it. **Never for grouping**: grouping is whitespace, type and the
timeline, which is why a route reads as a route without a box around each stop.
---
## 4. Colour corrections that fell out of the canvas move
Two state inks landed in "AA large only" on the deeper ground and were darkened
by ~2% — the hue is indistinguishable at this distance, the arithmetic is not:
| Token | Was | Now | On canvas | On white |
|---|---|---|---|---|
| `acceptGreen` | `#047857` | `#047354` | 4.25 → **4.54** | 5.86 |
| `warning` | `#AE5008` | `#A44B08` | 4.12 → **4.54** | 5.85 |
Every operational ink now clears **AA body (4.5:1)** on both grounds. Pinned in
`test/surface_system_test.dart` — a contrast rule that lives only in a comment
is a rule the next hex nudge undoes.
### What the colours mean
| Colour | Means |
|---|---|
| Brand red `#960019` | identity, navigation, primary action — **not** error |
| Green | confirmed success, completion |
| Amber | genuine attention/exception only — **never** normal pending |
| Error red | failure, and semantically distinct from brand red |
| Neutral secondary | pending, waiting, metadata |
> Pending was amber once. On a fresh morning every kitchen is pending, so the
> screen opened in warning and left nothing to say with when a stop actually
> went wrong.
---
## 5. Rules
1. **Choose a ground from the ladder**, not from the colour constants.
2. **A container is justified when it is a layer** — a modal sheet, a floating
command bar, a grouped operational workspace. Not because the content inside
it is a group.
3. **No card inside a card**, and no white card on a white surface.
4. **If a surface needs a border to be seen, the ground is wrong** — fix the
ground, not the surface.
5. **Never introduce a one-off colour, radius, shadow or spacing number in a
page.** Five near-duplicate tokens is how this started.
6. **Measure.** Any new surface pair needs ≥1.25 : 1 to read as two surfaces,
and any ink needs ≥4.5 : 1 on the darkest ground it can land on.
---
## 6. The second pass: the ladder made visible
The foundation pass fixed the ground; a device screenshot showed the result
still *read* flat — the silhouette of every page was unchanged. The second
pass gave the system its visible vocabulary, applied identically on Home,
Deliveries, Activity, Account and the sheets:
- **Hero heads.** A screen's key figure is a numeral (28sp w800, tabular),
captioned small — never a sentence in body type. Home: `15 · stops left`
with a 6pt determinate progress bar (canvas track, brand fill, green when
done) and `9 of 24 done · ≈4h · 38.9 km` under it. Activity: `4 · delivered
today`. Figures clamp at 1.3× text scale; captions ellipsise, numerals never.
- **Wells.** Anything that opens — Home's kitchens, Activity's trips — opens
into a canvas-toned inset (`radiusLg`, small padding). The well is the
ladder's one permitted nesting, and it is the *canvas* tone because that is
the only grey that stands on white (1.290:1).
- **Tags.** A label a rider matches against the physical world (`Bag 3`) is a
small filled tag: white on a well or tinted panel, canvas on white, brand
10% when selected, error 8% for `Missing`. Same shape everywhere — Home
rows, the pickup sheet, the confirm sheet, the Deliveries queue.
- **Placeholders recede.** An unassigned trip tab drops to `disabledFill` and
w600 and says `None yet`; empty slots never compete with real work.
- **Account joined the ladder**: canvas ground, the identity + day-figures as
one panel, each settings group a panel, labels on the canvas between them —
scoped to the Account tab so the eight other `settings_ui` screens are
untouched.
- **`neutralLight` is not a fill on white.** It measures 1.02:1 there; every
such use found on these pages now takes the canvas tone. The token keeps
its other jobs.
Pinned by: `home_gutter_test` (hero and route share an edge),
`stop_row_alignment_test` (the well's indent is bounded),
`home_structure_test` (one well, tags under 120pt, no borders or shadows in a
group), `activity_list_test` (a sheet is `working` or `canvas`, never the
warm off-white).
## 7. Known gaps
This was a foundation pass. It is not a completed product-wide redesign.
- **`GlassCard` still carries three separation mechanisms** — fill, border and
shadow. Its rim is documented as a fix for shadows crushing on a budget LCD,
reported twice from a device, so it was left alone rather than changed on a
guess. It should be revisited *on a device* now that the ground works.
- **No typography or spacing consolidation.** Arbitrary `.sp` values still exist
across pages; `MilerType` was not rationalised into a smaller scale.
- **Icon families are still mixed.** Lucide was adopted for Activity and the
status vocabulary; Material rounded remains widespread elsewhere.
- **Account/Profile was not audited or migrated.**
- **No device or text-scale QA** of the new ground, and **no GPU profiling** of
`milerGlassSheet`'s blur — so no blur change was made.