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

330
BOTTOM_SHEETS.md Normal file
View File

@@ -0,0 +1,330 @@
# Miler — Bottom Sheets
Every modal surface that rises from the bottom of the Miler rider app: what it
is, when it appears, what it looks like, and the rules it must obey.
**Scope.** `lib/` only. `lib/xpress` is the ported Xpress-rider lane and keeps
its own surfaces; it is not covered here and must not be used as a reference.
---
## 1. The one presenter
Every sheet in this app is opened through **`showMilerSheet()`**
(`lib/views/helpers/widgets/miler_sheet_kit.dart`). No screen calls
`showModalBottomSheet` and configures a route itself.
```dart
showMilerSheet<T>(
context,
builder: (ctx) => MilerSheetScaffold(child: …),
large: false, // slower entrance for sheets covering most of the screen
isDismissible: true,
enableDrag: true,
);
```
It fixes four things so no caller can get them wrong:
| Setting | Value | Why |
|---|---|---|
| `backgroundColor` | `transparent` | The scaffold paints the glass. An opaque route background shows as a square white corner behind the rounded one. |
| `isScrollControlled` | `true`, always | A sheet that cannot grow past half height is a sheet whose text field the keyboard covers. |
| `sheetAnimationStyle` | `kMilerSheetStyle` / `kMilerLargeSheetStyle` | One entrance curve, two durations. |
| Shape | Owned by `milerGlassSheet` | No caller re-declares a radius. |
### What this replaced
An audit found **six** presentation styles for one kind of surface: frosted
glass at radius 28; opaque `pureSurface` at 24; default white at 16 with **no**
`isScrollControlled` (so the keyboard covered the reason field); an ad-hoc
`Container` with its own shadow and a 50pt top margin; `Get.bottomSheet` flat at
16; and a floating card with margins. Five of them drew their own drag handle —
four at 40×4 in `borderSubtle`, which on frosted glass is close to invisible —
and each applied `SafeArea` its own way.
A rider crossing one shift met most of these. That is what makes an app feel
assembled rather than designed: the same gesture arriving on different-looking
surfaces.
---
## 2. The structure inside
**`MilerSheetScaffold`** owns the fabric, the handle, the content padding and
the bottom inset. A sheet author writes content and nothing else.
```
┌───────────────────────────────────┐
│ ▬▬▬▬ │ MilerSheetHandle — 40×4, borderStrong
│ │ 10 top / 14 bottom margin
│ ◐ Title │ MilerSheetHeader — 18sp/w700
│ One supporting line. │ subtitle 13.5sp/w600, badge tinted 21sp
│ │
│ ───────────────────────────── │ content — the sheet's own
│ │
│ [ Primary action ] │
└───────────────────────────────────┘
20dp gutter, matching the page beneath
```
### The inset rule — one line, and it has bitten this app before
```dart
bottomInset = max(viewInsets.bottom, viewPadding.bottom)
```
The keyboard **replaces** the gesture bar; whichever is up pays the inset.
**Summing them is the double-SafeArea bug** — it floats the CTA a gesture bar's
height above the keyboard. Never add a `SafeArea` inside a sheet.
The padding is animated (`DesignConstants.motionState`, `easeOutCubic`) because
a sheet that jumps its full keyboard height in one frame reads as a glitch
rather than a response.
### The handle
`borderStrong`, **not** `borderSubtle`. At 7% glass over a dimmed page, subtle
grey measures under 1.15:1 against its own ground — so the sheets most worth
flicking away looked least like they could be.
Hide it (`handle: false`) only for a sheet that must not be casually dismissed.
### The header badge
Optional, **tinted, never filled** — a filled disc at this size outweighs the
title it introduces. Pass the accent the subject already wears elsewhere (duty
green, brand red, a stop-kind accent) so the sheet is visibly about the thing
that opened it.
### `MilerSheetChoiceRow`
The standard row for a list of answers: label, optional icon, accent, selected
state, optional rule beneath. Label 14.5sp. Used wherever a sheet asks the rider
to pick one of several things.
---
## 3. The sheets
### Home
#### Duty — `duty_sheet.dart`
**Opens:** tapping the duty control in the app bar.
**Asks:** `Go on duty?` / `Go off duty?`
**Body:** one sentence of consequence — on duty, the hub can add you to the next
slot and live tracking runs; off duty, the hub stops assigning and tracking
pauses, and any booking you are holding has to be finished first.
**Commits with:** a button.
> **Why not a slide.** This had accumulated a handle, an icon disc, a title, a
> subtitle, a *drawn diagram of the switch moving*, three icon-and-text
> consequence rows and a slide-to-confirm — eight blocks for a question with two
> answers. The slide was borrowed from the payment flows, and this app's rule is
> that **slides are reserved for money and for custody**. Ending a shift is
> neither.
#### Stop detail — `stop_detail_sheet.dart`
**Opens:** tapping a stop on the trip card, or a queue row on Deliveries.
**Type:** `DraggableScrollableSheet` — `0.78` initial, `0.5` min, `0.95` max.
**Answers:** *where actually is this* (a small non-interactive map with the hub,
the stop and the leg between them) and *what exactly is here* (full address,
contact, parcel counts, cash, notes, OTP).
**Layout:** number chip + customer + status → map → `620 m away · 1 min ride` →
`DELIVER TO` + address → `WHAT TO DO HERE` + the task and its requirement →
a two-up **fact grid** (From / Bag / Order / Contact) on one white surface cut
by hairlines → Call and Navigate.
> The fact grid was a `label ── value` table with a fixed 110pt label column. The
> values — the half a rider needs — sat in a ragged right column and a long one
> wrapped under a wide empty label, putting a hole in the middle of the block.
> Stacked two to a row, the whole set is scanned in a couple of saccades.
This sheet is **why the card above it can afford to truncate**. The list is for
scanning; everything it drops lives here, one tap away.
#### Pickup preview — `pickup_preview_sheet.dart`
**Opens:** the `i` / details control on a kitchen, and the kitchen card's own tap.
**Answers:** *where am I going, and what am I collecting there* — the leg drawn
from where the rider is standing, plus the load waiting at the counter.
**Primary action:** `Start navigation` — and **only then** does Miler hand off to
Google Maps.
> Handing off means the rider leaves Miler: his route, bag counts and manifest
> all go behind another app. Committing to that on the strength of a kitchen's
> name and a straight-line distance is one tap too few.
#### Rung sheet — `stop_action_sheet.dart`
**Opens:** advancing a stop from Home.
**Asks:** `Arrived at Vidhya Kitchen` / the collection equivalent, with the count
of orders at that counter and the **named manifest**.
**Commits with:** `Slide to confirm arrival` / `Slide to confirm pickup`.
> Arriving and collecting are claims about the physical world — *I am at this
> counter*, *this bag is in my box*. They are written to the hub, they move food
> and money, and a stray thumb on a moving bike must not be able to make them.
#### Reject reasons — `homepage.dart`
**Opens:** rejecting one or more assigned stops.
**Asks:** `Why are you rejecting this stop?` (or `…these N stops?`)
**Subtitle:** *The hub needs a reason to re-route it to someone else.*
**Body:** a `MilerSheetChoiceRow` list — too far from my route; shop/customer
closed today; parcel too large for my box; not enough time left in my shift;
address looks wrong.
#### Turn on location — `homepage.dart`
**Opens:** when a permission or a fix is missing.
**Asks:** `Turn on location`
**Subtitle:** *Bookings and live tracking need your position. Nothing is assigned
to a rider the hub cannot see.*
#### Product details — `homepage.dart`
**Opens:** from an order row.
**Title:** `Product details`, with an inventory badge.
**Body:** a plain ruled list of whatever the payload actually carries. An empty
payload is **one quiet sentence**, not a padded red box.
---
### Deliveries and the live map
#### Live stop sheet — `map.dart`
**Type:** `DraggableScrollableSheet`, and the **only** persistent (non-modal)
sheet in the app. Three snap points, no more:
| Name | Extent | Purpose |
|---|---|---|
| Glance | `0.35` | Map-dominant; ETA still readable |
| **Working** | **`0.45`** | Default. ETA, identity, address and the action, without scrolling |
| Detail | `0.75` | Everything, map still legible |
**Hierarchy — how long → where → who → what action:**
```
▬▬▬▬
6 min ← the dominant operational figure
1.8 km away · arrive ~10:42
───────────────────────── ← one hairline, the sheet's only rule
Joe Mathew ☎ ← identity + Call
12 SNS Colony, Peelamedu
Bag 8
[ Navigate ] [ I've arrived ] pickup leg
⟶ Slide to finish stop delivery leg
```
One continuous surface. **No cards inside it** — the hierarchy is typography,
whitespace and that single hairline.
> The camera's bottom padding is derived from `_sheetWorking` and the real safe
> areas, not from a remembered pixel value. A hardcoded `300.h` agreed with 45%
> only on the device it was measured on: on a 320×568 phone it framed the route
> into a sliver, and at large text the stop pin ended up under the sheet.
**Call** uses `StopContact.forLeg()` — the kitchen's number on a pickup leg, the
customer's on a delivery leg, and a label that says which. It is omitted
entirely when there is no number rather than offered dead.
#### How did this stop end? — `map.dart`
**Opens:** *only* after the rider completes the slide on the live sheet. **The
slide itself commits nothing.**
**Title:** `How did this stop end?`
**Subtitle:** *This is recorded with the office straight away.*
**Choices — deliberately unequal:**
| Choice | Weight |
|---|---|
| **Delivered** | Filled primary — the forward/success path |
| Skipped | Warning/amber — an exception |
| Cancelled | Destructive |
The slider carries `Semantics(button, label: 'Finish this stop', hint: 'Slide,
or double tap, to choose how the stop ended', onTap: …)` — a drag-only control
is unusable with a screen reader, and with gloves on a cold morning. The
accessible path opens the **same** question, so deliberateness is preserved.
#### Skip — `skip_sheet.dart`
**Opens:** choosing Skip.
**Body:** the reason list — customer unreachable; customer not at the location;
wrong address; access problem; customer refused; delivery paused; quantity
issue; other. Renders through `MilerSheetChoiceRow`.
**Guard:** at the limit it shows `Skip Limit Reached` — *You have exceeded the
limit of 2 skips within 3 hours.*
#### Stop brief — `sheet.dart` (`_StopBrief`)
Not a sheet itself: the shared **identity block** used by the map sheet, Update
Status and Skip, so all three name the stop the same way. Before it, the skip
sheet asked "Skip this pickup" without ever saying *which* pickup.
---
### Bookings / orders
#### Reject booking — `orderstaus_button.dart`
**Title:** `Reject booking` · **Subtitle:** *The hub re-routes it once it knows why.*
#### Cancel this booking? — `orderstaus_button.dart`
**Title:** `Cancel this booking?` · **Subtitle:** *It returns to pending and the
hub reassigns it.*
#### Payment confirm — `collect_payment.dart`
A confirm sheet before money is taken. **Slide**, per the money rule.
---
## 4. Rules for a new sheet
1. **Open it with `showMilerSheet`.** Never configure `showModalBottomSheet`
yourself.
2. **Wrap the content in `MilerSheetScaffold`.** It is the handle, the gutter and
the inset.
3. **No `SafeArea` inside a sheet.** The scaffold already paid the inset. Adding
one is the double-inset bug.
4. **No card inside a sheet.** The sheet *is* the surface. A bordered white panel
on frosted white is a line the rider reads past to reach what he came for.
5. **One header idiom** — `MilerSheetHeader`, badge tinted not filled.
6. **No close button.** The handle and the barrier dismiss. An `X` duplicates
both, and this app had exactly one sheet with one.
7. **Slide is reserved** for custody and money — arrival, collection, delivery
outcome, payment. Everything else commits with a button.
8. **A drag-only control needs `Semantics` with an `onTap` alternative** that
reaches the same confirmation.
9. **Unequal choices look unequal.** Forward path filled, exception amber,
destructive distinct. Never three identical buttons.
10. **Absent, not empty.** A row with no value is not drawn. A group with nothing
in it is not offered.
---
## 5. Known gaps
**Every modal sheet in `lib/` now goes through `showMilerSheet` +
`MilerSheetScaffold`.** The four direct callers listed in the first version of
this document have been migrated:
| File | Was | Now |
|---|---|---|
| `skip_sheet.dart` | own `SafeArea` **wrapping** a hand-rolled glass + gutter + handle, plus a ✕ | kit presenter + scaffold + `MilerSheetHeader` |
| `collect_payment.dart` | right route flags written by hand | kit presenter (content already used the scaffold) |
| `multi_map.dart` | own radius 20, **no glass**, `SafeArea` **plus** `viewInsets` padding, 24 gutter, 22sp red title, ✕ | kit presenter + scaffold + header |
| `map.dart` `_PickupBottomSheet` | a `shape: …circular(24)` that clipped nothing behind a transparent route | kit presenter |
### What remains
- **`auth.dart` uses `Get.bottomSheet`** for its eight error notices. It is a
controller with no `BuildContext`, which is why it was left; migrating it
means giving the controller a navigator key or moving the notices to the
screens. Until then, auth errors are the one surface that does not speak the
system's language.
- **The empty-state artwork cannot be verified in a test.** `Image.asset`
resolves against a bundle the test binding does not carry, so no finder sees
it. The Activity empty state is asserted by its remaining control instead;
the picture itself needs a device check.
- **The live map sheet has not been visually QA'd** across the device and
text-scale matrix. `_PickupMapScreen` is private and needs Get,
SharedPreferences, Geolocator and a live map to pump; that harness does not
exist yet.
- **`milerGlassSheet`'s blur has not been profiled** on a mid-range Android with
the route animating. Readability and GPU cost outrank glassmorphism on an
outdoor screen; if it measures badly, a high-opacity surface is the answer.
**No profiling was run, so no blur change was made.**