import 'package:flutter/material.dart'; /// Miler Professional Design System Constants /// Centralized design tokens for spacing, radius, shadows, and layout /// ───────────────────────────────────────────────────────────────────────── /// SHARED RADII AND ELEVATION /// /// ── Why this used to be much bigger ── /// /// This class shipped 43 members: a nine-step spacing scale, eleven `EdgeInsets` /// presets, six radii, five shadows, a `cardDecoration` helper and three named /// border radii. An audit found **six of them used, 17 times in total.** The /// spacing scale had never been referenced once; every screen writes `16.w` or /// `12.h` inline, which is both more readable at the call site and correct for /// this app, because those need ScreenUtil's per-device scaling and a `const /// double` cannot have it. /// /// The 37 dead members were not harmless. A token nobody uses still has to be /// read, still looks authoritative, and is the reason someone reaches for /// `Colors.grey.shade100` instead — a design system that is mostly aspirational /// teaches people to ignore it. So the unused members are gone and what is left /// is only what the app actually reaches for. /// /// The audit metric that flagged this file was itself misleading, which is worth /// recording: 13 files import `design_constants.dart` and only 5 mention /// `DesignConstants`. The other 8 import it for [ButtonSizes], which lives in /// this same file and is used 72 times. Colocating a thriving class with a dead /// one hid both facts. /// /// **Adding to this class:** don't, until a second call site needs the value. /// Two uses make a token; one use is a local constant. /// ───────────────────────────────────────────────────────────────────────── class DesignConstants { DesignConstants._(); // ── Motion ──────────────────────────────────────────────────────────── // // The app was using 35 distinct durations. No shared vocabulary means every // transition is a fresh guess, and some of the guesses sat directly in the // rider's path between two stops — an 800ms entrance and several 500ms // cross-fades on screens he passes through 40 times a shift. // // Motion on a driver app is a tax paid all day. It earns its place only when // it explains a state change; anything else is time spent standing still. /// Press and release. Fast enough to feel like the surface responded rather /// than animated. static const Duration motionPress = Duration(milliseconds: 120); /// A state change in place: a chip selecting, a tick landing, a row opening. static const Duration motionState = Duration(milliseconds: 200); /// A page or a sheet arriving. The ceiling for anything on the critical path. static const Duration motionPage = Duration(milliseconds: 300); /// Celebration, and only celebration — once per completed route, never on a /// screen the rider is trying to get through. static const Duration motionCelebrate = Duration(milliseconds: 600); // ── Border radius ───────────────────────────────────────────────────── // // ── Three, because 26 communicated nothing ── // // An audit counted 26 distinct radii in use against the four declared here, // and found the most common value in the app was 14 — `ButtonSizes.radius`, // a *button* token, being applied to cards and sheets. Radius is one of the // strongest cues that two things belong to the same family; at 26 values it // says nothing, and two elements side by side at 12 and 14 read as a // rendering bug rather than a decision. // // `radius2xl` (20) is retired: it was the third-most-used value and had no // job the container radius could not do. // ── 12/16 → 14/20 ── // // Both steps went up one notch, together, after four screens were rebuilt // against reference designs and every one of them came back reading harder // than the picture it was drawn from. The shapes were right and the corners // were not: at 16 a full-width card on a phone is a rectangle with the edges // taken off, and the difference between that and something that reads as a // *soft object* is about four points. // // Moved as a pair so the ladder holds — an inner tile has to stay visibly // tighter than the surface it sits on, or the two corners fight — and moved // in the tokens rather than at call sites, because a radius that is softer // on the screens somebody redesigned last is exactly how an app ends up with // 26 of them again. /// **Inner** — inputs, chips, small tiles, panels *inside* a container. static const double radiusLg = 16.0; /// **Container** — cards, sheets, and anything that is a surface in its own /// right. Buttons use this too, so a button and the card it sits on agree. static const double radiusXl = 24.0; /// **Pill** — fully rounded: pills, circular tracks, avatars. static const double radiusFull = 999.0; // ── Elevation ───────────────────────────────────────────────────────── // // Only two survive, and both are used sparingly. Most cards in the app are // now borderless fills with no shadow at all — see // `ColorConstants.cardSurface` — so elevation is the exception, not the // default it once was. /// A card lifted just off the page. static const List shadowSm = [ BoxShadow(color: Color(0x1A000000), blurRadius: 4, offset: Offset(0, 2)), ]; /// Something floating over content: a menu, a dragged element. static const List shadowLg = [ BoxShadow(color: Color(0x2A000000), blurRadius: 12, offset: Offset(0, 8)), ]; /// A surface that floats **over** the list rather than in it: the selection /// bar, a snackbar. Wider and deeper than [shadowGlass] because it has to /// read as being on a different plane from the cards it covers — but still /// slate-tinted and still soft, because a hard drop shadow under a white bar /// reads as a grey stripe, which is exactly what it looked like. static const List shadowFloat = [ BoxShadow(color: Color(0x1F0F172A), blurRadius: 28, offset: Offset(0, 12)), BoxShadow(color: Color(0x140F172A), blurRadius: 4, offset: Offset(0, 2)), ]; /// ── What holds a borderless card off the page ── /// /// Two shadows, not one, and that is the whole trick: a wide soft pool gives /// the card its lift, and a tight near-opaque line under the bottom edge /// gives it a *defined* edge — without it a single blurred shadow reads as a /// smudge and the card looks unfinished rather than raised. /// /// Tinted with the page's slate rather than pure black: a neutral-black /// shadow on a blue-grey ground goes muddy, which is what makes a card look /// cheap next to the same card in a design file. static const List shadowGlass = [ BoxShadow(color: Color(0x0F0F172A), blurRadius: 18, offset: Offset(0, 6)), BoxShadow(color: Color(0x0A0F172A), blurRadius: 2, offset: Offset(0, 1)), ]; } /// ───────────────────────────────────────────────────────────────────────── /// BUTTON SIZING — one scale for the whole app. /// /// Buttons had drifted to a dozen different heights (44, 48, 52, 54, 56, 58) /// across the pickup flow, Home, auth and the sheets. On a screen the rider /// uses 30–50 times a day that reads as sloppiness, and worse, it breaks the /// muscle memory that lets him hit a control without looking: if "the green /// one at the bottom" is a different size on every screen, he has to aim. /// /// Three sizes, and only three. Every button in the app must pick one. /// ───────────────────────────────────────────────────────────────────────── class ButtonSizes { ButtonSizes._(); /// **Primary** — the one action a screen exists for. Full-width CTAs: /// Accept Trip, Start Pickup, Confirm, Navigate. static const double primary = 56.0; /// **Secondary** — supporting actions that sit beside or below a primary: /// Reject, Skip, Details, filter chips with a tap target. static const double secondary = 48.0; /// **Compact** — dense rows where several actions share a line, e.g. the /// per-stop Accept/Reject pair inside a trip card. Still above the 44dp /// accessibility floor. static const double compact = 44.0; /// Square icon-only buttons (call, overflow) match [primary] so they line up /// with the CTA they sit next to. static const double icon = 56.0; /// Corner radius, shared by every button size so they read as one family. /// /// 14 → 16 so it matches [DesignConstants.radiusXl]. At 14 a button sitting /// on a 16pt card had a visibly tighter corner than the card holding it, /// which is the kind of two-point difference nobody can name and everybody /// sees. static const double radius = DesignConstants.radiusXl; /// Absolute floor for any tappable target, per accessibility guidance. static const double minTapTarget = 44.0; } /// ───────────────────────────────────────────────────────────────────────── /// DESIGN TRIAL — full addresses on stop cards /// /// Turn on to see every stop card carrying its **complete** address rather than /// the trimmed one, over [kFullAddressMaxLines] lines instead of two. /// /// Two separate things are suppressed normally, and both are lifted together /// here or the trial would not show what it claims to: /// /// • **Home shortens the text.** `Trip.shortAddress` strips the tail every stop /// on the route shares — the city, the state, the pincode — because repeating /// "Coimbatore, Tamil Nadu 641006" on all six stops tells the rider nothing /// about which one he is looking at. With the trial on, the raw address is /// used instead. Bookings already shows the raw value. /// /// • **Both screens cap the lines.** Two, so a card's height does not depend on /// how verbose a customer was — a list of cards that step unevenly down the /// screen is harder to scan and fits fewer stops. /// /// Both caps exist for measured reasons, so this is a switch rather than a /// deletion. **Set to `false` to restore the shipped behaviour.** const bool kFullAddressTrial = true; /// Line cap while [kFullAddressTrial] is on. Four fits a full Indian address at /// 13sp in a stop card's column without letting one pathological record run the /// card off the screen. const int kFullAddressMaxLines = 4;