Redesign on the Stitch reference, in Plus Jakarta Sans

Four passes, and the shape they landed on.

The type face is Plus Jakarta Sans (variable, wght 200-800), which brings a
fix with it: it carries the rupee glyph and Switzer does not, so prices stop
being set in Geist Mono to work around a missing character. Mono stays where
it is earned - references and phone numbers, read digit by digit.

Surfaces lift rather than outline. Cards carry two very soft shadow layers
instead of a hairline, because eight outlined boxes down a screen read as a
wireframe. The tab bar floats as a pill again for the same reason it was
right to: it is now the same kind of object as everything above it.

Screens:

* Home is the greeting, the address, the sphere and one card. The card lost
  its progress bar - a filling line says "wait", and a parcel two days into
  a journey is not something anyone is waiting through - and gained the size
  that buys.
* Orders cards are four bands: identity, destination, route, and whatever is
  happening right now. Plus a search field, because the list is the archive.
* Tracking leads with the state at display size, then TRIP MILESTONES with a
  step counter, then the courier.
* Review is a route thread over two particular cards.
* Account opens on the person: avatar, name, and two counted figures.

Three real bugs the redesign surfaced:

* Quick dispatch handed `loadCities()` straight to a FutureBuilder, so the
  catalogue was refetched on every rebuild and Home never settled.
* Order cards showed the whole visit's weight on one destination's row -
  somebody else's parcel. Per group now, and only once actually weighed.
* The pickup window was printed beside "In transit", where it reads as a
  delivery time nobody promised.

Nothing invented. The reference shows EXPRESS PRIORITY, CARBON OFFSET,
CONCIERGE ELITE and hub-to-hub routing; this backend sends none of them, so
they are absent rather than mocked up.

flutter analyze: clean. flutter test: 88 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
This commit is contained in:
2026-09-24 11:08:25 +05:30
parent 06fa6b797a
commit 86b6af48c2
73 changed files with 3398 additions and 1722 deletions

View File

@@ -95,32 +95,28 @@ their own position without being told.
### 2.2 Typography
**Poppins for voice, Geist Mono for data.** Poppins (Indian Type Foundry, SIL
Open Font License) is the app's voice as of 2026-09-22.
**Switzer for voice, Geist Mono for data.** Switzer (Indian Type Foundry, via
Fontshare, ITF Free Font License — commercial use permitted) is the app's voice
as of 2026-09-23.
**Poppins has no variable release.** Google Fonts ships it as static cuts only,
so `pubspec.yaml` declares one file per weight and the sans styles below set
`fontWeight` and nothing else — `fontVariations` on a static font is ignored in
silence, which is worse than useless, because it reads as if it were doing the
work. Geist Mono is still variable and its three styles still carry the axis.
It is a neue-grotesque: flat-sided bowls, near-vertical terminals, a
straight-tailed `y`. It was chosen against a reference whose own face is almost
certainly PP Neue Montreal or Neue Haas Grotesk Display — both commercial, and
neither identifiable with certainty from a skewed mockup. Switzer is the
closest thing that can actually be bundled.
Only the four weights the design uses are bundled — **500, 600, 700, 800** —
because each is a separate ~155 KB file. That is ~625 KB of sans against the
165 KB a variable file cost, and it is the price of this typeface. Asking for
w900 anywhere would not fail; Flutter would quietly synthesise it from Bold,
and it would look like it.
**One variable file again, 141 KB.** Poppins held this slot for a day and has
no variable release: four static cuts at ~155 KB each, 625 KB of sans, and for
that release the sans styles carried `fontWeight` alone because
`fontVariations` on a static font is ignored in silence. The axis is back, so
every style below sets both — the axis does the work, the enum keeps the
semantics and the system-font fallback correct.
**Every weight is one step lighter than the face before it.** Poppins is built
on near-circles and carries far more ink at the same nominal weight than a
humanist sans. What was w900 is w800, w800 is w700, w700 is w600. At the old
numbers the headings looked inflated rather than strong.
**And the tracking is deeper again** — the opposite correction from the last
pass. Poppins is wide, round and has generous sidebearings, so display sizes
need pulling in harder than anything this app has used: the header runs at
**−1.2**. Body sizes stay near zero, because that same roundness is what makes
it legible small and tracking it in takes that back. Tracking belongs to a
typeface, not to a design.
**Tighter than anything before it.** A grotesque at display size wants pulling
in hard, and the reference sets its headlines almost touching: the header runs
at **−1.4**. Body sizes stay near zero. Tracking belongs to a typeface, not to
a design, and this is the third time these numbers have been rewritten for that
reason.
Letter spacing is authored in em by the design and converted per size, because
−0.035em is a different number of pixels on a 34pt header than on a 15pt row
@@ -129,13 +125,13 @@ tight.
| Style | Size / Height / Weight | Tracking | Used for |
| --- | --- | --- | --- |
| `headerTitle` | 34 / 1.0 / 800 | **−1.2** | The brand header's own title |
| `display` | 31 / 1.1 / 800 | −0.95 | The one dominant line on a screen |
| `title` | 25 / 1.16 / 800 | −0.65 | Screen headings on canvas |
| `sheetTitle` | 21 / 1.2 / 800 | −0.55 | Bottom-sheet titles |
| `heading` | 18 / 1.25 / 700 | −0.4 | The head above a group of cards |
| `cardTitle` | 15 / 1.4 / 600 | −0.15 | A row's own line — the workhorse |
| `body` | 15 / 1.5 / 500 | −0.1 | Paragraph text |
| `headerTitle` | 34 / 1.0 / 800 | **−1.4** | The tab root's own title |
| `display` | 31 / 1.1 / 800 | −1.1 | The one dominant line on a screen |
| `title` | 25 / 1.16 / 800 | −0.8 | Screen headings on canvas |
| `sheetTitle` | 21 / 1.2 / 800 | −0.65 | Bottom-sheet titles |
| `heading` | 18 / 1.25 / 700 | −0.45 | The head above a group of cards |
| `cardTitle` | 15 / 1.4 / 600 | −0.2 | A row's own line — the workhorse |
| `body` | 15 / 1.5 / 500 | −0.12 | Paragraph text |
| `lede` | 14.5 / 1.55 / 500 | — | Paragraph under a screen heading |
| `small` | 13.5 / 1.45 / 500 | — | The supporting line under a row title |
| `tiny` | 13 / 1.4 / 500 | — | The quietest line |
@@ -516,9 +512,7 @@ attached.
names, with an index. It is a copy, and a stale copy is worse than none, so
`tool/screens.sh` refreshes it in one command after the goldens are rendered.
The harness loads every Poppins cut, Geist Mono **and the Lucide icon font**
first — every cut, because Poppins is static and a `FontLoader` given one file
renders w800 headings by smearing the one weight it has —
The harness loads Switzer, Geist Mono **and the Lucide icon font** first —
without that, Flutter's test harness draws Ahem's black boxes and hollow
squares and the pictures are worthless.
@@ -734,7 +728,126 @@ having been pushed up by the thing that was tapped.
---
## 11. If you are adding something
## 11. The reference pass (2026-09-23)
A set of logistics-app references — white cards on a soft canvas, one dark
accent, big reference numbers, labelled columns — and one instruction: *less
red, less boxy, more modern*. What follows is what that actually meant in this
codebase.
### 11.1 The redness had one source
`DmBrandHeader` was 260pt of solid brand on Home, Orders and Account — three of
five screens opening on a wall of it. Every `onBrand*` token existed only to
survive that field, and the accent that is supposed to mean *do this* was the
background of half the product.
**It is the canvas now, with ink type.** What is left of the brand on those
screens is the sphere, the primary button and a live dot, which is what an
accent is for. The arcs survive at 3.5% crimson instead of 15% white — they
were struck from the mark's geometry and were never the colour doing the work.
Knock-ons: the header's address track became a bordered field and then a bare
line (below); the avatar and the Account/Orders actions dropped their on-brand
variants; the Miler card lost its crimson wash, which on a header-less screen
was the loudest surface left.
### 11.2 The canvas was doing no work
At `#F7F6F4` a white card was a **3% step** off the background, so the 1px
border was separating everything and every screen read as a list of outlined
boxes. Canvas → `#F1F0EC`, card radius **14 → 18**. The edge is still there; it
stopped being the only thing holding the layout together.
That change immediately exposed two screens whose content was floating on bare
canvas — the send screen's route and package rows, invisible as a problem at 3%
contrast and unfinished-looking at the new one. Both are on cards now.
**The gutter came in too**, `DmSpace.pad` **24 → 16**. Twenty-four was right
when the header was a crimson field and the canvas held loose rows; on a page
of white cards it left a broad margin down both sides and the cards read as
narrower than the screen.
### 11.3 Orders
The filter left the header. It was a segmented control recessed into the
crimson field, which made it look like part of the title bar rather than
something to press. It is three chips on the canvas, **ink** when selected —
one of three is always on, and crimson there would be a permanent red block on
a screen whose crimson is meant to be the status pills, the thing that changes.
The card follows the reference: **reference number and status pill, then From /
To as two columns, then Placed / Contents, then a horizontal journey rail.**
Two columns rather than a vertical thread, because the rail cost the full width
of the card to say two things; side by side, from and to are one glance and the
same route fits in half the height — which is what lets the journey rail exist
without the card becoming a page.
**The rail labels one node, not five.** The reference labels all three of its
nodes; that works at three and does not at five — "Booking created / Order
created / In transit / Out for delivery / Delivered" across 320pt is five
ellipses. The line carries *how far*, the word carries *where*.
"SEPTEMBER 2026" went from the header: it sat above a list that is not grouped
by month, does not scroll by month, and says "Today" and "Yesterday" on its own
rows.
### 11.4 Tracking, rebuilt
**The map went.** It was 178pt of basemap, two labels and a pin, on a screen
whose question is "where is my parcel". The pin was the *pickup address* — a
place the customer chose and already knows — not the Miler, who only has a
position for the twenty minutes between assignment and the door. A picture that
cannot show the thing it implies invites somebody to stare at a stationary dot
and conclude nothing is happening. The radar that preceded it went for the same
reason: animation standing in for an answer.
**The crimson head card went.** "Arun Kumar is assigned" at 27pt on a filled
brand field, with a supporting line, a progress rail and the window under it.
Crimson is the action colour and tracking is a screen you read. It is a white
card now: the reference at 26pt, a status pill, and the route as a thread.
**No progress bar inside it.** The journey rail below is the same five
milestones with the same fill, and two of them on one screen is the app
disagreeing with itself about which is the answer.
**The Miler card moved to the foot.** It sat directly under the reference,
which put *a person* between the customer and the answer they opened the screen
for. It is **who**; the screen's question is **where**. The journey has the
middle of the page now.
Also gone: a duplicate route card at the foot, the duplicate reference in the
top bar, and the white bed under the footer buttons.
### 11.5 Small things that were wrong
**The destination node is a map pin.** A rotated diamond distinguished the ends
of the thread — its whole argument — and did that by being a shape with no
meaning attached. A pin is the mark every customer already reads as *a place*,
so the thread needs no legend: hollow circle where you are, pin where it is
going.
**The status pill was floating mid-row.** `Flexible` gives a child the space it
needs and then leaves it at the *start* of what it was allotted. It takes an
`Align(centerRight)` to put a status where a status belongs.
**Home's address is a line, not a field.** It was a bordered white box with a
chevron — a text input that takes no text, which is the shape ride apps stopped
using years ago. A dot, the place, and a quiet "Change".
**BOOK / NOW is stacked**, which bought back size: both words get the sphere's
full width, so it is 30pt over two lines rather than the 24pt a single line had
to shrink to.
**Settings rows stopped truncating.** Two rounds of flex arithmetic produced
"Payment metho…" and "38 Mettupalaya…" in turn. The fix was not a third ratio:
the label never truncates, and a `value` is a word — anything longer belongs on
the screen the row opens.
---
## 12. If you are adding something
1. Take the value from `tokens.dart`. If it is not there, add it there.
2. Ask what the screen's one decision is. If your addition is not it, it is