Files
doormile_milderapp/lib/views/helpers/constants/narrative.dart
2026-08-28 11:13:15 +05:30

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,
);
}