One Touch, and a pickup map worth the name
Two ways to book the same parcel, and the difference said where the choice is made rather than in a paragraph about it. ONE TOUCH is the sphere: a state, a district, a window, and nothing else. The door, the recipient and the weight go out omitted, which the contract already treats as "not added yet" - the Miler writes them down standing at the address with the parcel in hand, which is the only moment anybody knows them. The pick/drop form under it is the other way in. PICKUP already carries the detected address; DROP is the only row with a question on it, and it asks for the full door - once per destination - before the window. Captions rather than an explainer: "Just a city and a time" under the sphere, "Enter the full address yourself" on the row. The pickup screen is a map screen now instead of a screen with a map on it. Full-bleed basemap, pin nailed to the centre, floating search and back, and a fixed sheet carrying what the pin resolved to. The collect-from sheet can hand off to it and carry the pin back. Also: * Home reads what-is-running, then the one gesture, then the form. The live card lost its courier row and half its height. * The sphere turns inside out on press - white floods from the centre and the word inverts through the clip, not by tween - and bounces back on release. * Order IDs copy, on Orders and on tracking. * Tracking says PICKUP and DROP with an arrow between them, carries the reference at the foot of the card it identifies, and shows every timing exactly once. * The wordmark bar is gone from all three roots; Orders and Account had no SafeArea under it, so their titles were sitting under the notch. flutter analyze: clean. flutter test: 90 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:
@@ -15,6 +15,7 @@ import '../widgets/misc.dart';
|
||||
import 'booking/booking_routes.dart';
|
||||
import 'booking/send_screen.dart';
|
||||
import 'sheets/destination_sheet.dart';
|
||||
import 'sheets/drop_address_sheet.dart';
|
||||
import 'sheets/place_search_sheet.dart';
|
||||
import 'sheets/window_sheet.dart';
|
||||
import 'tracking_screen.dart';
|
||||
@@ -52,19 +53,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
/// BOOK. The one action on this screen, and the whole flow's front door.
|
||||
///
|
||||
/// Two questions are asked here, as sheets over Home, before any form:
|
||||
/// ── Where, then when, then the review ──
|
||||
///
|
||||
/// where is it going? → when shall we collect? → the details
|
||||
/// Two sheets in sequence: states → districts, then the pickup window. The
|
||||
/// second rises while the first is still falling, so the handover reads as
|
||||
/// one surface changing its question rather than as a sheet failing and
|
||||
/// another arriving.
|
||||
///
|
||||
/// Both used to be answered somewhere else. The destination was a strip of
|
||||
/// cards halfway down the form; the window was picked *for* the customer,
|
||||
/// silently, the moment they chose a city — they arrived at a booking with a
|
||||
/// time on it that they had never agreed to. A pickup window is a promise
|
||||
/// about somebody's afternoon, and it is not ours to assume.
|
||||
///
|
||||
/// Dismissing the window sheet is allowed: the send screen carries "Choose a
|
||||
/// window" in the footer and its button stays disabled until there is one.
|
||||
Future<void> _book() async {
|
||||
/// Dismissing the window sheet is allowed. The review screen carries the
|
||||
/// window as its own row — brand-coloured while it is unanswered — and the
|
||||
/// button stays disabled until there is one, so backing out of the second
|
||||
/// sheet costs a tap rather than the booking.
|
||||
Future<void> _book({bool detailed = false}) async {
|
||||
final app = AppScope.read(context);
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
@@ -73,9 +73,26 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
final places = await showDestinationSheet(context);
|
||||
if (places == null || places.isEmpty || !mounted) return;
|
||||
|
||||
app.startBooking();
|
||||
app.startBooking(detailed: detailed);
|
||||
app.setDestinations(places);
|
||||
|
||||
// The full form asks for the door before it asks for the window, and it
|
||||
// asks once per destination: a visit that fans out to three places is
|
||||
// three addresses, and "for all parcels" is the whole point of choosing
|
||||
// this path over One Touch.
|
||||
if (detailed) {
|
||||
for (final group in app.draftDestinations) {
|
||||
if (!mounted) return;
|
||||
final saved = await showDropAddressSheet(
|
||||
context,
|
||||
details: group.details,
|
||||
district: group.destination.shortLabel,
|
||||
);
|
||||
if (saved != true) break;
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
await showWindowSheet(context);
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -95,7 +112,11 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
// The sphere's own field, floored so a very short window leaves it a
|
||||
// sphere rather than a slot. It is a *minimum*: the scroll view hands it
|
||||
// whatever is left once the card below has taken what it needs.
|
||||
final orbHeight = math.max(300.0, MediaQuery.sizeOf(context).height * 0.38);
|
||||
// Lower than it was. The sphere gained a second control under it and the
|
||||
// live card was sitting a third of a screen below both; the field only has
|
||||
// to be big enough for the sphere to float in, not to push everything else
|
||||
// off the fold.
|
||||
final orbHeight = math.max(260.0, MediaQuery.sizeOf(context).height * 0.30);
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
@@ -114,7 +135,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
// this is the top of the screen, so it takes the inset the bar used
|
||||
// to hold.
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(DmSpace.pad, 14, DmSpace.pad, 0),
|
||||
padding: const EdgeInsets.fromLTRB(DmSpace.pad, 26, DmSpace.pad, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -155,17 +176,29 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_PickupLine(
|
||||
title: app.pickup?.title ?? 'Set a pickup address',
|
||||
onTap: () async {
|
||||
final place = await showPlaceSearchSheet(context);
|
||||
if (place != null && context.mounted) app.setPickup(place);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── What is running comes first ──
|
||||
//
|
||||
// The live card sat under the sphere, at the foot of the screen. A
|
||||
// customer with a parcel in the network opens this app to check on
|
||||
// it, not to send another one — so it is the first thing under their
|
||||
// own name, and the sphere is what they scroll past it to.
|
||||
if (active != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
DmSpace.pad,
|
||||
14,
|
||||
DmSpace.pad,
|
||||
0,
|
||||
),
|
||||
child: _LiveCard(booking: active),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(duration: 380.ms, delay: 160.ms)
|
||||
.slideY(begin: -0.08, curve: DmMotion.ease),
|
||||
Expanded(
|
||||
child: AnimatedScale(
|
||||
scale: _handingOver ? 0.96 : 1,
|
||||
@@ -201,7 +234,30 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
DmBookOrb(onTap: _book, held: _handingOver)
|
||||
// ── What each way asks for ──
|
||||
//
|
||||
// Two ways to book the same parcel is not
|
||||
// something a customer can infer from a sphere
|
||||
// and a form. One line each, in place: this one
|
||||
// under the sphere, the other on the DROP row.
|
||||
// Said side by side in a paragraph they would be
|
||||
// a comparison nobody reads; said where the
|
||||
// choice is made they are two captions.
|
||||
DmBookOrb(
|
||||
onTap: _book,
|
||||
held: _handingOver,
|
||||
// Short enough to sit on one line, and
|
||||
// written to be read against the DROP
|
||||
// row's note rather than on its own:
|
||||
//
|
||||
// Just a city and a time
|
||||
// Enter the full address yourself
|
||||
//
|
||||
// Two captions, same length, opposite
|
||||
// answers. A paragraph comparing them
|
||||
// would say more and be read by nobody.
|
||||
caption: 'Just a city and a time',
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(duration: 420.ms)
|
||||
.scale(
|
||||
@@ -210,18 +266,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
duration: 620.ms,
|
||||
curve: Curves.easeOutBack,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// The promise, as a chip. It was a sentence under
|
||||
// the sphere on first run and nothing afterwards;
|
||||
// a returning customer is the one who most wants
|
||||
// to know a Miler is three minutes away.
|
||||
const _PromiseChip(
|
||||
icon: LucideIcons.zap,
|
||||
label: '3 min pickup',
|
||||
).animate().fadeIn(
|
||||
duration: 380.ms,
|
||||
delay: 220.ms,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -232,29 +276,29 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// ── The foot: what is running, and nothing else ──
|
||||
// ── Everything under the sphere is pinned ──
|
||||
//
|
||||
// A "quick dispatch" row of shortcut cities lived here and was
|
||||
// removed. It was a second way to start the one thing this screen
|
||||
// starts, and it cost the sphere the space that makes it read as an
|
||||
// object floating in a page rather than as a button in a stack.
|
||||
// The scroll view centres the sphere in whatever height is left, and
|
||||
// it cannot know about anything laid out after it — so anything laid
|
||||
// out inside that column pushed the thing before it under the fold.
|
||||
// The form sits outside it, and the sphere floats in what remains.
|
||||
//
|
||||
// The card sits outside the scroll view, so the sphere is centred in
|
||||
// what is actually left rather than in a viewport pretending the
|
||||
// card is not there.
|
||||
if (active != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
DmSpace.pad,
|
||||
14,
|
||||
DmSpace.pad,
|
||||
6,
|
||||
),
|
||||
child: _LiveCard(booking: active),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(duration: 380.ms, delay: 160.ms)
|
||||
.slideY(begin: 0.12, curve: DmMotion.ease),
|
||||
// A "3 min pickup" chip used to sit here. It was riding the sphere's
|
||||
// lower arc — the field is centred on the disc, not on the glow
|
||||
// round it, so a chip pinned directly under the column overlapped
|
||||
// the shadow the sphere throws.
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(DmSpace.pad, 0, DmSpace.pad, 0),
|
||||
child: _PickDropForm(
|
||||
pickup: app.pickup?.title,
|
||||
onPickup: () async {
|
||||
final place = await showPlaceSearchSheet(context);
|
||||
if (place != null && context.mounted) app.setPickup(place);
|
||||
},
|
||||
onDrop: () => _book(detailed: true),
|
||||
).animate().fadeIn(duration: 380.ms, delay: 300.ms),
|
||||
),
|
||||
|
||||
// Clears the floating tab bar, which the shell stacks over the body.
|
||||
//
|
||||
// Measured, not guessed. A fixed 74 was right on the 844pt golden
|
||||
@@ -273,22 +317,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The live booking, at the foot of Home.
|
||||
/// The live booking, at the head of Home.
|
||||
///
|
||||
/// ── The loader went, and the card grew ──
|
||||
/// ── Smaller, and without the courier ──
|
||||
///
|
||||
/// It carried a thin filled line under the destination — a progress bar across
|
||||
/// the five customer milestones. It was the one fact that cannot be written as
|
||||
/// a word, and it still read as a *loader*: a bar that fills is the shape an
|
||||
/// app uses to say "wait", and a parcel two days into a journey is not
|
||||
/// something the customer is waiting through. The rail on tracking says the
|
||||
/// same thing with five labelled nodes and no ambiguity.
|
||||
/// It carried a rule and a second row naming the Miler and their plate. Both
|
||||
/// are real and neither belongs here: this card sits above the sphere now, so
|
||||
/// every point it takes is a point the one action on the screen gives up — and
|
||||
/// the Miler's name is on the screen this card opens, beside the button that
|
||||
/// rings them, which is where it is any use.
|
||||
///
|
||||
/// What the space bought is size. This is the only thing on Home besides the
|
||||
/// sphere and it was a 90pt strip — a chip, a place and a courier compressed
|
||||
/// into three tight lines. The destination is set at display weight with its
|
||||
/// promise under it now, a rule separates the journey from the person carrying
|
||||
/// it, and the arrow is a target rather than a glyph.
|
||||
/// What is left is what a glance asks: what is happening, where, and when.
|
||||
/// Three lines and an arrow.
|
||||
class _LiveCard extends StatelessWidget {
|
||||
const _LiveCard({required this.booking});
|
||||
|
||||
@@ -298,7 +338,6 @@ class _LiveCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final app = AppScope.read(context);
|
||||
final slot = app.slotById(booking.slotId);
|
||||
final miler = booking.miler ?? booking.deliveryAgent;
|
||||
|
||||
// The district alone. "Chennai, Tamil Nadu" shares its row with the state
|
||||
// and the reference, and on a 402pt device it rendered "Chenna…" — the
|
||||
@@ -334,20 +373,20 @@ class _LiveCard extends StatelessWidget {
|
||||
},
|
||||
children: [
|
||||
DmCardCell(
|
||||
padding: const EdgeInsets.fromLTRB(18, 17, 18, 18),
|
||||
padding: const EdgeInsets.fromLTRB(16, 13, 14, 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const DmLiveDot(size: 7),
|
||||
const SizedBox(width: 9),
|
||||
const DmLiveDot(size: 6),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
booking.stage.chipLabel.toUpperCase(),
|
||||
style: DmText.eyebrow.copyWith(
|
||||
fontSize: 10,
|
||||
fontSize: 9.5,
|
||||
letterSpacing: 0.9,
|
||||
color: DmColors.brand,
|
||||
),
|
||||
@@ -359,7 +398,10 @@ class _LiveCard extends StatelessWidget {
|
||||
Flexible(
|
||||
child: Text(
|
||||
booking.displayId,
|
||||
style: DmText.monoSm.copyWith(color: DmColors.ink3),
|
||||
style: DmText.monoSm.copyWith(
|
||||
fontSize: 11,
|
||||
color: DmColors.ink3,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -367,7 +409,7 @@ class _LiveCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const SizedBox(height: 9),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -377,15 +419,14 @@ class _LiveCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
place,
|
||||
style: DmText.headerTitle.copyWith(fontSize: 26),
|
||||
style: DmText.headerTitle.copyWith(fontSize: 21),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
promise,
|
||||
style: DmText.body.copyWith(
|
||||
fontSize: 14.5,
|
||||
style: DmText.small.copyWith(
|
||||
fontSize: 13,
|
||||
color: DmColors.ink3,
|
||||
),
|
||||
maxLines: 1,
|
||||
@@ -395,12 +436,9 @@ class _LiveCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// A disc, not a bare chevron. It is the only affordance on
|
||||
// the card and it sits against 26pt of type, where a 17pt
|
||||
// glyph on its own disappears.
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
color: DmColors.brandSoft,
|
||||
@@ -408,47 +446,12 @@ class _LiveCard extends StatelessWidget {
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.arrowRight,
|
||||
size: 19,
|
||||
size: 17,
|
||||
color: DmColors.brand,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (miler != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: DmColors.hairline,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
DmAvatar(initials: miler.initials, size: 38),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
miler.name,
|
||||
style: DmText.cardTitle.copyWith(fontSize: 14.5),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
miler.vehicle,
|
||||
style: DmText.monoSm.copyWith(color: DmColors.ink3),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -457,103 +460,178 @@ class _LiveCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the Miler is coming to, under the greeting.
|
||||
/// Pickup over drop, on one card — the shape every ride app asks a journey in.
|
||||
///
|
||||
/// ── Box → line → pill → line → row → pill ──
|
||||
/// ── Why the app has two ways to start and only one of them is a sphere ──
|
||||
///
|
||||
/// It has been most shapes. The one it has landed on is the reference's: a
|
||||
/// small bordered pill holding a brand pin, the address, and a chevron-down.
|
||||
/// One Touch is the product: a state, a district, a window, and the Miler
|
||||
/// writes down the door when they get there. It is a sphere because it is one
|
||||
/// gesture, and a gesture cannot be a form.
|
||||
///
|
||||
/// The pill is right *here* and was wrong before because the screen changed
|
||||
/// around it. On a page whose only other objects were a name and a sphere it
|
||||
/// was a container built to hold one line of text. On a page that now opens
|
||||
/// with a wordmark bar and ends with a floating one, it is the middle term —
|
||||
/// and the chevron-down is the honest glyph, because what it opens is a
|
||||
/// chooser, not another screen.
|
||||
class _PickupLine extends StatelessWidget {
|
||||
const _PickupLine({required this.title, required this.onTap});
|
||||
/// This is the same booking for a customer who already knows the door. Both
|
||||
/// ends are here because a journey has two, and seeing them together is what
|
||||
/// makes it obvious that one is already answered — the pickup carries the
|
||||
/// address the app detected, so the only row with a question on it is the
|
||||
/// drop.
|
||||
///
|
||||
/// It replaced a line of text reading "Enter the full address instead", which
|
||||
/// said what would happen and showed none of it. A form that is already half
|
||||
/// filled in is a better argument for itself than a sentence about forms.
|
||||
class _PickDropForm extends StatelessWidget {
|
||||
const _PickDropForm({
|
||||
required this.pickup,
|
||||
required this.onPickup,
|
||||
required this.onDrop,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? pickup;
|
||||
final VoidCallback onPickup;
|
||||
final VoidCallback onDrop;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DmCard(
|
||||
children: [
|
||||
DmCardCell(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 14, 6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_End(
|
||||
label: 'PICKUP',
|
||||
value: pickup ?? 'Set a pickup address',
|
||||
unset: pickup == null,
|
||||
origin: true,
|
||||
last: false,
|
||||
onTap: onPickup,
|
||||
),
|
||||
_End(
|
||||
label: 'DROP',
|
||||
value: 'Where is it going?',
|
||||
note: 'Enter the full address yourself',
|
||||
unset: true,
|
||||
origin: false,
|
||||
last: true,
|
||||
onTap: onDrop,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One end of the journey: a node on a thread, a micro-label, and the answer
|
||||
/// or the question.
|
||||
class _End extends StatelessWidget {
|
||||
const _End({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.unset,
|
||||
required this.origin,
|
||||
required this.last,
|
||||
required this.onTap,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
/// What choosing this row means. Only the drop carries one — the pickup is
|
||||
/// already answered, and a caption under an answer is noise.
|
||||
final String? note;
|
||||
final bool unset;
|
||||
final bool origin;
|
||||
final bool last;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Pickup from $title. Change',
|
||||
label: '$label $value',
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 40, maxWidth: 300),
|
||||
padding: const EdgeInsets.fromLTRB(11, 8, 11, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: DmColors.surface,
|
||||
borderRadius: DmRadius.all(DmRadius.pill),
|
||||
border: Border.all(color: DmColors.border),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(LucideIcons.mapPin, size: 15, color: DmColors.brand),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: DmText.cardTitle.copyWith(fontSize: 14),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// The node and the thread it hangs from. The thread belongs to
|
||||
// the stop above the join, so the last one ends clean.
|
||||
SizedBox(
|
||||
width: 20,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 17),
|
||||
Container(
|
||||
width: 9,
|
||||
height: 9,
|
||||
decoration: BoxDecoration(
|
||||
color: unset && !origin
|
||||
? Colors.transparent
|
||||
: (origin ? DmColors.ink : DmColors.brand),
|
||||
shape: BoxShape.circle,
|
||||
border: unset && !origin
|
||||
? Border.all(color: DmColors.brand, width: 2)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (!last)
|
||||
const Expanded(
|
||||
child: VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
color: DmColors.border,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 10, 0, last ? 12 : 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(label, style: DmText.eyebrow),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: DmText.cardTitle.copyWith(
|
||||
fontSize: 15.5,
|
||||
color: unset ? DmColors.brand : null,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (note != null)
|
||||
Text(
|
||||
note!,
|
||||
style: DmText.small.copyWith(
|
||||
fontSize: 12.5,
|
||||
color: DmColors.ink3,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Icon(
|
||||
LucideIcons.chevronDown,
|
||||
size: 15,
|
||||
color: DmColors.ink4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 17,
|
||||
color: DmColors.ink4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The promise under the sphere: a glyph, a figure, a tint.
|
||||
///
|
||||
/// It was a full sentence — "A Miler collects from your door." — shown on the
|
||||
/// first run only. That is the app explaining itself to somebody who has
|
||||
/// already pressed nothing; what a customer actually wants before they commit
|
||||
/// is how long they will wait, and that is worth saying every time.
|
||||
class _PromiseChip extends StatelessWidget {
|
||||
const _PromiseChip({required this.icon, required this.label});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(11, 7, 13, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: DmColors.surface,
|
||||
borderRadius: DmRadius.all(DmRadius.pill),
|
||||
boxShadow: DmShadow.card,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 13, color: DmColors.brand),
|
||||
const SizedBox(width: 7),
|
||||
Text(
|
||||
label,
|
||||
style: DmText.label.copyWith(fontSize: 12.5, color: DmColors.ink),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user