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

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