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>
331 lines
15 KiB
Markdown
331 lines
15 KiB
Markdown
# 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.**
|