175 lines
8.4 KiB
Dart
175 lines
8.4 KiB
Dart
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,
|
|
);
|
|
}
|