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>
203 lines
9.1 KiB
Markdown
203 lines
9.1 KiB
Markdown
# 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.
|