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:
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user