Files
doormile_milderapp/DESIGN_SYSTEM.md
Thiru-tenext d7348e253f 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>
2026-08-22 05:40:35 +05:30

203 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.