175 lines
7.9 KiB
Dart
175 lines
7.9 KiB
Dart
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';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// FOUR LAYERS, AND WHAT EACH ONE MEANS
|
|
///
|
|
/// The app had **five** near-identical light surfaces — `pureSurface`,
|
|
/// `surface`, `neutralLight`, `cardSurface`, `daylightSurface` — and measured
|
|
/// against the page ground, three of them were invisible:
|
|
///
|
|
/// ```
|
|
/// neutralLight #F2F4F8 1.021 : 1 ← indistinguishable
|
|
/// cardSurface #EDEFF4 1.023 : 1 ← indistinguishable
|
|
/// surface #FCF9F8 1.073 : 1 ← and warm, unlike everything else
|
|
/// pureSurface #FFFFFF 1.124 : 1 ← the one that mattered, and too close
|
|
/// ```
|
|
///
|
|
/// Five tokens doing the work of two is why the app looked flat *and* why
|
|
/// individual screens grew their own borders and shadows: a surface that will
|
|
/// not separate on its own has to be outlined, and once one screen outlines
|
|
/// something every screen does, in its own way.
|
|
///
|
|
/// The fix was not more containers. It was giving the ground somewhere to be:
|
|
/// the canvas moved down to #DEE3EA, white now separates at 1.290:1, and the
|
|
/// layers below are named for **what a surface means** rather than for what
|
|
/// colour it happens to be.
|
|
///
|
|
/// ── The ladder ──
|
|
///
|
|
/// ```
|
|
/// 0 canvas the page itself. Never white.
|
|
/// 1 working a region the rider reads or works in. White, no border.
|
|
/// 2 raised something that acts: a command bar, a live card. Shadow.
|
|
/// 3 floating over everything, with a scrim: sheets, dialogs.
|
|
/// ```
|
|
///
|
|
/// A container is justified when it is one of these layers. It is not
|
|
/// justified because the content inside it is a group — grouping is done by
|
|
/// whitespace, type and the timeline, which is why a route reads as a route
|
|
/// without a box around each stop.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
abstract final class MilerSurface {
|
|
/// **Layer 0.** The page. Every scaffold in the app sits on this.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// ── #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.
|
|
///
|
|
/// No border. At 1.290:1 against [canvas] it separates on its own, and an
|
|
/// outline on top of a step that already works is a line to read past.
|
|
static const Color working = ColorConstants.pureSurface;
|
|
|
|
/// **Layer 2.** Something that *acts*: the floating command bar, the live
|
|
/// card, a control that owns the bottom of the screen.
|
|
///
|
|
/// Same fill as [working] — the difference is [raisedShadow], because what
|
|
/// separates an action surface from a reading surface is that it appears to
|
|
/// sit above the page rather than in it.
|
|
static const Color raised = ColorConstants.pureSurface;
|
|
|
|
/// **Layer 3.** Over everything, with a scrim beneath: modal sheets and
|
|
/// dialogs. Painted by `milerGlassSheet`; this names the layer.
|
|
static const Color floating = ColorConstants.glassCard;
|
|
|
|
/// A quiet tonal block *inside* layer 1 — a fact grid, a folded group.
|
|
///
|
|
/// The only nesting the system allows, and only when the block is a
|
|
/// different **kind** of thing from what surrounds it. Never for grouping.
|
|
static const Color inset = ColorConstants.neutralLight;
|
|
|
|
/// Layer 2's lift. One shadow, used everywhere something is raised, so
|
|
/// "floating" looks the same on every screen.
|
|
static const List<BoxShadow> raisedShadow = DesignConstants.shadowSm;
|
|
|
|
/// Layer 3's lift.
|
|
static const List<BoxShadow> floatingShadow = DesignConstants.shadowFloat;
|
|
|
|
/// The dim behind a layer-3 surface. Separation comes from the scrim first
|
|
/// and the shadow second — a sheet that needs a heavy shadow to be legible
|
|
/// is a sheet whose scrim is too weak.
|
|
static const Color scrim = Color(0x66000000);
|
|
|
|
/// 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.
|
|
///
|
|
/// 14 → 20, the brief's figure for the padding inside a card.
|
|
static const double panelPad = 16;
|
|
}
|
|
|
|
/// ── Layer 1, as a widget ──
|
|
///
|
|
/// The ladder above says a working region is *white, standing on the canvas,
|
|
/// with no border*. Home was the screen that proved why that has to be a
|
|
/// widget rather than a paragraph: its sheet was painted with the canvas and
|
|
/// its content placed straight onto it, so the whole page came out one flat
|
|
/// grey with nothing white anywhere on it. Every rule was followed except the
|
|
/// one that does the work — something has to actually stand on the ground.
|
|
///
|
|
/// No border and no shadow, deliberately. White separates from the canvas at
|
|
/// 1.290 : 1, which is above the ~1.25 : 1 where two tones start reading as
|
|
/// two surfaces, so an outline here would be a line to read past rather than
|
|
/// a line that says anything.
|
|
class MilerPanel extends StatelessWidget {
|
|
final Widget child;
|
|
|
|
/// Space below this panel before the next one. The canvas showing through
|
|
/// the gap is what makes them read as separate regions.
|
|
final double gap;
|
|
|
|
/// Set false where the child already owns its own horizontal padding — a
|
|
/// list whose rows need to bleed to the panel edge, for instance.
|
|
final bool padded;
|
|
|
|
const MilerPanel({
|
|
super.key,
|
|
required this.child,
|
|
this.gap = 10,
|
|
this.padded = true,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
margin: EdgeInsets.fromLTRB(
|
|
MilerSurface.panelGutter,
|
|
0,
|
|
MilerSurface.panelGutter,
|
|
gap,
|
|
),
|
|
padding: padded
|
|
? EdgeInsets.symmetric(
|
|
horizontal: MilerSurface.panelPad,
|
|
vertical: MilerSurface.panelPad,
|
|
)
|
|
: EdgeInsets.zero,
|
|
// ── 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,
|
|
);
|
|
}
|
|
}
|