Miler rider app: surface system, visible design language, backend lifecycle

Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 05:40:35 +05:30
parent c8350563d9
commit d7348e253f
387 changed files with 80693 additions and 12272 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart' show LatLng;
@@ -148,7 +149,7 @@ class _MilerAttribution extends StatelessWidget {
'© OpenStreetMap',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w500,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
@@ -338,13 +339,13 @@ class MilerNumberPin extends StatelessWidget {
],
),
child: done
? const Icon(Icons.check_rounded, size: 16, color: Colors.white)
? const Icon(LucideIcons.check, size: 16, color: Colors.white)
: Text(
'$number',
style: const TextStyle(
fontSize: 14,
height: 1,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: Colors.white,
fontFamily: FontConstants.fontFamily,
),
@@ -445,3 +446,148 @@ double milerDistanceMetres(LatLng a, LatLng b) {
}
double _rad(double deg) => deg * math.pi / 180.0;
/// ─────────────────────────────────────────────────────────────────────────
/// ONE LEG, DRAWN HONESTLY
///
/// The small orientation maps on the stop detail and pickup preview sheets
/// each hand-rolled the same picture and both told the same two lies:
///
/// • **A straight line pretending to be the ride.** The polyline was just
/// `[rider, stop]`, drawn solid — geometry no road follows, in the same
/// stroke the real map screen uses for an actual route.
/// • **A journey with one end.** The camera parked on the destination at a
/// fixed zoom, so a rider 4 km out watched the line exit the frame into
/// nothing — a pin, a stroke, and no second point anywhere on screen.
///
/// This widget is that picture done once, properly. It asks OSRM for the road
/// (same [MilerRouter] the map screen trusts), fits the camera to BOTH ends
/// with room to breathe, and always draws both endpoints — the destination
/// pin and the rider's dot. While the route is in flight, or when the router
/// is unreachable, the leg shows as a DASHED direct line: dashes are the map
/// vocabulary for "as the crow flies", so the fallback stops impersonating a
/// road. Non-interactive by design — orientation, not navigation.
/// ─────────────────────────────────────────────────────────────────────────
class MilerLegMap extends StatefulWidget {
/// The destination — the one end a leg always has.
final LatLng to;
/// Where the rider is. Null draws the destination alone, centred.
final LatLng? from;
/// The destination pin's accent and glyph — pass the stop's own.
final Color accent;
final IconData icon;
const MilerLegMap({
super.key,
required this.to,
required this.accent,
required this.icon,
this.from,
});
@override
State<MilerLegMap> createState() => _MilerLegMapState();
}
class _MilerLegMapState extends State<MilerLegMap> {
final MapController _controller = MapController();
List<LatLng> _road = const [];
bool _ready = false;
@override
void initState() {
super.initState();
_fetchRoad();
}
@override
void didUpdateWidget(covariant MilerLegMap old) {
super.didUpdateWidget(old);
if (old.from != widget.from || old.to != widget.to) {
_road = const [];
_fetchRoad();
_fit();
}
}
Future<void> _fetchRoad() async {
final from = widget.from;
if (from == null) return;
final road = await MilerRouter.route(from, widget.to);
if (!mounted || road.isEmpty) return;
setState(() => _road = road);
// The road can bulge outside the straight-line bounds; refit around it.
_fit();
}
void _fit() {
if (!_ready) return;
final from = widget.from;
if (from == null) return;
final points = _road.isNotEmpty ? _road : [from, widget.to];
_controller.fitCamera(
CameraFit.coordinates(
coordinates: points,
padding: const EdgeInsets.fromLTRB(36, 44, 36, 30),
),
);
}
@override
Widget build(BuildContext context) {
final from = widget.from;
return IgnorePointer(
child: MilerMap(
controller: _controller,
initialCenter: from == null
? widget.to
: LatLng(
(widget.to.latitude + from.latitude) / 2,
(widget.to.longitude + from.longitude) / 2,
),
initialZoom: 13,
onReady: () {
_ready = true;
// After layout, when the viewport has a size to fit within.
WidgetsBinding.instance.addPostFrameCallback((_) => _fit());
},
polylines: [
if (from != null)
Polyline(
points: _road.isNotEmpty ? _road : [from, widget.to],
color: widget.accent,
strokeWidth: 4,
strokeCap: StrokeCap.round,
strokeJoin: StrokeJoin.round,
// Dashed while it is a bearing, solid once it is a road.
pattern: _road.isNotEmpty
? const StrokePattern.solid()
: StrokePattern.dashed(segments: const [14, 10]),
),
],
markers: [
milerMarker(
point: widget.to,
width: MilerPin.size + 8,
height: MilerPin.size + MilerPin.tail,
child: MilerPin(
color: widget.accent,
icon: widget.icon,
emphasised: true,
),
),
if (from != null)
milerMarker(
point: from,
width: MilerRiderDot.size,
height: MilerRiderDot.size,
child: const MilerRiderDot(),
),
],
),
);
}
}