Book a pickup, track it to delivery — the rebuilt customer app. - Design language from doormile-screens.html: brand #8F0F06, Manrope + Geist Mono (variable fonts), bordered cards instead of shadows, crimson brand headers, sliding tab indicator, mono for anything read digit by digit. - lib/data (one live API implementation, plus a debug-only offline fake), lib/state, lib/ui (tokens, widgets, screens). - 84 tests, plus a design snapshot harness that renders every screen with the real fonts: flutter test test/design_snapshot_test.dart --run-skipped --update-goldens This replaces the previous app (pubspec 'doormile', app id com.doormile.customer). That tree remains in history at 6c7d656; note its android/app/google-services.json is not carried over, and the application id here is in.doormile.customer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
472 lines
22 KiB
Markdown
472 lines
22 KiB
Markdown
# Doormile Customer App — Design Documentation
|
||
|
||
How every screen is designed, why it is designed that way, and where each
|
||
decision lives in the code.
|
||
|
||
Companion to `README.md` (which covers running and shipping the app).
|
||
|
||
---
|
||
|
||
## 1. Principles
|
||
|
||
Five rules decide almost every argument in this app.
|
||
|
||
1. **One decision per screen.** A screen asks one thing and offers one dominant
|
||
action. Everything else is secondary or absent.
|
||
2. **Lift, don't outline.** Surfaces separate through soft shadow on a warm
|
||
canvas, not through borders. Borders appear only where an outline is
|
||
genuinely load-bearing.
|
||
3. **Optional must look optional.** Anything the customer can skip is labelled
|
||
*Optional* and, when skipped, described calmly — never with warning styling.
|
||
4. **Say it in customer language.** No consignment, waybill, AWB, MPS or piece.
|
||
The customer books a pickup and sends packages.
|
||
5. **Complexity appears only when asked for.** Multi-destination, multi-package
|
||
and every advanced field stay folded away until someone reaches for them.
|
||
|
||
---
|
||
|
||
## 2. Foundations
|
||
|
||
All of this lives in `lib/ui/tokens.dart`. No screen hardcodes a value.
|
||
|
||
### 2.1 Colour
|
||
|
||
One accent. Warm neutrals. Semantic colour used sparingly and only with meaning.
|
||
|
||
| Token | Hex | Used for |
|
||
| --- | --- | --- |
|
||
| `brand` | `#960019` | Primary actions, selection, live status, links |
|
||
| `brandPress` | `#7A0015` | Pressed state of a primary surface |
|
||
| `brandSoft` | `#FDF2F4` | Selected tiles, tinted glyph circles, brand chips |
|
||
| `brandLine` | `#F3D9DF` | Pressed state of a soft brand surface |
|
||
| `ink` | `#14100F` | Primary text — a warm near-black, never `#000` |
|
||
| `ink2` | `#5A5350` | Secondary text, lede paragraphs |
|
||
| `ink3` | `#8B8480` | Tertiary text, eyebrows, captions |
|
||
| `ink4` | `#B5AEAA` | Placeholders, disabled labels, chevrons |
|
||
| `canvas` | `#F7F5F3` | Page background — warm off-white |
|
||
| `surface` | `#FFFFFF` | Cards, sheets, the tab bar |
|
||
| `surfaceAlt` | `#F2EFEC` | Filled inputs, quiet chips, skeleton base |
|
||
| `hairline` | `#EFEBE8` | Dividers inside a shared surface |
|
||
| `border` | `#E3DEDA` | The rare real outline |
|
||
| `ok` / `okSoft` | `#0E6B46` / `#E9F4EF` | Delivered, money settled, confirmations |
|
||
| `danger` | `#B3231F` | Cancel and other destructive labels |
|
||
|
||
**Rules.** Green means completed or paid — never decoration. Red is either the
|
||
brand or destruction, and context always disambiguates. Disabled states desaturate
|
||
rather than grey out to a different hue.
|
||
|
||
### 2.2 Typography
|
||
|
||
**Inter**, bundled as a real asset in four weights (400/500/600/700) so the app
|
||
renders identically on every device. Headings carry tight negative tracking;
|
||
body text stays open.
|
||
|
||
| Style | Size / Height / Weight | Tracking | Used for |
|
||
| --- | --- | --- | --- |
|
||
| `display` | 32 / 1.14 / 700 | −1.1 | The one dominant line on a screen (tracking status, "Pickup booked") |
|
||
| `pageTitle` | 28 / 1.18 / 700 | −0.9 | Tab-root titles (Orders, Account) and the Home question |
|
||
| `title` | 26 / 1.2 / 700 | −0.8 | Step headings inside the booking flow |
|
||
| `heading` | 21 / 1.25 / 600 | −0.5 | Card headings, amounts |
|
||
| `sheetTitle` | 21 / 1.25 / 700 | −0.6 | Bottom-sheet titles |
|
||
| `cardTitle` | 17 / 1.3 / 600 | −0.35 | Card and row titles |
|
||
| `body` | 16 / 1.5 / 400 | −0.1 | Input text, long copy |
|
||
| `bodyStrong` | 16 / 1.4 / 600 | −0.25 | Emphasised row titles |
|
||
| `lede` | 15 / 1.5 / 400 | −0.1 | The supporting line under a heading |
|
||
| `value` | 15 / 1.4 / 500 | −0.15 | Data values in summary rows |
|
||
| `label` | 13 / 1.3 / 600 | −0.05 | Buttons-in-text, chips, inline actions |
|
||
| `small` | 13 / 1.45 / 400 | −0.05 | Captions, secondary metadata |
|
||
| `eyebrow` | 11 / 1.3 / 600 | **+0.85**, uppercase | Micro-labels: `PICKUP`, `SENDING TO`, `STEP 2 OF 4` |
|
||
| `sectionHead` | 15 / 1.3 / 600 | −0.2 | Sentence-case section headings ("Recent orders") |
|
||
|
||
Two label systems on purpose: **uppercase eyebrows** name a *field*, **sentence-case
|
||
section heads** name a *group*. Mixing them was the fastest way to make the first
|
||
version look like a wireframe.
|
||
|
||
### 2.3 Shape
|
||
|
||
`xs 10 · sm 14 · md 18 · lg 22 · xl 28 · pill 999`
|
||
|
||
Tightest on inputs (14), mid on buttons and tiles (18), wider on cards (22),
|
||
widest on sheets and the hero CTA (28). Radius increases with the size of the
|
||
surface, which keeps corner curvature visually constant.
|
||
|
||
### 2.4 Elevation
|
||
|
||
Four shadows, each with one job.
|
||
|
||
- **`soft`** — resting elevation for every card. Two layers: a 2px contact
|
||
shadow plus a 24px ambient one at −12 spread. The contact layer is what stops
|
||
cards floating.
|
||
- **`lift`** — brand-tinted, for the single hero CTA on Home.
|
||
- **`float`** — controls hovering over content (map badges, toast, locate button).
|
||
- **`sheet`** — upward shadow under a bottom sheet.
|
||
|
||
### 2.5 Spacing
|
||
|
||
- Page padding **20**, used everywhere so every screen shares a left edge.
|
||
- Minimum touch target **48**; primary button height **54**.
|
||
- Vertical rhythm in 4s: 4 / 8 / 12 / 16 / 20 / 26 / 28.
|
||
- Section gap 26–28, intra-card gap 12–16, label-to-field 8.
|
||
|
||
### 2.6 Motion
|
||
|
||
`fast 140ms · base 220ms · slow 340ms`, all on `easeOutCubic`.
|
||
|
||
Press feedback is a scale to 0.98–0.985 at `fast`. Selection and expansion run
|
||
at `base`. Progress rails and route drawing run at `slow`. Continuous animations
|
||
(radar, live dot, map pulse, skeleton shimmer) run on their own loops and are
|
||
the reason tests pump fixed slices instead of settling.
|
||
|
||
---
|
||
|
||
## 3. Component library
|
||
|
||
`lib/ui/widgets/` — screens are thin because these carry the design.
|
||
|
||
| Component | File | Role |
|
||
| --- | --- | --- |
|
||
| `DmButton` | `buttons.dart` | primary / soft / ghost / outline, busy state, optional trailing icon |
|
||
| `DmChipButton`, `DmTextAction`, `DmIconButton` | `buttons.dart` | secondary affordances |
|
||
| `DmTopBar`, `DmStepHeader`, `DmFooter`, `DmPageTitle`, `DmScreenHeader` | `chrome.dart` | screen scaffolding |
|
||
| `DmOptionTile`, `DmActionRow` | `option_tile.dart` | selectable list card, tappable action row |
|
||
| `DmTextField`, `DmSegmented`, `DmRow`, `DmRowGroup`, `DmSwitch` | `inputs.dart` | form controls |
|
||
| `DmSummaryCard`, `DmSummaryRow`, `DmNote`, `DmConfirmedStrip` | `summary.dart` | fact display |
|
||
| `DmAsyncList`, `DmSkeleton`, `DmEmptyState` | `states.dart` | every remote-data state in one wrapper |
|
||
| `DmPill`, `DmLiveDot`, `DmAvatar`, `DoormileMark` | `misc.dart` | status and identity |
|
||
| `DmTag`, `DmChoiceChip`, `DmGridTile`, `DmStepper`, `DmInfoBanner`, `DmCopyRow` | `pieces.dart` | small primitives |
|
||
| `DmRouteMap`, `DmRadarPulse` | `route_map.dart` | painted map, matching animation |
|
||
| `DmRouteRail` | `route_rail.dart` | pickup → drop, vertical |
|
||
| `DmMilestones` | `milestones.dart` | the journey checklist |
|
||
| `DmToast`, `showDmSheet`, `DmSheetHeader` | `feedback.dart` | transient and modal |
|
||
|
||
Four of these do disproportionate work:
|
||
|
||
**`DmAsyncList`** wraps every backend-driven list. Loading skeleton, empty state,
|
||
error-with-retry and the success builder all live in one place, so a new endpoint
|
||
inherits four designed states for free. It is why the empty and error screens
|
||
look intentional rather than bolted on.
|
||
|
||
**`DmRouteRail`** — an origin ring, a hairline connector, a brand pin. The same
|
||
component anchors an order card, the tracking facts and the receipt, so a journey
|
||
reads identically wherever it appears.
|
||
|
||
**`DmRouteMap`** — a `CustomPainter`: rotated city blocks, white roads, a cubic
|
||
route stroked with a white casing then brand fill, an origin ring, a destination
|
||
pin with a blurred drop shadow, and a pulsing live marker positioned along the
|
||
path via `PathMetrics`. No map SDK, no API key. Swap it for a real map widget and
|
||
nothing around it changes.
|
||
|
||
**`DmOptionTile`** — white card at rest with `soft` shadow; selected becomes
|
||
`brandSoft` with a 1.5px brand border and a filled check; disabled drops to
|
||
`surfaceAlt` and shows the backend's reason. One component covers choose,
|
||
chosen, and cannot-choose.
|
||
|
||
---
|
||
|
||
## 4. Screens
|
||
|
||
### 4.1 Auth — Login, Sign up, OTP
|
||
|
||
`lib/ui/screens/auth/`
|
||
|
||
**Design.** A maroon brand panel at the top, a white sheet rounded 30px over it
|
||
holding the form. This is the only full-brand moment in the app: it establishes
|
||
the identity once, then the product stays quiet. `AuthScaffold` owns the panel,
|
||
the sheet, the pinned footer and the legal line, so all three screens are
|
||
identical in structure and differ only in fields.
|
||
|
||
- **Login** — Phone/Email segmented, `+91` prefix field with a clear button, a
|
||
CTA that only enables on a valid input, and *New to Doormile? Create account*.
|
||
- **Sign up** — full name, phone, optional email. Validation is the same shape:
|
||
the CTA enables, it never scolds.
|
||
- **OTP** — its own screen, not an inline step, because it has its own back
|
||
semantics. The number is shown with a *Change* affordance, four large boxes
|
||
auto-advance, a pasted code spreads itself across them, a resend countdown
|
||
ticks from 0:30, and verification fires automatically on the fourth digit.
|
||
|
||
**Why a brand panel here and nowhere else:** auth is the one screen with no
|
||
content of its own, so it is the only place a large colour field costs nothing.
|
||
|
||
### 4.2 Shell and navigation
|
||
|
||
`shell_screen.dart` — Home · Orders · Account in an `IndexedStack`.
|
||
|
||
The bar **floats**: a white surface inset 16 from the edges, radius 28, lifted
|
||
by a two-layer shadow, so it sits on the canvas like every other surface rather
|
||
than sealing the bottom of the screen. The active tab is a `brandSoft` pill that
|
||
**slides** between destinations on `easeOutBack`, its icon switching from
|
||
outline to filled with a small upward nudge and its label easing to `w600` in
|
||
brand. Inactive icons and labels stay `ink3`.
|
||
|
||
Booking and tracking screens are **pushed over** the shell, so the tab bar never
|
||
appears where it would compete with a step flow.
|
||
|
||
### 4.3 Home
|
||
|
||
`home_screen.dart`
|
||
|
||
Three things, in priority order:
|
||
|
||
1. **Greeting + question** — "Good afternoon, Joe" in `lede`, then "Where should
|
||
we pick up?" in `pageTitle`. The screen asks the question the CTA answers.
|
||
2. **Book a Pickup** — the only maroon block in the app, radius 28, `lift`
|
||
shadow, title + one supporting line + an arrow in a translucent circle.
|
||
3. **What's happening** — an *Active* card (live dot, window tag, stage, who is
|
||
coming, Track →) and recent orders as route cards.
|
||
|
||
Pull-to-refresh re-detects location and refreshes orders. The pickup-address row
|
||
was removed once the pin moved into booking step 1 — Home should not carry a
|
||
setting.
|
||
|
||
### 4.4 Booking
|
||
|
||
Four steps, a shared `DmStepHeader` (`STEP n OF 4` on the left, the step name on
|
||
the right, an inset rounded progress rail beneath).
|
||
|
||
**Step 1 — Pickup point** (`pickup_location_screen.dart`)
|
||
Map panel with a pulsing pin and a locate button, the detected address in a card
|
||
with *Change* (opens the place-search sheet), and **Next**. Location is detected
|
||
first; correcting it is one tap; nothing is typed unless the customer wants to.
|
||
|
||
**Step 2 — Destination** (`destination_screen.dart`)
|
||
The screen that carries the multi-destination model without exposing it.
|
||
|
||
- Opens on a **state chip row** (horizontal, scrollable). Only states we can
|
||
actually serve appear — a state with nothing open is never offered. Choosing
|
||
one reveals a **two-column district grid** of serviceable districts only.
|
||
Unsupported places are never disabled tiles; they are named once, quietly:
|
||
*"Coming soon: Madurai"*. The delivery promise moves to the settled card,
|
||
where it informs a decision instead of decorating a picker.
|
||
- Choosing a district **collapses the block into a card**: the place, *How many
|
||
packages?* with a `− 1 +` stepper defaulting to 1, and *Add exact delivery
|
||
details · Optional*.
|
||
- Below it, a quiet **+ Send to another place**. That is the entire
|
||
multi-destination affordance — one text action, absent until the first place
|
||
is settled.
|
||
- Returning from Review shows settled cards, not the picker (`initState` opens
|
||
the first incomplete block, or none).
|
||
|
||
Caps come from `BookingLimits` (backend-served, 20 packages / 5 destinations).
|
||
When a cap is reached the action is replaced by a plain sentence — *"Up to 5
|
||
places in one pickup. Book another pickup for more."* — never an error.
|
||
|
||
**Step 3 — Pickup time** (`slot_screen.dart`)
|
||
The pickup address restated in a compact card with *Change*, then windows
|
||
grouped by day. Each slot card can carry badges (*Fastest pickup*, *4 Milers
|
||
nearby*) and a caption; full windows are shown disabled with their reason. The
|
||
slot belongs to the whole pickup — one visit collects everything.
|
||
|
||
**Step 4 — Review** (`review_screen.dart`)
|
||
Compact by design and identical in shape for one destination or five.
|
||
|
||
- Route map with a badge that adapts: *Direct door-to-door transit* or *One
|
||
pickup · 3 destinations*.
|
||
- Summary card: one **connected rail** — the pickup origin, then every
|
||
destination hanging off it with its package count, exactly as an order card
|
||
reads. `DmRouteRail` takes a list of stops for this. *Edit* sits on the
|
||
pickup row, *Change destinations* under the stops, and **Pickup time** is the
|
||
row beneath.
|
||
- Optional omissions are stated once, calmly: *"Not added — the Miler can
|
||
confirm this at pickup."* With several places it becomes a single line rather
|
||
than repeating.
|
||
- For multi: `6 packages · 3 destinations` · *One Miler visit*.
|
||
- Combined estimate with the honest caveat that the final amount settles after
|
||
the Miler verifies.
|
||
- One dominant CTA: **Book Pickup**.
|
||
|
||
**Confirmation** (`confirmed_screen.dart`)
|
||
A green check that pops in on `easeOutBack`, "Pickup booked", the package
|
||
summary as a brand tag, a map with a *Searching for the nearest Miler* badge,
|
||
the reference with a copy action, the scheduled window and destinations, and two
|
||
quiet informational banners. Then **Track pickup** / **Back to home**.
|
||
|
||
### 4.5 Tracking
|
||
|
||
`tracking_screen.dart` — one screen that changes shape by stage.
|
||
|
||
- **Status header.** Eyebrow (`PICKUP STATUS` / `ORDER STATUS`) with a status
|
||
pill, then the dominant line — personalised where it helps: *"Arun Kumar is on
|
||
the way"*. Below it one or two fact chips (ETA, window).
|
||
- **Hero.** While a Miler is being matched: a **radar pulse** with three
|
||
expanding rings. Once assigned: the **route map** with the live marker, badged
|
||
*En route · 1.4 km away*.
|
||
- **Miler card.** Avatar, name, rating and completed pickups, vehicle and type,
|
||
then **Call Miler** (soft) and **Message** (outline).
|
||
- **Milestones.** Seven customer stages, not nine backend ones: *Pickup booked
|
||
→ Miler assigned → Pickup in progress → Package collected → In transit → Out
|
||
for delivery → Delivered*. Operational detail — "Miler on the way", "Miler
|
||
arrived at your door", "Order created" — appears as one line of context under
|
||
the milestone in progress, never as another permanent row. Completed
|
||
milestones carry a filled check and a timestamp; the current one a ringed
|
||
marker and a **LIVE** tag.
|
||
|
||
Status pills everywhere use the same customer language
|
||
(`JourneyStage.milestoneLabel`), so a pill never reads "Order created".
|
||
- **Your packages.** Appears **only** after `orderCreated` and only when the
|
||
pickup carried several destinations — each with its own tracking ID, package
|
||
count and status pill. Before collection the customer has one scheduled visit,
|
||
not several shipments, and the UI says exactly that.
|
||
- **Facts.** Route rail plus pickup window and reference/tracking number.
|
||
- **Footer.** *Cancel pickup booking* while backend policy allows it, then
|
||
*Need help with this order?*, then *Book another pickup* once finished.
|
||
|
||
### 4.6 Orders
|
||
|
||
`orders_screen.dart`, `order_row.dart`
|
||
|
||
Page title with a live subtitle ("3 bookings in progress"), an Active /
|
||
Completed / Cancelled segmented control, and **route cards**: a compact
|
||
`DmRouteRail`, a hairline, then the identifier and its facts on the left and a
|
||
status pill on the right.
|
||
|
||
**Ownership changes at collection.** Before the Miler arrives, a booking is one
|
||
row — *"Pickup DM-482913 · 3 packages · 2 destinations"* — because the customer
|
||
has one scheduled visit. Once verification creates the orders, each destination
|
||
stands on its own row with its own tracking number and journey, carrying a quiet
|
||
line back to the visit they shared: *"Collected together · Pickup DM-471200"*.
|
||
`AppState.entriesOf` produces these rows; the card renders either shape.
|
||
|
||
Opening a destination row follows **that order** — tracking takes a
|
||
`focusedDestination` and shows its own status, milestones and rail, with the
|
||
sibling packages listed beneath. Live rows open Tracking; finished ones open the
|
||
receipt.
|
||
|
||
### 4.7 Order details (receipt)
|
||
|
||
`order_details_screen.dart`
|
||
|
||
For a finished order: status and delivered date, the full route rail, then
|
||
**Collected at your door** — what the Miler actually recorded at pickup:
|
||
the photographs they took (one per package, captioned "1 of 3"), the **weighed
|
||
weight**, and who weighed it and when, tagged *Miler verified*. This is the
|
||
customer's evidence of what was handed over, and the weight the final price was
|
||
calculated from.
|
||
|
||
Then a **Packages** breakdown when there was more than one destination, and a
|
||
**Payment** block itemising parcel, base fare, distance, weight adjustment,
|
||
verified weight, **total paid** and method. Then the facts (reference, per-destination tracking,
|
||
window, recipient, collected by) and the journey milestones.
|
||
|
||
This screen answers "what did it cost and when did it land" without making
|
||
anyone hunt.
|
||
|
||
### 4.8 Account
|
||
|
||
`account_screen.dart` — a profile card, then grouped rows: preferences, then
|
||
**Prototype controls** (simulate empty service areas / network error / slow
|
||
network, reset data). Sign out is a ghost danger button, and the build version
|
||
sits quietly beneath.
|
||
|
||
The prototype controls are deliberately in the product rather than hidden: they
|
||
are how anyone reviewing the app reaches the states that are otherwise hard to
|
||
produce.
|
||
|
||
### 4.9 Sheets
|
||
|
||
`showDmSheet` gives every sheet the same grabber, 28px top radius and padding.
|
||
|
||
- **Place search** — search field, debounced results, skeletons while loading.
|
||
- **Delivery details** — the optional address and recipient fields for one
|
||
destination. *Save details* / *Skip for now*.
|
||
- **Cancel booking** — the reference and window restated, optional reason chips,
|
||
then *Keep my pickup* (primary) and *Yes, cancel pickup* (ghost, danger). The
|
||
safe choice is the prominent one.
|
||
|
||
---
|
||
|
||
## 5. State design
|
||
|
||
Every remote list has four designed states, provided by `DmAsyncList`:
|
||
|
||
| State | Treatment |
|
||
| --- | --- |
|
||
| Loading | Shimmering skeleton rows at the real row height, so nothing jumps |
|
||
| Empty | 64px glyph circle, heading, calm explanation, optional soft action |
|
||
| Error | Wi-Fi-off glyph, *"Couldn't load this"*, the server's message, **Retry** |
|
||
| Unavailable item | Rendered, dimmed, with the backend's reason attached |
|
||
|
||
Disabled controls desaturate and stay in place. A capped action is replaced by an
|
||
explanatory sentence, never an alert.
|
||
|
||
---
|
||
|
||
## 6. Interaction and haptics
|
||
|
||
- Selection (option tile, chip, grid tile, stepper, segmented, tab) →
|
||
`HapticFeedback.selectionClick()`.
|
||
- Booking confirmed and booking cancelled → `HapticFeedback.mediumImpact()`.
|
||
- Page transitions fade and slide 6% from the right on `easeOutCubic`.
|
||
- Pull-to-refresh on Home and Orders.
|
||
- Edge-to-edge, portrait-locked, dark status-bar icons — light over the auth
|
||
brand panel.
|
||
|
||
---
|
||
|
||
## 7. Accessibility and resilience
|
||
|
||
- **No text-scale clamp.** Components adapt instead: rows grow vertically,
|
||
buttons use a minimum height rather than a fixed one, the district grid drops
|
||
to one column past 1.15×, and important text wraps. Ellipsis is reserved for
|
||
secondary information — a delivery promise may truncate, a place name never
|
||
does. Two widget tests drive the whole booking flow, Orders and the receipt at
|
||
**1.8× text scale**; any overflow fails them.
|
||
- Every interactive surface carries `Semantics` with button/selected/enabled.
|
||
- Minimum 48px targets throughout.
|
||
- Every text row that can meet long data is `Flexible` with `ellipsis` — the
|
||
widget tests run in a font where every glyph is full-width, which is how the
|
||
remaining overflows were found and fixed.
|
||
- Contrast: `ink` on `surface` and white on `brand` both clear AA.
|
||
|
||
---
|
||
|
||
## 8. Voice
|
||
|
||
- Customer language only: *pickup*, *packages*, *destination*, *Miler*.
|
||
- Never: consignment, waybill, AWB, MPS, piece, manifest.
|
||
- Optional things are described, not warned about.
|
||
- Numbers are stated plainly — *"6 packages · 3 destinations"*, *"₹64 paid"*.
|
||
- Buttons are verbs: *Book Pickup*, *Track pickup*, *Cancel pickup booking*.
|
||
|
||
---
|
||
|
||
## 9. How design maps to code
|
||
|
||
```
|
||
lib/ui/tokens.dart every colour, type style, radius, shadow, duration
|
||
lib/ui/widgets/ the component library — screens compose, never restyle
|
||
lib/ui/screens/ layout and copy only
|
||
lib/data/doormile_api.dart the only place mock data lives
|
||
lib/state/app_state.dart one source of truth; all stage side effects
|
||
```
|
||
|
||
Rules that keep it honest: a screen never hardcodes a colour or size; business
|
||
rules (serviceability, caps, slot availability, pricing) never live in a widget;
|
||
and anything the backend will own is already served through the API layer so the
|
||
swap is a body change, not a redesign.
|
||
|
||
---
|
||
|
||
## 10. What is real and what is mocked
|
||
|
||
**Real:** the whole UI, navigation, state machine, validation, caps enforcement,
|
||
optionality rules, and every loading/empty/error path.
|
||
|
||
**Mocked in `doormile_api.dart`:** serviceable states and districts, pickup
|
||
slots, geocoding and place search, fare estimates, booking creation and
|
||
cancellation, and booking limits. Each returns the shape a real endpoint would.
|
||
|
||
**Stand-ins:** `DmRouteMap` and `DmMapPanel` are painted, not a map SDK. Pickup
|
||
photos render as honest placeholders — `ParcelVerification.photos` holds the
|
||
references, so `Image.network(...)` drops straight in. The *Simulate update*
|
||
stepper on Tracking replaces backend push events — deleting that one widget is
|
||
the only change needed when real status events arrive.
|
||
|
||
**Not built yet:** session persistence, push notifications, in-app support chat,
|
||
per-destination cancellation (cancellation is whole-pickup, before collection
|
||
only).
|
||
|
||
---
|
||
|
||
*This document is the CX UI baseline. The visual system, screens and booking
|
||
structure are approved and settled.*
|