Files
doormile_customer_app/lib/ui/widgets/feedback.dart
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

188 lines
4.7 KiB
Dart

import 'package:flutter/material.dart';
import '../tokens.dart';
/// Dark pill toast, matching the prototype's transient confirmations.
class DmToast {
DmToast._();
static OverlayEntry? _entry;
static void show(BuildContext context, String message) {
final overlay = Overlay.maybeOf(context);
if (overlay == null) return;
_entry?.remove();
final entry = OverlayEntry(
builder: (context) => _ToastBody(message: message),
);
_entry = entry;
overlay.insert(entry);
Future<void>.delayed(const Duration(milliseconds: 2400), () {
if (_entry == entry) {
entry.remove();
_entry = null;
}
});
}
/// Called when the app is torn down in tests.
static void dismiss() {
_entry?.remove();
_entry = null;
}
}
class _ToastBody extends StatefulWidget {
const _ToastBody({required this.message});
final String message;
@override
State<_ToastBody> createState() => _ToastBodyState();
}
class _ToastBodyState extends State<_ToastBody>
with SingleTickerProviderStateMixin {
late final AnimationController _c = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 240),
)..forward();
@override
void dispose() {
_c.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bottom = MediaQuery.of(context).viewInsets.bottom;
return Positioned(
left: 20,
right: 20,
bottom: 96 + bottom,
child: FadeTransition(
opacity: _c,
child: SlideTransition(
position: Tween(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(parent: _c, curve: Curves.easeOutCubic)),
child: Center(
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 11,
),
decoration: BoxDecoration(
color: const Color(0xF0191716),
borderRadius: DmRadius.all(DmRadius.sm),
),
child: Text(
widget.message,
textAlign: TextAlign.center,
style: DmText.label.copyWith(
fontSize: 13.5,
color: Colors.white,
),
),
),
),
),
),
),
);
}
}
/// Bottom sheet with the prototype's grip, radius and padding.
Future<T?> showDmSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
bool isScrollControlled = true,
}) {
return showModalBottomSheet<T>(
context: context,
isScrollControlled: isScrollControlled,
backgroundColor: DmColors.canvas,
barrierColor: DmColors.scrim,
elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(DmRadius.xl)),
),
clipBehavior: Clip.antiAlias,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SafeArea(
top: false,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.86,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(
DmSpace.pad,
10,
DmSpace.pad,
20,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _SheetGrip(),
Flexible(child: builder(context)),
],
),
),
),
),
),
);
}
class _SheetGrip extends StatelessWidget {
const _SheetGrip();
@override
Widget build(BuildContext context) {
return Container(
width: 36,
height: 4,
margin: const EdgeInsets.only(top: 2, bottom: 18),
decoration: BoxDecoration(
color: DmColors.border,
borderRadius: DmRadius.all(2),
),
);
}
}
/// Sheet heading + supporting line.
class DmSheetHeader extends StatelessWidget {
const DmSheetHeader({super.key, required this.title, this.subtitle});
final String title;
final String? subtitle;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: DmText.sheetTitle),
if (subtitle != null) ...[
const SizedBox(height: 3),
Text(subtitle!, style: DmText.tiny),
],
const SizedBox(height: 18),
],
);
}
}