Files
doormile_customer_app/lib/ui/tokens.dart
Thiru-tenext 0d66627c3c Replace the customer app with Doormile CX
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>
2026-09-15 16:07:33 +05:30

396 lines
14 KiB
Dart

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.
///
/// **Manrope 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.
static const brand = Color(0xFF8F0F06);
/// Pressed state, and crimson text sitting on a [brandSoft] wash where the
/// full brand would vibrate against the tint.
static const brandPress = Color(0xFF730C05);
static const brandDeep = brandPress;
/// 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.
static const canvas = Color(0xFFF7F6F4);
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);
/// 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._();
/// Tiles, chips, the small square that holds a glyph.
static const xs = 8.0;
/// Buttons, fields, icon buttons. The design's most-used corner.
static const sm = 9.0;
/// Cards and every panel that holds rows.
static const md = 10.0;
/// Sheets' grabber area, the segmented control, the header's bottom sweep.
static const lg = 12.0;
/// Bottom sheets and the auth sheet that overlaps the hero.
static const xl = 16.0;
/// The brand header's bottom corners.
static const header = 18.0;
static const pill = 999.0;
/// Kept for call sites that mean "a primary bar's corner".
static const book = sm;
static BorderRadius all(double r) => BorderRadius.circular(r);
}
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;
/// 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),
),
];
/// 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);
}
/// Manrope for voice, Geist Mono for data.
///
/// Both ship as single variable-font files, which is why every style below sets
/// `fontVariations` as well as `fontWeight` — see the note inside the class.
class DmText {
DmText._();
static const _f = 'Manrope';
static const _mono = 'GeistMono';
// The weight axis. Manrope and Geist Mono both ship as variable fonts, 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.
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.12,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -1.09, color: DmColors.ink,
);
/// The brand header's own title, sitting on crimson.
static const headerTitle = TextStyle(
fontFamily: _f, fontSize: 34, height: 1.0,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -1.36, color: Colors.white,
);
/// Screen headings on a canvas.
static const title = TextStyle(
fontFamily: _f, fontSize: 25, height: 1.18,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -0.75, 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: 19, height: 1.2,
fontWeight: FontWeight.w800, fontVariations: _w800,
letterSpacing: -0.57, 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.w700, fontVariations: _w700,
letterSpacing: -0.18, 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.w700, fontVariations: _w700,
color: DmColors.ink2,
);
static const button = TextStyle(
fontFamily: _f, fontSize: 15.5, height: 1.2,
fontWeight: FontWeight.w700, fontVariations: _w700,
letterSpacing: -0.23,
);
/// 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.w700, fontVariations: _w700,
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.w700, fontVariations: _w700,
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.w800, fontVariations: _w800,
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.w700, fontVariations: _w700,
color: DmColors.ink,
);
}