Files
doormile_milderapp/BOTTOM_SHEETS.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

331 lines
15 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 — 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.**