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:
@@ -7,7 +7,6 @@ import '../../../state/app_scope.dart';
|
||||
import '../../state_marks.dart';
|
||||
import '../../tokens.dart';
|
||||
import '../../widgets/buttons.dart';
|
||||
import '../../widgets/cards.dart';
|
||||
import '../../widgets/feedback.dart';
|
||||
import '../../widgets/inputs.dart';
|
||||
import '../../widgets/states.dart';
|
||||
@@ -165,8 +164,29 @@ class _DestinationSheetState extends State<_DestinationSheet> {
|
||||
Expanded(
|
||||
child: DmSheetHeader(
|
||||
title: browsing?.name ?? 'Where is it going?',
|
||||
// ── A line of orientation ──
|
||||
//
|
||||
// The sheet asked a question and then showed a list, and a
|
||||
// customer who has never used it cannot tell whether tapping
|
||||
// a state chooses it or opens it. One sentence says which,
|
||||
// and at the district step says how many they may pick.
|
||||
subtitle: browsing == null
|
||||
? 'Pick a state to see the cities it serves'
|
||||
: (app.allowsMultipleDestinations
|
||||
? 'Choose one or more districts'
|
||||
: 'Choose a district'),
|
||||
),
|
||||
),
|
||||
// A close, because a sheet that can only be dismissed by dragging
|
||||
// is a sheet somebody will get stuck in.
|
||||
DmIconButton(
|
||||
icon: LucideIcons.x,
|
||||
tooltip: 'Close',
|
||||
background: DmColors.surface,
|
||||
border: Colors.transparent,
|
||||
size: 38,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
DmSearchField(
|
||||
@@ -295,23 +315,22 @@ class _StateRow extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return _Row(
|
||||
onTap: onTap,
|
||||
leading: DmTile(icon: stateMark(area.code), tone: DmTileTone.wash),
|
||||
// The mark, with nothing behind it. It sat in a 38pt washed-crimson
|
||||
// tile, so a list of six states was six pink squares in a column — six
|
||||
// containers and six spots of brand to hold six glyphs that read
|
||||
// perfectly well on the sheet's own surface.
|
||||
leading: Icon(stateMark(area.code), size: 21, color: DmColors.ink3),
|
||||
title: area.name,
|
||||
subtitle:
|
||||
'$count ${count == 1 ? 'city' : 'cities'}'
|
||||
'${area.transitTag == null ? '' : ' · ${area.transitTag}'}',
|
||||
// The number of districts already chosen in this state. It was a filled
|
||||
// crimson pill — a badge, for a figure that is only ever 1, 2 or 3.
|
||||
trailing: chosen == 0
|
||||
? null
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: DmColors.brand,
|
||||
borderRadius: DmRadius.all(DmRadius.pill),
|
||||
),
|
||||
child: Text(
|
||||
'$chosen',
|
||||
style: DmText.label.copyWith(color: Colors.white),
|
||||
),
|
||||
: Text(
|
||||
'$chosen',
|
||||
style: DmText.cardTitle.copyWith(color: DmColors.brand),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -400,11 +419,27 @@ class _Row extends StatelessWidget {
|
||||
selected: selected,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
// ── Cards, not ruled rows ──
|
||||
//
|
||||
// The rules were the right correction to the washed-crimson tiles that
|
||||
// came before them, and they left the sheet as the one screen in the
|
||||
// app still made of open rows while everything behind it had moved to
|
||||
// lifted white. A chooser is a set of objects you pick between, which
|
||||
// is exactly what a card is for; the selected one takes the brand
|
||||
// edge, so *chosen* reads at a glance rather than from a tick.
|
||||
child: AnimatedContainer(
|
||||
duration: DmMotion.fast,
|
||||
curve: DmMotion.ease,
|
||||
constraints: const BoxConstraints(minHeight: DmSpace.tap),
|
||||
padding: EdgeInsets.symmetric(vertical: leading == null ? 14 : 10),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: DmColors.hairline)),
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.fromLTRB(14, 13, 14, 13),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? DmColors.brandSoft : DmColors.surface,
|
||||
borderRadius: DmRadius.all(DmRadius.md),
|
||||
border: selected
|
||||
? Border.all(color: DmColors.brand, width: 1.5)
|
||||
: null,
|
||||
boxShadow: selected ? null : DmShadow.card,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -88,18 +88,27 @@ class _WindowSheetState extends State<_WindowSheet> {
|
||||
// One row of days rather than a heading per day: the list
|
||||
// under it stays short enough to take in at a glance, which
|
||||
// is the whole reason this is a sheet.
|
||||
Row(
|
||||
children: [
|
||||
for (final d in days) ...[
|
||||
if (d != days.first) const SizedBox(width: 8),
|
||||
DmChoiceChip(
|
||||
label: d,
|
||||
selected: d == day,
|
||||
expand: true,
|
||||
onTap: () => setState(() => _day = d),
|
||||
),
|
||||
// The segments sit in a groove, which is what makes the
|
||||
// chosen one read as raised rather than as merely white.
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: DmColors.groove,
|
||||
borderRadius: DmRadius.all(DmRadius.md),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
for (final d in days) ...[
|
||||
if (d != days.first) const SizedBox(width: 4),
|
||||
DmChoiceChip(
|
||||
label: d,
|
||||
selected: d == day,
|
||||
expand: true,
|
||||
onTap: () => setState(() => _day = d),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
for (final slot in shown) ...[
|
||||
|
||||
Reference in New Issue
Block a user