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>
174 lines
5.0 KiB
Dart
174 lines
5.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
|
|
import '../tokens.dart';
|
|
import 'buttons.dart';
|
|
import 'cards.dart';
|
|
|
|
/// One labelled fact, optionally with an inline edit action.
|
|
class DmSummaryRow extends StatelessWidget {
|
|
const DmSummaryRow({
|
|
super.key,
|
|
required this.label,
|
|
required this.value,
|
|
this.secondary,
|
|
this.muted = false,
|
|
this.mono = false,
|
|
this.actionLabel,
|
|
this.onAction,
|
|
});
|
|
|
|
final String label;
|
|
final String value;
|
|
|
|
/// Smaller grey line under the value (street, postcode…).
|
|
final String? secondary;
|
|
|
|
/// Renders the value as placeholder text ("Not added — …").
|
|
final bool muted;
|
|
|
|
/// Sets the value in mono — references, phone numbers, pincodes.
|
|
final bool mono;
|
|
|
|
final String? actionLabel;
|
|
final VoidCallback? onAction;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DmCardCell(
|
|
padding: EdgeInsets.fromLTRB(16, 14, actionLabel == null ? 16 : 10, 14),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label.toUpperCase(), style: DmText.eyebrow),
|
|
const SizedBox(height: 5),
|
|
Text(
|
|
value,
|
|
style: muted
|
|
? DmText.small.copyWith(color: DmColors.ink3)
|
|
: mono
|
|
? DmText.mono.copyWith(fontSize: 15, color: DmColors.ink)
|
|
: DmText.cardTitle,
|
|
),
|
|
if (secondary != null) ...[
|
|
const SizedBox(height: 3),
|
|
Text(secondary!, style: DmText.small),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
if (actionLabel != null)
|
|
DmTextAction(label: actionLabel!, onPressed: onAction),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// White card that rules its children apart — Review, the confirmation summary,
|
|
/// and the tracking facts card.
|
|
///
|
|
/// A thin wrapper over [DmCard] so the many call sites that say "summary card"
|
|
/// keep saying it; new code can use [DmCard] directly and pick a tone.
|
|
class DmSummaryCard extends StatelessWidget {
|
|
const DmSummaryCard({super.key, required this.children, this.flat = false});
|
|
|
|
final List<Widget> children;
|
|
|
|
/// Sits on the canvas rather than lifting off it.
|
|
final bool flat;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DmCard(
|
|
tone: flat ? DmCardTone.sunken : DmCardTone.plain,
|
|
children: children,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Quiet explanatory note — the "this creates a pickup request" block.
|
|
class DmNote extends StatelessWidget {
|
|
const DmNote({super.key, required this.child, this.icon = LucideIcons.info});
|
|
|
|
final Widget child;
|
|
final IconData icon;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(14, 14, 15, 14),
|
|
decoration: BoxDecoration(
|
|
color: DmColors.surfaceAlt,
|
|
border: Border.all(color: DmColors.border),
|
|
borderRadius: DmRadius.all(DmRadius.md),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 17, color: DmColors.ink3),
|
|
const SizedBox(width: 11),
|
|
Expanded(child: child),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Confirmation strip — "Delivering to Coimbatore, Tamil Nadu".
|
|
///
|
|
/// Crimson, not green. The app has one accent, and a confirmation inside the
|
|
/// booking flow is a brand moment, not a system status: green here would be
|
|
/// the only green in the product and would read as a validation message.
|
|
class DmConfirmedStrip extends StatelessWidget {
|
|
const DmConfirmedStrip({super.key, required this.prefix, required this.value});
|
|
|
|
final String prefix;
|
|
final String value;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(13, 12, 15, 12),
|
|
decoration: BoxDecoration(
|
|
color: DmColors.brandSoft,
|
|
border: Border.all(color: DmColors.brandLine),
|
|
borderRadius: DmRadius.all(DmRadius.md),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 22,
|
|
height: 22,
|
|
alignment: Alignment.center,
|
|
decoration: const BoxDecoration(
|
|
color: DmColors.brand,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(LucideIcons.check, size: 13, color: Colors.white),
|
|
),
|
|
const SizedBox(width: 11),
|
|
Expanded(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
style: DmText.small.copyWith(color: DmColors.brandPress),
|
|
children: [
|
|
TextSpan(text: prefix),
|
|
TextSpan(
|
|
text: value,
|
|
style: const TextStyle(fontWeight: FontWeight.w700),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|