production

This commit is contained in:
2026-08-28 11:13:15 +05:30
parent d7348e253f
commit 5723d373b2
162 changed files with 17924 additions and 7026 deletions

View File

@@ -4,12 +4,14 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:shimmer/shimmer.dart';
import 'miler_app_bar.dart' show milerGlassSurface;
import 'package:miler/Models/stop_status.dart';
import 'package:miler/views/helpers/constants/miler_type.dart';
import 'package:miler/views/helpers/widgets/miler_app_bar.dart'
show milerBrandSurface;
import '../constants/Colorconstants.dart';
import '../constants/design_constants.dart';
@@ -24,6 +26,96 @@ import '../constants/Font_constant.dart';
/// every screen stays visually consistent.
/// ───────────────────────────────────────────────────────────────────────────
/// ─────────────────────────────────────────────────────────────────────────
/// A BAND — a section that runs the full width of the phone
///
/// The alternative, and what most of this app still does, is a card: a
/// surface inset from both edges by a page margin, closed with a radius and
/// usually an outline. That shape is right when a screen shows a *list of
/// objects* — a stop, a job, a booking — because the margin is what says
/// "these are separate things".
///
/// It is wrong for a screen that is one long thing: a form, a review, a
/// receipt. There the margin buys nothing and costs twice. It costs width —
/// 32 to 40 points off a 390pt phone, spent on two strips of empty ground
/// beside text that is mostly addresses — and it costs a second boundary,
/// because the gap between two cards already separates them and the outline
/// says it again.
///
/// A band takes the other trade: it runs edge to edge, the page's ground
/// showing between one band and the next is the only separator, and the
/// margin moves *inside* as padding, where it is air around words rather than
/// air around a box. Same breathing room at the left of a line, ~40pt more
/// line to breathe in.
///
/// ── The rail ──
///
/// A band cannot carry state in an outline it does not have, so state that has
/// to read at arm's length goes in the fill, and a finished band adds [rail] —
/// a short accent stripe down its leading edge. It is the one mark that stays
/// legible when a phone is at arm's length in sun, and it is cheap: three
/// points of colour rather than a whole tinted border.
/// ─────────────────────────────────────────────────────────────────────────
class MilerBand extends StatelessWidget {
final Widget child;
/// The band's ground. White unless the band's state says otherwise, and an
/// **opaque** colour either way — see [ColorConstants.tint] for why a
/// translucent wash is the wrong tool for a surface.
final Color? color;
/// An accent stripe down the leading edge, for a band whose state has to
/// read without being examined. Null draws no stripe and no inset.
final Color? rail;
/// Overrides the standard inset. Pass only where the child owns its own
/// horizontal padding — a row that has to bleed to the band's edge.
final EdgeInsets? padding;
const MilerBand({
super.key,
required this.child,
this.color,
this.rail,
this.padding,
});
/// The inset a band keeps around its content.
///
/// One number for the whole app. With the bands running edge to edge, their
/// padding is the only thing left aligning the page's left margin — two
/// bands a couple of points apart is exactly how a column of text stops
/// looking like a column.
static double get pad => 20.w;
/// The gap of page ground between one band and the next. This is the only
/// separator a band has, so it is a real gap rather than a hairline.
static double get gap => 10.h;
@override
Widget build(BuildContext context) {
// Animated because [color] and [rail] carry state: a section going from
// outstanding to finished should be seen to change, and a hard cut on a
// full-width fill reads as the screen having been replaced. Bands whose
// state never moves pay nothing for this.
return AnimatedContainer(
duration: DesignConstants.motionState,
curve: Curves.easeOut,
width: double.infinity,
padding: padding ?? EdgeInsets.symmetric(horizontal: pad, vertical: 16.h),
decoration: BoxDecoration(
color: color ?? ColorConstants.pureSurface,
border: rail == null
? null
: Border(
left: BorderSide(color: rail!, width: 3.w),
),
),
child: child,
);
}
}
/// White rounded card with the design-system shadow.
class AppCard extends StatelessWidget {
final Widget child;
@@ -792,6 +884,12 @@ PreferredSizeWidget milerPageBar(
/// True for a screen that genuinely *dismisses* rather than pops — a sheet
/// presented modally. Only this gets the cross.
bool isModal = false,
/// The colour of the surface directly under the bar, so the curve at its
/// bottom corners opens onto the page rather than onto more brand.
/// Transparent reveals the Scaffold's own background, which is right for
/// every pushed page here. See [milerBrandSurface].
Color pageColor = Colors.transparent,
}) {
final IconData backIcon = isModal ? LucideIcons.x : LucideIcons.arrowLeft;
final bool hasControlRow = onBack != null || (actions?.isNotEmpty ?? false);
@@ -805,10 +903,15 @@ PreferredSizeWidget milerPageBar(
// the phone AND the colour of the status-bar glyphs, which reads as leaving
// the app rather than opening a page inside it.
//
// Solid [ColorConstants.primary], and everything on it inverts. The wash
// survives as `milerGlassSurface()` for the sheets, which sit over content
// and still want it.
backgroundColor: ColorConstants.primary,
// One ground, and everything on it inverts. The wash and the blur it was
// painted behind are both gone; what the bar wears now is
// [kBrandGradient], with its bottom corners taken off.
//
// **Transparent**, and the surface does all the painting: an opaque fill
// behind a rounded surface fills in the two corners the curve exists to
// open. See [milerBrandSurface].
backgroundColor: Colors.transparent,
flexibleSpace: milerBrandSurface(pageColor: pageColor),
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.white,
iconTheme: const IconThemeData(color: Colors.white),
@@ -937,17 +1040,28 @@ Duration staggerDelay(int index, {int stepMs = 45, int maxMs = 320}) =>
/// ─────────────────────────────────────────────────────────────────────────
/// GLASS CARD — the one surface every list card in the app is drawn on.
///
/// A translucent pane, a soft wide shadow, and **no border**.
/// An opaque surface, a hairline, and a soft wide shadow.
///
/// ── Why no border ──
/// ── The name is older than the material ──
///
/// A border, a fill and a shadow are three devices doing one job, and the
/// border is the one that competes with the content: six bordered rectangles
/// down a phone screen read as a form to be filled in, not a route to be
/// ridden. Every large logistics app the rider already uses — Uber, Swiggy,
/// Amazon Shopper — separates list cards with elevation alone, and it survives
/// what a hairline does not: a scratched screen in direct sun, where a 1px
/// #E2E8F0 line is simply not there.
/// It was a translucent pane with no border at all, and both halves of that
/// have been undone. The pane went opaque because a translucent card takes its
/// contrast from whatever happens to be behind it — the same address is crisp
/// over the canvas and grey over a map — and the hairline came back because on
/// the budget LCD this app ships to, a shadow crushes to nothing and a
/// borderless card reads as printed onto the page.
///
/// The name is kept because seven call sites use it and a rename buys nothing.
///
/// ── What the border is and is not ──
///
/// The risk a border carries is that it competes with the content: six heavily
/// bordered rectangles down a phone screen read as a form to be filled in, not
/// a route to be ridden. So it is a *hairline* and it is nearly the card's own
/// colour — it is not asked to separate the card from the page, which is the
/// shadow's job and the canvas step's. It is asked to finish an edge that the
/// shadow has already softened, and to be the thing still standing when the
/// shadow is crushed away by sunlight.
///
/// ── Why one widget rather than a decoration constant ──
///
@@ -969,8 +1083,8 @@ class GlassCard extends StatelessWidget {
final String? semanticLabel;
final String? semanticHint;
/// A wash over the pane, for the one card on a screen that is *live*.
/// Deliberately a whisper — see [ColorConstants.glassCardLive].
/// The card's fill, for the one card on a screen that is *live*. Deliberately
/// a whisper — see [ColorConstants.glassCardLive].
final Color? tint;
/// Dims the whole card without changing a single colour inside it, for a
@@ -998,31 +1112,26 @@ class GlassCard extends StatelessWidget {
margin: margin,
padding: padding,
decoration: BoxDecoration(
color: ColorConstants.glassCard,
borderRadius: BorderRadius.circular(radius),
// ── The rim is what a budget LCD can actually render ──
// ── The tint replaces the fill, it does not sit over it ──
//
// On a calibrated panel the slate shadow separates the pane from the
// It used to be a translucent wash laid *inside* the card, on the
// reasoning that a live card should be the same glass as everything
// else, warmed. With opaque surfaces that overlay would cover the
// content rather than warm it, so the live card is simply a card with a
// different fill — which is what it always looked like anyway.
color: tint ?? ColorConstants.glassCard,
borderRadius: BorderRadius.circular(radius),
// ── The hairline is what a budget LCD can actually render ──
//
// On a calibrated panel the shadow alone separates the card from the
// ground; on the LCD this app ships to, shadows crush to nothing and
// the cards read as printed on the page — reported twice from the
// device. The one-pixel rim is the same trick the frosted sheets use
// ([milerGlassSheet]'s glassRim): a line of light on the curve costs
// nothing at any brightness and says "edge" where blur cannot.
// device. A one-pixel edge costs nothing at any brightness and says
// "edge" where a blur cannot.
border: Border.all(color: ColorConstants.borderSubtle),
boxShadow: DesignConstants.shadowGlass,
),
// The tint rides *inside* the pane rather than replacing it, so a live
// card is the same glass everything else is, warmed — not a different
// material.
child: tint == null
? child
: DecoratedBox(
decoration: BoxDecoration(
color: tint,
borderRadius: BorderRadius.circular(radius),
),
child: child,
),
child: child,
);
final tappable = onTap == null