Files
Thiru-tenext 0d66627c3c Replace the customer app with Doormile CX
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>
2026-09-15 16:07:33 +05:30

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),
),
],
),
),
),
],
),
);
}
}