import 'package:flutter/widgets.dart'; import 'package:miler/views/helpers/constants/Colorconstants.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. static const Color canvas = ColorConstants.daylightSurface; /// **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 raisedShadow = DesignConstants.shadowSm; /// Layer 3's lift. static const List 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. static const double panelGutter = 12; /// The breathing room *inside* a layer-1 panel. static const double panelPad = 14; } /// ── 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, decoration: BoxDecoration( color: MilerSurface.working, borderRadius: BorderRadius.circular(DesignConstants.radiusXl), ), child: child, ); } }