Files
doormile_customer_app/lib/ui/tokens.dart
Thiru-tenext 86b6af48c2 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
2026-09-24 11:08:25 +05:30

536 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
/// Design tokens. Every colour, radius, shadow and text style in the app comes
/// from here — screens never hardcode a value.
///
/// Direction (2026-09-15 rebuild, from the 14-screen design):
///
/// **Edges, not elevation.** Surfaces are separated by a 1px warm hairline and
/// a 10pt radius, not by a shadow. The only shadows left in the system are the
/// crimson glow under the one committing action on a screen, the float under a
/// control that genuinely hovers over a map, and the sheet lift. A screen that
/// drops shadows everywhere has no way left to say "this one matters".
///
/// **One accent, three intensities.** Crimson fills the header and the primary
/// action, washes (`brandSoft` over `brandLine`) carry supporting information,
/// and everything else is warm neutral. There is no second hue competing for
/// meaning — `ok` survives only for the delivered state, `danger` only for
/// destructive text.
///
/// **Poppins for voice, Geist Mono for data.** Headings run at w800 with tight
/// negative tracking; anything a customer might read back aloud or check
/// digit-by-digit — a booking reference, a phone number, a fare, a plate, a
/// countdown — is set in mono so the glyphs line up and a transposed character
/// is visible.
class DmColors {
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 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);
/// The wash's own edge, and the outline on quiet brand buttons.
static const brandLine = Color(0xFFF0DEDC);
// ── Ink — warm neutrals, never pure black ────────────────────────────────
static const ink = Color(0xFF191716);
static const ink2 = Color(0xFF5C5654);
static const ink3 = Color(0xFF6E6864);
static const ink4 = Color(0xFFB4AEAA);
// ── Surfaces ─────────────────────────────────────────────────────────────
/// The page canvas.
///
/// 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);
/// A card's own edge — the workhorse of this design.
static const border = Color(0xFFE8E5E2);
// ── On-brand text ────────────────────────────────────────────────────────
/// Secondary text on a crimson field. Not white-at-opacity: a tinted rose
/// keeps its warmth where translucent white goes grey and dirty.
static const onBrand2 = Color(0xFFFFD9D4);
/// Tertiary text on a crimson field.
static const onBrand3 = Color(0xFFFFD1CB);
/// Recessed track inside the header (the segmented control's bed).
static const onBrandSunken = Color(0x24000000);
/// A control resting on the crimson field.
static const onBrandRaise = Color(0x2EFFFFFF);
// ── Semantic ─────────────────────────────────────────────────────────────
static const ok = Color(0xFF0A6B56);
static const okSoft = Color(0xFFE2F4EF);
static const danger = Color(0xFFB3231F);
/// "You are here" on a map. Deliberately not the brand crimson — on a map
/// crimson means the pickup pin, and blue is the convention every customer
/// already reads as their own position.
static const locationDot = Color(0xFF1B6EF3);
static const locationHalo = Color(0x241B6EF3);
static const scrim = Color(0x80191716);
// Backwards-compatible aliases used across the widget layer.
static const bg = canvas;
static const surface2 = surfaceAlt;
static const line = hairline;
static const lineStrong = border;
}
class DmRadius {
DmRadius._();
// Every corner was opened up on 2026-09-22. At 8–12 the app measured
// correct and looked like a form: a screen of tight rectangles, which is
// exactly the "boxy" the redesign was called in to fix. Four points across
// the scale is the difference between a panel and a card, and it costs
// nothing but the token.
/// Tiles, chips, the small square that holds a glyph.
static const xs = 10.0;
/// Buttons, fields, icon buttons. The design's most-used corner.
static const sm = 12.0;
/// Cards and every panel that holds rows.
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;
/// Bottom sheets and the auth sheet that overlaps the hero. Moved with the
/// scale — at 16 it had collided with [lg], and a sheet that shares a corner
/// with the cards inside it has stopped being a separate surface.
static const xl = 20.0;
/// The brand header's bottom corners.
static const header = 26.0;
static const pill = 999.0;
/// A primary bar's corner. Softer than a field's, because the committing
/// action is the one element on a screen allowed to look like an object
/// rather than part of the form.
static const book = 16.0;
static BorderRadius all(double r) => BorderRadius.circular(r);
}
class DmSpace {
DmSpace._();
/// 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;
/// Primary button height.
static const button = 54.0;
/// Field height — matched to [button] so a form and its action share a rhythm.
static const field = 54.0;
}
class DmShadow {
DmShadow._();
/// Under the one committing action on a screen, and under a selected slot.
static const lift = [
BoxShadow(
color: Color(0x808F0F06),
blurRadius: 24,
spreadRadius: -12,
offset: Offset(0, 10),
),
];
/// A smaller version of [lift] for chips and half-width controls.
static const liftSm = [
BoxShadow(
color: Color(0xA68F0F06),
blurRadius: 16,
spreadRadius: -8,
offset: Offset(0, 7),
),
];
/// 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(
color: Color(0x0D191716),
blurRadius: 2,
offset: Offset(0, 1),
),
];
static const sheet = [
BoxShadow(
color: Color(0x2E191716),
blurRadius: 40,
spreadRadius: -20,
offset: Offset(0, -8),
),
];
// Aliases kept for existing call sites.
static const two = float;
static const three = sheet;
}
class DmMotion {
DmMotion._();
static const fast = Duration(milliseconds: 140);
static const base = Duration(milliseconds: 250);
static const slow = Duration(milliseconds: 460);
static const ease = Curves.easeOutCubic;
/// The bottom nav's travelling pill — a touch of overshoot, which reads as
/// the indicator catching up rather than teleporting.
static const glide = Cubic(0.34, 1.06, 0.36, 1);
/// Screen transitions. An emphasised decelerate: it leaves quickly and
/// settles slowly, so a push feels driven at the start and arrived-at by the
/// end. `easeOutCubic` is too even across a distance this short — it reads
/// as a constant-speed slide.
static const enter = Cubic(0.05, 0.7, 0.1, 1);
}
/// Poppins for voice, Geist Mono for data.
///
/// ── Why the sans styles look different from the mono ones ──
///
/// Poppins has no variable release. Google Fonts ships it as static cuts only,
/// so the sans styles below set `fontWeight` and nothing else: there is no
/// `wght` axis to move, and `fontVariations` on a static font is ignored in
/// silence, which is worse than useless — it reads as if it were doing the
/// work. `pubspec.yaml` declares one file per weight and Flutter matches on the
/// enum. Geist Mono is still variable and its three styles still carry the axis.
///
/// Only the four weights the design uses are bundled — 500, 600, 700, 800 —
/// because each one is a separate ~155 KB file. Asking for w900 anywhere would
/// not fail; Flutter would quietly synthesise it from Bold, and it would look
/// like it.
///
/// ── Every weight is one step lighter than it was ──
///
/// Poppins is a geometric face built on near-circles, and it carries far more
/// ink at the same nominal weight than the humanist faces before it. Styles
/// that were w900 are w800, w800 are w700, w700 are w600. Set at the old
/// numbers the headings looked inflated rather than strong.
///
/// ── And the tracking is deeper again ──
///
/// The opposite correction from the last face. Poppins is wide, round and has
/// generous default 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 the same roundness is what makes it legible small and tracking
/// it in takes that back. Tracking belongs to a typeface, not to a design.
class DmText {
DmText._();
/// ── 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. 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 every style: the axis does the work, the enum
// keeps the semantics (and the system-font fallback) correct.
//
// 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
// head than on a 15pt row title, and splitting the difference would soften
// exactly the heads that are supposed to feel tight.
// ── Display ──────────────────────────────────────────────────────────────
/// Screen-dominating statement — "Pickup booked", "Arun is 12 minutes away".
static const display = TextStyle(
fontFamily: _f, fontSize: 31, height: 1.1,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -1.1, color: DmColors.ink,
);
/// 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, 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, fontVariations: _w800,
letterSpacing: -0.8, color: DmColors.ink,
);
/// Tab-root page titles where no brand header is used.
static const pageTitle = title;
/// 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, fontVariations: _w700,
letterSpacing: -0.45, color: DmColors.ink,
);
static const sheetTitle = TextStyle(
fontFamily: _f, fontSize: 21, height: 1.2,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -0.65, color: DmColors.ink,
);
// ── Rows ─────────────────────────────────────────────────────────────────
/// 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, fontVariations: _w600,
letterSpacing: -0.2, color: DmColors.ink,
);
/// Alias: the same weight of statement, used mid-form.
static const bodyStrong = cardTitle;
static const body = TextStyle(
fontFamily: _f, fontSize: 15, height: 1.5,
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, 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, 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, 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, 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, fontVariations: _w600,
color: DmColors.ink2,
);
static const button = TextStyle(
fontFamily: _f, fontSize: 16, height: 1.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, fontVariations: _w600,
color: DmColors.ink2,
);
/// Uppercase micro-label above a group — the design's `micro`. Crimson when
/// 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, 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, 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, fontVariations: _w700,
letterSpacing: -0.3, color: DmColors.ink,
);
/// Alias kept for existing call sites.
static const groupHead = sectionHead;
/// Field input text.
static const field = TextStyle(
fontFamily: _f, fontSize: 16, height: 1.3,
fontWeight: FontWeight.w600, fontVariations: _w600,
color: DmColors.ink,
);
// ── Mono: anything checkable digit by digit ──────────────────────────────
/// References, plates and phone numbers sitting inline in a row.
static const mono = TextStyle(
fontFamily: _mono, fontSize: 13, height: 1.4,
fontWeight: FontWeight.w500, fontVariations: _w500,
letterSpacing: -0.26, color: DmColors.ink3,
);
/// Step counters, timers, the smallest data.
static const monoSm = TextStyle(
fontFamily: _mono, fontSize: 11.5, height: 1.3,
fontWeight: FontWeight.w500, fontVariations: _w500,
letterSpacing: -0.23, color: DmColors.ink3,
);
/// A booking reference given its own line, or a fare.
static const monoLg = TextStyle(
fontFamily: _mono, fontSize: 19, height: 1.2,
fontWeight: FontWeight.w500, fontVariations: _w500,
letterSpacing: -0.38, color: DmColors.ink,
);
/// The OTP digits.
static const otp = TextStyle(
fontFamily: _f, fontSize: 24, height: 1.1,
fontWeight: FontWeight.w600, fontVariations: _w600,
color: DmColors.ink,
);
}