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

@@ -27,13 +27,22 @@ class DmColors {
// ── Brand ────────────────────────────────────────────────────────────────
/// The mark's single colour.
///
/// Briefly brightened to a vermilion on 2026-09-23 to match a reference
/// design and put straight back. The palette is not up for redesign — the
/// reference's *layout* is what the app is being drawn against, and the
/// oxblood carries it perfectly well.
static const brand = Color(0xFF8F0F06);
/// Pressed state, and crimson text sitting on a [brandSoft] wash where the
/// Pressed state, and brand text sitting on a [brandSoft] wash where the
/// full brand would vibrate against the tint.
static const brandPress = Color(0xFF730C05);
static const brandDeep = brandPress;
/// The lit edge of the sphere and of the primary button's gradient. Named
/// rather than repeated as a literal in three files.
static const brandLit = Color(0xFFA6180B);
/// Tint behind selected options, wash cards and brand chips.
static const brandSoft = Color(0xFFFBF3F2);
@@ -48,13 +57,25 @@ class DmColors {
// ── Surfaces ─────────────────────────────────────────────────────────────
/// The page canvas.
static const canvas = Color(0xFFF7F6F4);
///
/// Deepened on 2026-09-23. At `#F7F6F4` a white card on it was a 3% step,
/// so the 1px border was doing all the separating and every screen read as
/// a list of outlined boxes. A canvas the cards can sit *on* is what lets
/// the edge go quiet.
static const canvas = Color(0xFFF1F0EC);
static const surface = Color(0xFFFFFFFF);
/// Filled inputs, quiet chips, tiles, skeleton bases. Also the "sunken"
/// treatment for a disabled primary action.
static const surfaceAlt = Color(0xFFF1EFEC);
/// The bed a segmented control's chosen segment is raised out of.
///
/// A step below [surfaceAlt], which is within a point of the canvas and so
/// drew no groove at all on a sheet. A segment only reads as *raised* if
/// there is something visible for it to be raised out of.
static const groove = Color(0xFFE7E4E0);
/// Barely-there divider between rows inside one card.
static const hairline = Color(0xFFF1EFEC);
@@ -111,7 +132,15 @@ class DmRadius {
static const sm = 12.0;
/// Cards and every panel that holds rows.
static const md = 14.0;
static const md = 18.0;
/// A card that stands alone on the canvas rather than in a stack of rows.
/// Wider than [md] because a shadowed card with no edge needs the corner to
/// carry the shape the border used to.
static const card = 20.0;
/// A chip: a status pill, a tier badge, a stat tile's glyph well.
static const chip = 8.0;
/// Sheets' grabber area, the segmented control, the header's bottom sweep.
static const lg = 16.0;
@@ -137,11 +166,14 @@ class DmRadius {
class DmSpace {
DmSpace._();
/// Horizontal page padding. 22 on the brand header's body, 24 on canvas
/// content — the header's crimson field reads as tighter than it measures,
/// so it takes the smaller inset.
static const pad = 24.0;
static const headerPad = 22.0;
/// Horizontal page padding.
///
/// 24 once, 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 — the reference designs this
/// was drawn against run their cards to within about a finger of the edge.
static const pad = 16.0;
static const headerPad = 16.0;
/// Minimum touch target.
static const tap = 48.0;
@@ -176,6 +208,53 @@ class DmShadow {
),
];
/// A card resting on the canvas.
///
/// ── Why the cards stopped having edges ──
///
/// Every surface in this app was separated by a 1px hairline, which is
/// honest and cheap and, repeated eight times down a screen, reads as a
/// wireframe: the outline is the loudest thing about an object whose whole
/// job is to be quiet and hold something else.
///
/// Two very soft layers do it instead — a wide, faint ambient wash and a
/// short contact shadow just under the shape. Neither is visible as a
/// shadow; together they lift the white off the warm canvas so the card is
/// read as *nearer* rather than as *outlined*. That is the difference
/// between a premium consumer surface and a bordered panel, and it costs
/// nothing but two `BoxShadow`s.
static const card = [
BoxShadow(
color: Color(0x0D1F1A17),
blurRadius: 22,
spreadRadius: -10,
offset: Offset(0, 8),
),
BoxShadow(
color: Color(0x081F1A17),
blurRadius: 3,
spreadRadius: -1,
offset: Offset(0, 1),
),
];
/// A card the customer has to act on, or the one live thing in a list.
/// The same two layers, a third again as deep.
static const cardRaised = [
BoxShadow(
color: Color(0x141F1A17),
blurRadius: 30,
spreadRadius: -12,
offset: Offset(0, 10),
),
BoxShadow(
color: Color(0x0B1F1A17),
blurRadius: 4,
spreadRadius: -1,
offset: Offset(0, 2),
),
];
/// Controls that genuinely float over content — map buttons, map chips.
static const float = [
BoxShadow(
@@ -250,18 +329,33 @@ class DmMotion {
class DmText {
DmText._();
static const _f = 'Poppins';
/// ── Switzer → Plus Jakarta Sans (2026-09-24) ──
///
/// Both are variable neo-grotesques and the swap is a one-line change, but
/// two things come with it. Plus Jakarta's humanist curves and taller
/// x-height give the place names the editorial weight the reference design
/// has, and — the practical part — **it carries the rupee glyph**, which
/// Switzer does not. Every amount in this app was set in Geist Mono for that
/// reason alone; the ones that are prices rather than data can now be type.
///
/// Its axis is wght 200–800, so every weight the design uses is real rather
/// than synthesised.
static const _f = 'PlusJakartaSans';
static const _mono = 'GeistMono';
// The weight axis, for Geist Mono only. It ships as one variable file, so a
// style has to move `wght` to change weight — `fontWeight` alone picks the
// The weight axis. Switzer and Geist Mono both ship as one variable file, so
// a style has to move `wght` to change weight — `fontWeight` alone picks the
// nearest named instance and, on a single-file VF, that is always the
// default. Both are set on its styles: the axis does the work, the enum keeps
// the semantics (and the system-font fallback) correct.
// default. Both are set on every style: the axis does the work, the enum
// keeps the semantics (and the system-font fallback) correct.
//
// Poppins needs none of this. It is four static files and Flutter matches
// them on `fontWeight`.
// This came back when Poppins left. Poppins has no variable release, so for
// one release the sans styles carried `fontWeight` alone and this block was
// the mono's private business.
static const _w500 = [FontVariation('wght', 500)];
static const _w600 = [FontVariation('wght', 600)];
static const _w700 = [FontVariation('wght', 700)];
static const _w800 = [FontVariation('wght', 800)];
// Letter spacing is quoted in em by the design and converted here at each
// size, because -0.035em is a different number of pixels on a 31pt display
@@ -273,22 +367,26 @@ class DmText {
/// Screen-dominating statement — "Pickup booked", "Arun is 12 minutes away".
static const display = TextStyle(
fontFamily: _f, fontSize: 31, height: 1.1,
fontWeight: FontWeight.w800,
letterSpacing: -0.95, color: DmColors.ink,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -1.1, color: DmColors.ink,
);
/// The brand header's own title, sitting on crimson.
/// The largest line on a screen: the name on Home, the title of a tab root.
///
/// It was white, because it was written for a crimson header. That header is
/// a canvas now and every caller was overriding the colour back to ink, so
/// the white was a trap waiting for the first caller who did not.
static const headerTitle = TextStyle(
fontFamily: _f, fontSize: 34, height: 1.0,
fontWeight: FontWeight.w800,
letterSpacing: -1.2, color: Colors.white,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -1.4, color: DmColors.ink,
);
/// Screen headings on a canvas.
static const title = TextStyle(
fontFamily: _f, fontSize: 25, height: 1.16,
fontWeight: FontWeight.w800,
letterSpacing: -0.65, color: DmColors.ink,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -0.8, color: DmColors.ink,
);
/// Tab-root page titles where no brand header is used.
@@ -297,14 +395,14 @@ class DmText {
/// The heading above a group of cards, and a sheet's own title.
static const heading = TextStyle(
fontFamily: _f, fontSize: 18, height: 1.25,
fontWeight: FontWeight.w700,
letterSpacing: -0.4, color: DmColors.ink,
fontWeight: FontWeight.w700, fontVariations: _w700,
letterSpacing: -0.45, color: DmColors.ink,
);
static const sheetTitle = TextStyle(
fontFamily: _f, fontSize: 21, height: 1.2,
fontWeight: FontWeight.w800,
letterSpacing: -0.55, color: DmColors.ink,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -0.65, color: DmColors.ink,
);
// ── Rows ─────────────────────────────────────────────────────────────────
@@ -312,8 +410,8 @@ class DmText {
/// A row's own line — `t1` in the design, and the workhorse of the app.
static const cardTitle = TextStyle(
fontFamily: _f, fontSize: 15, height: 1.4,
fontWeight: FontWeight.w600,
letterSpacing: -0.15, color: DmColors.ink,
fontWeight: FontWeight.w600, fontVariations: _w600,
letterSpacing: -0.2, color: DmColors.ink,
);
/// Alias: the same weight of statement, used mid-form.
@@ -321,55 +419,55 @@ class DmText {
static const body = TextStyle(
fontFamily: _f, fontSize: 15, height: 1.5,
fontWeight: FontWeight.w500,
letterSpacing: -0.1, color: DmColors.ink,
fontWeight: FontWeight.w500, fontVariations: _w500,
letterSpacing: -0.12, color: DmColors.ink,
);
/// Supporting line under a row title — `t2`.
static const small = TextStyle(
fontFamily: _f, fontSize: 13.5, height: 1.45,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, fontVariations: _w500,
color: DmColors.ink2,
);
/// The quietest line — `t3`. Timestamps, counts, "collected on".
static const tiny = TextStyle(
fontFamily: _f, fontSize: 13, height: 1.4,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, fontVariations: _w500,
color: DmColors.ink3,
);
/// Paragraph under a screen heading.
static const lede = TextStyle(
fontFamily: _f, fontSize: 14.5, height: 1.55,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, fontVariations: _w500,
color: DmColors.ink2,
);
/// The right-hand value in a settings row.
static const value = TextStyle(
fontFamily: _f, fontSize: 13.5, height: 1.4,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontVariations: _w600,
color: DmColors.ink2,
);
/// Field labels, tags, chips, inline actions.
static const label = TextStyle(
fontFamily: _f, fontSize: 12.5, height: 1.3,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontVariations: _w600,
color: DmColors.ink2,
);
static const button = TextStyle(
fontFamily: _f, fontSize: 16, height: 1.2,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
fontWeight: FontWeight.w700, fontVariations: _w700,
letterSpacing: -0.25,
);
/// The text-only action under a primary button ("Back to home").
static const textButton = TextStyle(
fontFamily: _f, fontSize: 14.5, height: 1.2,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontVariations: _w600,
color: DmColors.ink2,
);
@@ -377,22 +475,22 @@ class DmText {
/// it heads brand content, [DmColors.ink3] when it heads a neutral group.
static const eyebrow = TextStyle(
fontFamily: _f, fontSize: 10.5, height: 1.3,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontVariations: _w600,
letterSpacing: 1.16, color: DmColors.ink3,
);
/// The eyebrow as it appears on the crimson header.
static const headerEyebrow = TextStyle(
fontFamily: _f, fontSize: 11.5, height: 1.3,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w700, fontVariations: _w700,
letterSpacing: 1.84, color: DmColors.onBrand3,
);
/// Sentence-case heading above a group of rows.
static const sectionHead = TextStyle(
fontFamily: _f, fontSize: 15, height: 1.3,
fontWeight: FontWeight.w700,
letterSpacing: -0.25, color: DmColors.ink,
fontWeight: FontWeight.w700, fontVariations: _w700,
letterSpacing: -0.3, color: DmColors.ink,
);
/// Alias kept for existing call sites.
@@ -401,7 +499,7 @@ class DmText {
/// Field input text.
static const field = TextStyle(
fontFamily: _f, fontSize: 16, height: 1.3,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontVariations: _w600,
color: DmColors.ink,
);
@@ -431,7 +529,7 @@ class DmText {
/// The OTP digits.
static const otp = TextStyle(
fontFamily: _f, fontSize: 24, height: 1.1,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontVariations: _w600,
color: DmColors.ink,
);
}