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:
330
BOTTOM_SHEETS.md
Normal file
330
BOTTOM_SHEETS.md
Normal 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.**
|
||||
Reference in New Issue
Block a user