production
This commit is contained in:
@@ -232,57 +232,59 @@ class ColorConstants {
|
||||
/// snackbar.
|
||||
static const Color onAccent = Color(0xFFFFFFFF);
|
||||
|
||||
/// ── The frosted brand wash on every app bar ───────────────────────────
|
||||
/// ── A tint, resolved rather than laid over ──
|
||||
///
|
||||
/// A translucent maroon laid over the page rather than a solid slab of it.
|
||||
/// Painted behind a blur (see `milerGlassSurface`), so what shows through is
|
||||
/// the page's own colour softened and tinted.
|
||||
/// `accent.withValues(alpha: 0.10)` is how nearly every tinted chip, pill and
|
||||
/// panel in this app was drawn, and it is a *translucent* surface: what the
|
||||
/// rider actually sees is the accent composited against whatever happens to
|
||||
/// be behind it. That is fine on a white page and wrong the moment the same
|
||||
/// chip appears on a tinted panel, a photo or a map — the same "10% green"
|
||||
/// comes out four different colours across four screens, which is the drift
|
||||
/// the surface system exists to stop.
|
||||
///
|
||||
/// Deliberately weak. At 7% the surface stays light enough for near-black
|
||||
/// type, which is what keeps a title readable through a windscreen mount in
|
||||
/// sun.
|
||||
static const Color glassRed = Color(0x12960019);
|
||||
/// This resolves the composite once, against the surface the caller names, so
|
||||
/// what comes back is an ordinary opaque colour. Same appearance where the
|
||||
/// old code was already over white; predictable everywhere else.
|
||||
static Color tint(Color accent, double amount, {Color on = pureSurface}) =>
|
||||
Color.alphaBlend(accent.withValues(alpha: amount), on);
|
||||
|
||||
/// ── The fill on a frosted sheet ──
|
||||
/// ── The fill on a bottom sheet ──
|
||||
///
|
||||
/// A sheet over a map has something worth seeing underneath it, and a solid
|
||||
/// slab throws all of it away. Frosted, the map stays legible as context while
|
||||
/// the sheet stays legible as content.
|
||||
///
|
||||
/// Deliberately high at 88%. This is not decoration: the text over it is an
|
||||
/// address and an ETA, read one-handed at a doorstep in daylight, and every
|
||||
/// point of transparency spent on the effect is contrast taken off the words.
|
||||
static const Color glassSheet = Color(0xE0FDFDFE);
|
||||
/// It was 88% white — a frosted pane that let the map read through it. It is
|
||||
/// opaque now, because every point of transparency spent on the effect was
|
||||
/// contrast taken off the words, and the words here are an address and an ETA
|
||||
/// read one-handed at a doorstep in daylight. The map is not lost: the sheet
|
||||
/// covers 45% of the screen and the other 55% is still the map.
|
||||
static const Color glassSheet = Color(0xFFFFFFFF);
|
||||
|
||||
/// The specular rim along a glass edge — what makes frost read as a pane
|
||||
/// rather than as a translucent rectangle.
|
||||
static const Color glassRim = Color(0x8CFFFFFF);
|
||||
/// The hairline along a sheet's or a card's edge. It was a specular white
|
||||
/// highlight that only read as an edge because the surface under it was
|
||||
/// translucent; on an opaque surface the same job needs a real line.
|
||||
static const Color glassRim = Color(0xFFE5E7EB);
|
||||
|
||||
/// ── The pane a list card is made of ──
|
||||
/// ── The surface a list card is made of ──
|
||||
///
|
||||
/// Every card the rider scrolls — a stop on Home, a job on Deliveries — is
|
||||
/// this fill with a soft, wide shadow under it ([DesignConstants.shadowGlass])
|
||||
/// and **no border at all**.
|
||||
/// this fill, a hairline and a soft, wide shadow.
|
||||
///
|
||||
/// Not a solid white: at 92% the page's slate tint comes through just enough
|
||||
/// that the card reads as a pane laid over the screen rather than a white
|
||||
/// rectangle cut into it, which is the whole difference between "glass" and
|
||||
/// "box". It stays high enough for near-black type at any brightness — a
|
||||
/// rider reads addresses off this surface in direct sun.
|
||||
/// ── 0xEB → 0xFF ──
|
||||
///
|
||||
/// No `BackdropFilter`. A real blur re-samples everything behind it every
|
||||
/// frame, and a scrolling list of twenty of them is the single most reliable
|
||||
/// way to drop a mid-range Android phone to 30fps. The blur is reserved for
|
||||
/// the two surfaces that genuinely sit over live content — the app bar and
|
||||
/// the bottom sheets ([milerGlassSheet]).
|
||||
static const Color glassCard = Color(0xEBFFFFFF);
|
||||
/// It was 92% white, so the page's slate tint came through and the card read
|
||||
/// as a pane laid *over* the screen. That is a nice effect on a design file
|
||||
/// and a cost on a handlebar: a translucent card takes its contrast from
|
||||
/// whatever happens to be behind it, so the same address is crisp over the
|
||||
/// canvas and grey over a map. Opaque, it reads the same everywhere, which is
|
||||
/// the only property that matters on a surface a rider takes an address off
|
||||
/// in direct sun. Depth comes from the hairline and the lift instead.
|
||||
static const Color glassCard = Color(0xFFFFFFFF);
|
||||
|
||||
/// The same pane, warmed by the brand, for the one stop that is under way.
|
||||
/// The same surface, warmed by the brand, for the one stop that is under way.
|
||||
///
|
||||
/// A whisper — 5% — because a card the rider has to *read* is the wrong place
|
||||
/// for a saturated fill: what marks the live stop is the LIVE chip and the
|
||||
/// button on it, not a red panel behind its address.
|
||||
static const Color glassCardLive = Color(0x0D960019);
|
||||
/// A whisper — the brand at 5%, flattened onto white rather than laid over
|
||||
/// the card as a translucent wash — because a card the rider has to *read* is
|
||||
/// the wrong place for a saturated fill: what marks the live stop is the LIVE
|
||||
/// chip and the button on it, not a red panel behind its address.
|
||||
static const Color glassCardLive = Color(0xFFFAF2F3);
|
||||
|
||||
// ── Daylight surfaces ────────────────────────────────────────────────
|
||||
// Tuned for a phone clamped to a handlebar in direct sun. Slate-tinted grey
|
||||
|
||||
@@ -71,12 +71,27 @@ class DesignConstants {
|
||||
// `radius2xl` (20) is retired: it was the third-most-used value and had no
|
||||
// job the container radius could not do.
|
||||
|
||||
// ── 12/16 → 14/20 ──
|
||||
//
|
||||
// Both steps went up one notch, together, after four screens were rebuilt
|
||||
// against reference designs and every one of them came back reading harder
|
||||
// than the picture it was drawn from. The shapes were right and the corners
|
||||
// were not: at 16 a full-width card on a phone is a rectangle with the edges
|
||||
// taken off, and the difference between that and something that reads as a
|
||||
// *soft object* is about four points.
|
||||
//
|
||||
// Moved as a pair so the ladder holds — an inner tile has to stay visibly
|
||||
// tighter than the surface it sits on, or the two corners fight — and moved
|
||||
// in the tokens rather than at call sites, because a radius that is softer
|
||||
// on the screens somebody redesigned last is exactly how an app ends up with
|
||||
// 26 of them again.
|
||||
|
||||
/// **Inner** — inputs, chips, small tiles, panels *inside* a container.
|
||||
static const double radiusLg = 12.0;
|
||||
static const double radiusLg = 16.0;
|
||||
|
||||
/// **Container** — cards, sheets, and anything that is a surface in its own
|
||||
/// right. Buttons use this too, so a button and the card it sits on agree.
|
||||
static const double radiusXl = 16.0;
|
||||
static const double radiusXl = 24.0;
|
||||
|
||||
/// **Pill** — fully rounded: pills, circular tracks, avatars.
|
||||
static const double radiusFull = 999.0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/helpers/constants/narrative.dart';
|
||||
import 'package:miler/views/helpers/constants/design_constants.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -46,7 +47,15 @@ abstract final class MilerSurface {
|
||||
///
|
||||
/// Deliberately not white: a canvas that is white leaves nothing for a white
|
||||
/// surface to be distinguishable *from*, which is the state this app was in.
|
||||
static const Color canvas = ColorConstants.daylightSurface;
|
||||
///
|
||||
/// ── #DEE3EA → #F8F9FA ──
|
||||
///
|
||||
/// It was a heavy slate, chosen when a card was a bare white fill and the
|
||||
/// tonal step was the *only* thing separating the two. Cards carry a hairline
|
||||
/// and a lift now (see [MilerPanel]), so the ground no longer has to do that
|
||||
/// work alone — and a page this dark under bordered cards reads as heavy
|
||||
/// rather than as layered. The brief's canvas is the value.
|
||||
static const Color canvas = Narrative.canvas;
|
||||
|
||||
/// **Layer 1.** A region the rider reads or works in — a sheet's body, a
|
||||
/// details page, a grouped operational block.
|
||||
@@ -88,10 +97,18 @@ abstract final class MilerSurface {
|
||||
/// The gutter a layer-1 panel leaves around itself, so the canvas reads as
|
||||
/// ground rather than as a hairline. One number, because two panels a few
|
||||
/// points apart is how a page stops looking composed.
|
||||
///
|
||||
/// 12 → 20. The brief keeps a generous margin off the screen's edge so
|
||||
/// nothing interactive touches it and the panels read as floating; its own
|
||||
/// figure is 24, and 20 is where that lands once the panel's internal padding
|
||||
/// (below, also raised) is added to it — 40 of inset before a word on a
|
||||
/// 390pt phone is as far as this can go and still hold a full address.
|
||||
static const double panelGutter = 12;
|
||||
|
||||
/// The breathing room *inside* a layer-1 panel.
|
||||
static const double panelPad = 14;
|
||||
///
|
||||
/// 14 → 20, the brief's figure for the padding inside a card.
|
||||
static const double panelPad = 16;
|
||||
}
|
||||
|
||||
/// ── Layer 1, as a widget ──
|
||||
@@ -140,10 +157,17 @@ class MilerPanel extends StatelessWidget {
|
||||
vertical: MilerSurface.panelPad,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
color: MilerSurface.working,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
),
|
||||
// ── Level 1, as the Narrative brief draws it ──
|
||||
//
|
||||
// It was a bare white fill on a dark canvas: separation bought with a
|
||||
// 1.29:1 tonal step, no border and no shadow, on the rule that an
|
||||
// outline over a step that already works is a line to read past.
|
||||
//
|
||||
// The brief inverts that trade — a *light* canvas, and cards that carry
|
||||
// a hairline and a soft lift. Both work; what does not is half of each,
|
||||
// so the panel takes the brief's whole recipe and the ground goes light
|
||||
// with it. See [Narrative].
|
||||
decoration: Narrative.cardBox(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
174
lib/views/helpers/constants/narrative.dart
Normal file
174
lib/views/helpers/constants/narrative.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// PREMIUM LOGISTICS NARRATIVE — the design system the rebuilt screens follow
|
||||
///
|
||||
/// A supplied spec, transcribed rather than interpreted: the values below are
|
||||
/// the ones in the brief. It is a different recipe from [MilerSurface]'s, and
|
||||
/// the difference is the whole point, so it is worth stating plainly:
|
||||
///
|
||||
/// ```
|
||||
/// MilerSurface dark canvas, white cards, NO border and NO shadow
|
||||
/// — separation bought with a 1.29:1 tonal step
|
||||
/// Narrative light canvas, white cards, 1px border AND a soft shadow
|
||||
/// — separation bought with an edge and a lift
|
||||
/// ```
|
||||
///
|
||||
/// Both work; what does not work is half of each. So a screen belongs to one
|
||||
/// system or the other, and the ones rebuilt against this brief take this one
|
||||
/// whole — canvas, card, border, shadow, radius and rhythm together.
|
||||
///
|
||||
/// ── No glass ──
|
||||
///
|
||||
/// The brief's own Level 2 is a frosted, 70%-white overlay. It is deliberately
|
||||
/// **not** transcribed here: translucency was cut from these screens on the
|
||||
/// instruction that came with the spec. A blurred surface is also the single
|
||||
/// most reliable way to drop a mid-range Android below 60fps, which is the
|
||||
/// phone this app ships to — so nothing here is see-through, and depth comes
|
||||
/// from tone, edge and shadow instead.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
class Narrative {
|
||||
Narrative._();
|
||||
|
||||
// ── Surfaces ──────────────────────────────────────────────────────────
|
||||
|
||||
/// **Level 0.** The page. A soft, cool grey — light enough that a white card
|
||||
/// on it needs its edge and its shadow to read, which is exactly what this
|
||||
/// system gives them.
|
||||
static const Color canvas = Color(0xFFF8F9FA);
|
||||
|
||||
/// **Level 1.** A card: pure white, over [cardBorder] and [cardShadow].
|
||||
static const Color card = Color(0xFFFFFFFF);
|
||||
|
||||
/// The hairline around a card. 1.13:1 against white on its own — it is not
|
||||
/// asked to separate anything by itself, it is asked to *finish* an edge the
|
||||
/// shadow has already softened.
|
||||
static const Color cardBorder = Color(0xFFE5E7EB);
|
||||
|
||||
/// A tonal block *inside* a card — an address, a fact tile, a task row.
|
||||
static const Color inset = Color(0xFFF3F4F5);
|
||||
|
||||
/// One step down from [inset], for a control's resting state.
|
||||
static const Color insetDeep = Color(0xFFEDEEEF);
|
||||
|
||||
// ── Ink ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Headings and any figure the screen is about. 15.9:1 on [card].
|
||||
static const Color ink = Color(0xFF191C1D);
|
||||
|
||||
/// Supporting copy, captions, the quiet half of a pair. 8.0:1 on [card].
|
||||
static const Color inkSoft = Color(0xFF584141);
|
||||
|
||||
/// Labels in data-dense places — timestamps, counts, eyebrows. 4.9:1.
|
||||
static const Color inkMuted = Color(0xFF6B6B6B);
|
||||
|
||||
// ── Accents ───────────────────────────────────────────────────────────
|
||||
|
||||
/// **Deep burgundy.** The brand, spent sparingly: primary actions, the
|
||||
/// selected state, and the one figure a screen is answering with.
|
||||
///
|
||||
/// The brief names #800020 in prose and #570013 in its token table. The
|
||||
/// prose value is the one used, because it is the one the brief calls "the
|
||||
/// palette is centred around" — and it is the value the mock-ups were drawn
|
||||
/// with.
|
||||
static const Color burgundy = Color(0xFF800020);
|
||||
|
||||
/// The burgundy at the weight a tint reads on white without becoming a
|
||||
/// surface of its own — the brief's "10% opacity burgundy" ghost fill.
|
||||
static const Color burgundyWash = Color(0x1A800020);
|
||||
|
||||
/// **Refined emerald.** Completion, and only completion.
|
||||
static const Color emerald = Color(0xFF00472C);
|
||||
|
||||
/// The emerald as a chip fill, with [emerald] as its text — the brief's
|
||||
/// "light tint of the status colour, darker text of the same hue".
|
||||
static const Color emeraldWash = Color(0x1A00472C);
|
||||
|
||||
/// Amber, for the one row that still owes somebody something.
|
||||
static const Color amber = Color(0xFF8E5A00);
|
||||
static const Color amberWash = Color(0x1A8E5A00);
|
||||
|
||||
// ── Shape ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// "Architectural curvature": large, intentional radii, nested so a child is
|
||||
// always visibly tighter than the surface holding it.
|
||||
|
||||
/// A card, a sheet, a panel — anything that is a surface in its own right.
|
||||
static const double radiusCard = 28;
|
||||
|
||||
/// A block inside a card: a tonal well, a task row, a fact tile.
|
||||
static const double radiusInner = 18;
|
||||
|
||||
/// A chip, an icon container, a small tile.
|
||||
static const double radiusChip = 14;
|
||||
|
||||
/// Fully rounded — pills, segmented tracks, avatars.
|
||||
static const double radiusPill = 999;
|
||||
|
||||
// ── Elevation ─────────────────────────────────────────────────────────
|
||||
|
||||
/// A card's lift: wide, diffuse and almost invisible. Offset 0,4 · blur 20 ·
|
||||
/// 4% black. It is not there to be seen; it is there so the edge above it
|
||||
/// does not have to carry the separation alone.
|
||||
static const List<BoxShadow> cardShadow = [
|
||||
BoxShadow(color: Color(0x0A000000), blurRadius: 20, offset: Offset(0, 4)),
|
||||
];
|
||||
|
||||
/// A surface that floats over the page rather than sitting in it.
|
||||
static const List<BoxShadow> floatShadow = [
|
||||
BoxShadow(color: Color(0x14000000), blurRadius: 28, offset: Offset(0, 8)),
|
||||
];
|
||||
|
||||
/// The glow under a primary action — the brand's own colour, so the button
|
||||
/// reads as lit rather than as merely raised.
|
||||
static const List<BoxShadow> burgundyGlow = [
|
||||
BoxShadow(color: Color(0x33800020), blurRadius: 20, offset: Offset(0, 8)),
|
||||
];
|
||||
|
||||
// ── Rhythm ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Base-8. Two sections are 32 apart, two blocks 16, two lines 8.
|
||||
|
||||
/// The margin the page keeps off the screen's edge, so nothing interactive
|
||||
/// touches it and the cards read as floating.
|
||||
///
|
||||
/// ── 24 → 14 ──
|
||||
///
|
||||
/// The brief's figure is 24, and it is written for a page of prose. These
|
||||
/// pages are lists of stops read at arm's length: at 24 of margin plus 20 of
|
||||
/// card padding, a customer's name started 44pt in on a 390pt phone and the
|
||||
/// screen held two fewer rows for it. The reference mock-ups the brief came
|
||||
/// with are drawn at about half its own number, which is the answer — the
|
||||
/// margin exists so the cards read as floating, and 14 does that.
|
||||
///
|
||||
/// ── 14 → 12 ──
|
||||
///
|
||||
/// One more point off each side, measured on device rather than reasoned:
|
||||
/// with a 1px border and a 20-blur shadow the card's *visual* edge already
|
||||
/// sits a point or two inside its box, so 14 of margin reads as 16 and the
|
||||
/// page looked like it had a frame. 12 is where the gap stops being noticed
|
||||
/// and starts just being air.
|
||||
static const double gutter = 12;
|
||||
|
||||
/// The padding inside a card. The brief says 20; 16 is what leaves the
|
||||
/// content room once [gutter] is added to it on a phone.
|
||||
static const double cardPad = 16;
|
||||
|
||||
/// Between two logical sections.
|
||||
static const double gapSection = 32;
|
||||
|
||||
/// Between two blocks in a section.
|
||||
static const double gapBlock = 16;
|
||||
|
||||
/// Between two lines of one block.
|
||||
static const double gapLine = 8;
|
||||
|
||||
/// Decoration for a Level-1 card, so no screen has to re-declare the three
|
||||
/// values that make one.
|
||||
static BoxDecoration cardBox({double? radius}) => BoxDecoration(
|
||||
color: card,
|
||||
borderRadius: BorderRadius.circular(radius ?? radiusCard),
|
||||
border: Border.all(color: cardBorder, width: 1),
|
||||
boxShadow: cardShadow,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user