BOOK NOW, OpenStreetMap, and one segmented control instead of two
── The circle says what pressing it does ── "ONE TOUCH" named the *mechanism* — one gesture, no form — which is something the product team knows and a customer has to be taught. Nobody arrives at that screen wanting a touch; they want a parcel collected. The caption under the sphere still carries what makes it different from the form below. Renamed in the comments too. A codebase explaining "One Touch" against a button that says BOOK NOW is a trap for whoever reads it next. ── OpenStreetMap everywhere ── One line: the default provider was CARTO, it is `osm`. Nothing else moves — `DmMapTiles` already reads the template, subdomains, retina flag and attribution off the provider, so the credit line follows on its own. One thing recorded on the provider itself rather than left to be discovered: these are donated servers and the OSM Foundation's tile policy does not permit a distributed app to lean on them. A block looks like every tile turning into the ground colour at once, with no other symptom. Moving off it is one define — `DM_MAP_PROVIDER=carto|maptiler|stadia`, all serving OpenStreetMap data — and the map_config test now asserts the identifying User-Agent rather than only the URL, because that is what attributable traffic depends on. ── Orders had a second copy of the segmented control ── Its own `_Tab`, a pill radius, 3pt of padding and the count folded into the label's text — beside the pickup window's day switcher, which is DmChoiceChip in a rounded groove with 4pt of padding and the count in a bubble. Two controls doing one job, drifting apart a padding value at a time. It is the same control now, and `_Tab` is gone. DmChoiceChip's horizontal padding drops 12 → 9: three of them split a 390pt phone and "Cancelled" truncated to "Cancell…" at the old value. The day switcher has two chips and acres of room, so it loses nothing.
This commit is contained in:
@@ -266,7 +266,7 @@ class _SendScreenState extends State<SendScreen> {
|
||||
),
|
||||
// ── Only the long way asks for this ──
|
||||
//
|
||||
// One Touch books on a state, a district and a window; the door is
|
||||
// BOOK NOW books on a state, a district and a window; the door is
|
||||
// filled in by the Miler who is standing at it. A customer who chose
|
||||
// the full form is choosing to save them that step, so the row is
|
||||
// here, it is outstanding until it is answered, and the button waits
|
||||
|
||||
@@ -87,7 +87,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
// 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.
|
||||
// this path over BOOK NOW.
|
||||
if (detailed) {
|
||||
for (final group in app.draftDestinations) {
|
||||
if (!mounted) return;
|
||||
@@ -730,7 +730,7 @@ class _PickupLine extends StatelessWidget {
|
||||
///
|
||||
/// ── Why the app has two ways to start and only one of them is a sphere ──
|
||||
///
|
||||
/// One Touch is the product: a state, a district, a window, and the Miler
|
||||
/// BOOK NOW 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.
|
||||
///
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import '../../data/models.dart';
|
||||
import '../../state/app_scope.dart';
|
||||
import '../tokens.dart';
|
||||
import '../widgets/pieces.dart';
|
||||
import '../widgets/inputs.dart';
|
||||
import '../widgets/states.dart';
|
||||
import 'order_row.dart';
|
||||
@@ -183,20 +183,22 @@ class _OrdersScreenState extends State<OrdersScreen> {
|
||||
///
|
||||
/// Scrollable because a fourth state is one backend release away and a row of
|
||||
/// four fixed chips at 1.8x text has nowhere to go. Each carries its count in
|
||||
/// the same pill, because "Active" and "4" are one fact.
|
||||
/// Active / Past / Cancelled, as a segmented control.
|
||||
///
|
||||
/// ── Chips → text tabs → a segment ──
|
||||
/// ── Chips → text tabs → a segment → the app's own segment ──
|
||||
///
|
||||
/// The chips were three containers, one of them filled solid black, sitting
|
||||
/// above a list of cards — the loudest object on the screen was the filter.
|
||||
/// Plain text tabs with a rule fixed that and left the row looking like a
|
||||
/// heading rather than a control.
|
||||
/// The chips were three containers, one filled solid black, sitting above a
|
||||
/// list of cards — the loudest object on the screen was the filter. Plain text
|
||||
/// tabs with a rule fixed that and left the row looking like a heading rather
|
||||
/// than a control. A groove with a raised chip in it was the right shape.
|
||||
///
|
||||
/// A groove with a raised chip in it is the shape that is unambiguously a
|
||||
/// control and still quiet: white lifted out of a warm bed, ink type, and the
|
||||
/// count beside the label because that is the reason anyone looks at the two
|
||||
/// tabs they are not on.
|
||||
/// This was then a *second* implementation of that shape: its own `_Tab`, a
|
||||
/// pill radius, 3pt of padding and the count folded into the label's text —
|
||||
/// beside the pickup window's day switcher, which is [DmChoiceChip] in a
|
||||
/// rounded groove with 4pt of padding and the count in a bubble. Two controls
|
||||
/// doing one job, drifting apart a padding value at a time.
|
||||
///
|
||||
/// It is the same control now. The count was already a parameter of it.
|
||||
class _Filter extends StatelessWidget {
|
||||
const _Filter({
|
||||
required this.labels,
|
||||
@@ -213,88 +215,28 @@ class _Filter extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: DmColors.groove,
|
||||
borderRadius: DmRadius.all(DmRadius.pill),
|
||||
borderRadius: DmRadius.all(DmRadius.md),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < labels.length; i++)
|
||||
Flexible(
|
||||
child: _Tab(
|
||||
label: labels[i],
|
||||
count: counts[i],
|
||||
selected: i == index,
|
||||
onTap: () => onChanged(i),
|
||||
),
|
||||
for (var i = 0; i < labels.length; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 4),
|
||||
DmChoiceChip(
|
||||
label: labels[i],
|
||||
// Zero is not a figure worth a bubble — an empty tab says so by
|
||||
// being empty when you open it.
|
||||
count: (counts[i] ?? 0) > 0 ? counts[i] : null,
|
||||
selected: i == index,
|
||||
expand: true,
|
||||
onTap: () => onChanged(i),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Tab extends StatelessWidget {
|
||||
const _Tab({
|
||||
required this.label,
|
||||
required this.count,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int? count;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
onTap();
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: DmMotion.base,
|
||||
curve: DmMotion.ease,
|
||||
constraints: const BoxConstraints(minHeight: 34),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? DmColors.surface : Colors.transparent,
|
||||
borderRadius: DmRadius.all(DmRadius.pill),
|
||||
boxShadow: selected ? DmShadow.card : null,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: label),
|
||||
if (count != null && count! > 0)
|
||||
TextSpan(
|
||||
text: ' ($count)',
|
||||
style: DmText.small.copyWith(
|
||||
fontSize: 12,
|
||||
color: selected ? DmColors.ink3 : DmColors.ink4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: DmText.label.copyWith(
|
||||
fontSize: 13,
|
||||
color: selected ? DmColors.ink : DmColors.ink3,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import '../../widgets/inputs.dart';
|
||||
///
|
||||
/// Every field on it is optional to the contract and every one of them is
|
||||
/// something the Miler can fill in at the door. That is the whole argument for
|
||||
/// One Touch. A customer who chooses the full form is choosing to save the
|
||||
/// BOOK NOW. A customer who chooses the full form is choosing to save the
|
||||
/// Miler that step, so the form has to be quick enough to be worth choosing:
|
||||
/// one surface, five fields, one button.
|
||||
///
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../../widgets/feedback.dart';
|
||||
import 'destination_sheet.dart';
|
||||
import 'window_sheet.dart';
|
||||
|
||||
/// What ONE TOUCH asks: where, and when — on one surface.
|
||||
/// What BOOK NOW asks: where, and when — on one surface.
|
||||
///
|
||||
/// ── What this replaced ──
|
||||
///
|
||||
|
||||
@@ -60,7 +60,14 @@ class DmBookOrb extends StatefulWidget {
|
||||
const DmBookOrb({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
this.label = 'ONE TOUCH',
|
||||
// ── "BOOK NOW", not "ONE TOUCH" ──
|
||||
//
|
||||
// The old word named the *mechanism* — one gesture, no form — which is a
|
||||
// thing the product team knows and a customer has to be taught. Nobody
|
||||
// arrives at this screen wanting a touch; they want a parcel collected.
|
||||
// "BOOK NOW" says what pressing it does, and the caption underneath still
|
||||
// carries what makes it different from the form below.
|
||||
this.label = 'BOOK NOW',
|
||||
this.caption,
|
||||
this.held = false,
|
||||
this.field = maxField,
|
||||
|
||||
@@ -131,7 +131,11 @@ class DmChoiceChip extends StatelessWidget {
|
||||
duration: DmMotion.base,
|
||||
curve: DmMotion.ease,
|
||||
constraints: const BoxConstraints(minHeight: 40),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||
// 9, not 12. Three of these split a 390pt phone between them on the
|
||||
// Orders filter, and at 12 the widest label truncated to "Cancell…".
|
||||
// The pickup window's day switcher has two chips and acres of room, so
|
||||
// it loses nothing.
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 9),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? DmColors.surface : Colors.transparent,
|
||||
@@ -155,7 +159,7 @@ class DmChoiceChip extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (count != null) ...[
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: 5),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
Reference in New Issue
Block a user