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

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../tokens.dart';
@@ -20,15 +21,15 @@ enum DmNodeKind {
/// Where the parcel starts. A hollow circle: a door, not a decision.
origin,
/// A sorting hub in between. An outlined diamond — the parcel passes
/// A sorting hub in between. A hollow crimson ring — the parcel passes
/// through, it does not stop here.
hub,
/// The destination. A filled crimson diamond, the only solid node on the
/// thread, so the eye lands on where the parcel is going.
/// The destination. A filled map pin, the only solid node on the thread, so
/// the eye lands on where the parcel is going.
drop,
/// A leg that has not been arranged yet — dashed outline.
/// A leg that has not been arranged yet — an outlined pin.
pending,
}
@@ -243,44 +244,31 @@ class _Node extends StatelessWidget {
border: Border.all(color: DmColors.ink4, width: 2),
),
),
// Rotated squares rather than a second circle: on a thread of three
// identical dots nothing tells you which end matters, and at 11pt a
// diamond is distinguishable from a circle at a glance where two
// shades of the same dot are not.
DmNodeKind.hub => Transform.rotate(
angle: 0.785398,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: DmColors.surface,
borderRadius: BorderRadius.circular(2),
border: Border.all(color: DmColors.brand, width: 2),
),
// ── The destination is a pin, not a diamond ──
//
// A rotated square distinguishes the ends of the thread, which was
// the whole argument for it, and it does that by being a shape with
// no meaning attached. A map pin is the mark every customer already
// reads as *a place*, so the thread stops needing a legend: hollow
// circle where you are, pin where it is going.
DmNodeKind.hub => Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: DmColors.surface,
border: Border.all(color: DmColors.brand, width: 2),
),
),
DmNodeKind.drop => Transform.rotate(
angle: 0.785398,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: DmColors.brand,
borderRadius: BorderRadius.circular(2),
),
),
DmNodeKind.drop => const Icon(
LucideIcons.mapPin,
size: 15,
color: DmColors.brand,
),
DmNodeKind.pending => Transform.rotate(
angle: 0.785398,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: DmColors.surface,
borderRadius: BorderRadius.circular(2),
border: Border.all(color: DmColors.ink4, width: 1.5),
),
),
DmNodeKind.pending => const Icon(
LucideIcons.mapPin,
size: 15,
color: DmColors.ink4,
),
},
),