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:
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user