From 1bf89adb6ad3531426f02d66ba1d5074f79cf920 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Thu, 20 Aug 2026 16:53:34 +0530 Subject: [PATCH] updates on the design on the kpi cards --- .../cards/statistics/HoverSocialCard.js | 40 -- .../nearle_components/PageHeader.js | 89 ++- .../nearle_components/PageLayout.js | 8 +- src/components/nearle_components/StatCard.js | 250 +++++--- src/globalPolish.css | 170 +++++- src/pages/nearle/login.js | 557 +++++++++++++++--- src/pages/nearle/orders/OrdersPreview.js | 19 +- src/themes/astryx.js | 27 +- src/themes/dt/primitives.js | 37 +- src/themes/dt/tokens.js | 50 +- 10 files changed, 973 insertions(+), 274 deletions(-) delete mode 100644 src/components/cards/statistics/HoverSocialCard.js diff --git a/src/components/cards/statistics/HoverSocialCard.js b/src/components/cards/statistics/HoverSocialCard.js deleted file mode 100644 index 44a5f2a..0000000 --- a/src/components/cards/statistics/HoverSocialCard.js +++ /dev/null @@ -1,40 +0,0 @@ -import PropTypes from 'prop-types'; - -import { Card } from '@astryxdesign/core/Card'; -import { HStack } from '@astryxdesign/core/HStack'; -import { VStack } from '@astryxdesign/core/VStack'; -import { Heading } from '@astryxdesign/core/Heading'; -import { Text } from '@astryxdesign/core/Text'; - -// ===========================|| HOVER SOCIAL CARD ||=========================== // -// Solid-colour metric tile used on the order-preview summary row. `color` is a -// caller-supplied background hex, so it stays an inline style — Astryx's Card -// `variant` palette can't express an arbitrary colour. - -const HoverSocialCard = ({ primary, secondary, percentage, color, style }) => ( - - - - - {secondary} - - - {primary} - - - - {typeof percentage === 'number' ? `${percentage} %` : percentage} - - - -); - -HoverSocialCard.propTypes = { - primary: PropTypes.node, - secondary: PropTypes.node, - percentage: PropTypes.node, - color: PropTypes.string, - style: PropTypes.object -}; - -export default HoverSocialCard; diff --git a/src/components/nearle_components/PageHeader.js b/src/components/nearle_components/PageHeader.js index f155c2f..09477f4 100644 --- a/src/components/nearle_components/PageHeader.js +++ b/src/components/nearle_components/PageHeader.js @@ -6,40 +6,83 @@ import { Heading } from '@astryxdesign/core/Heading'; import { Text } from '@astryxdesign/core/Text'; import { StatusDot } from '@astryxdesign/core/StatusDot'; -// ==============================|| PAGE HEADER (Doormile-style clean title) ||============================== // -// A plain, un-boxed page title + optional live subtitle + an action slot -// (zone selector, primary button, etc.). Replaces the heavy boxed/gradient -// header so every operator page opens with a calm, corporate heading. -// -// Astryx: Heading level={1} resolves to --text-heading-1-size (1.5rem), the -// same size the old MUI `variant="h3"` rendered — the visual weight of every -// page title is unchanged by the migration. The old responsive -// `direction={{ xs: 'column', sm: 'row' }}` is now wrap="wrap" on a single -// HStack: the action slot drops to its own line once it can't fit, which is -// the behaviour that breakpoint was approximating. +import { DT } from 'themes/dt/tokens'; -export default function PageHeader({ title, subtitle, live = false, action }) { +// ==============================|| PAGE HEADER ||============================== // +// +// A plain, un-boxed page title + optional live indicator + an action slot +// (zone selector, primary button, etc.), so every operator page opens with a +// calm, corporate heading. +// +// The STRUCTURE follows KROW's page frame; the colours and the type are the +// console's own and are unchanged — `Heading level={1}` and `Text +// type="supporting"` resolve to exactly the same tokens they did before. +// +// Title · meta · ● Live [ actions ] +// subtitle +// ────────────────────────────────────────────────────────────── +// +// Two things the KROW work changed: +// +// - The bottom hairline. KROW closes every page header with a rule, and it +// is what makes the header read as a REGION of the page rather than as +// the first item in the content stack — without it, a page header and a +// section heading look identical and the page has no top. +// +// - `live` moved from beside the subtitle up onto the title row. It was +// putting the page's only motion cue on its quietest line; a live +// indicator belongs where people look for it. It is still the same +// pulsing `StatusDot variant="success"` — nothing about its colour moved. +// +// `meta` is new and optional: a real count that belongs beside the name +// ("1,248 orders"), with tabular figures so it doesn't jitter as it updates. +// +// `action` keeps its name and still wraps to its own line once it can't fit, +// which is the behaviour the old responsive `direction={{ xs: 'column', sm: +// 'row' }}` was approximating. + +export default function PageHeader({ title, subtitle, meta, live = false, action }) { return ( - - - - {title} - - {subtitle && ( - + + + {/* minWidth in `style` — VStack has no such prop (see StatCard). */} + + + + {title} + + {meta != null && ( + + {meta} + + )} {live && } - {subtitle} + + {subtitle && {subtitle}} + + {action && ( + + {action} )} - - {action && {action}} - + + ); } PageHeader.propTypes = { title: PropTypes.node, subtitle: PropTypes.node, + meta: PropTypes.node, live: PropTypes.bool, action: PropTypes.node }; diff --git a/src/components/nearle_components/PageLayout.js b/src/components/nearle_components/PageLayout.js index 73bcc10..54cceea 100644 --- a/src/components/nearle_components/PageLayout.js +++ b/src/components/nearle_components/PageLayout.js @@ -62,7 +62,7 @@ const FILL_CLASS = 'dt-page-fill'; // shell (e.g. Create Order's `.co-root` token block). It used to be dropped // silently, which made every page-scoped rule fail to match with no error — // the styles simply never applied. -export function PageShell({ children, gap = 4, fill = false, className }) { +export function PageShell({ children, gap = 3, fill = false, className }) { return ( <> {fill && ( @@ -106,8 +106,12 @@ PageShell.propTypes = { // // `max` caps the column count so 3 cards don't each become 400px wide on an // ultrawide monitor; it defaults to the number of children. +// +// minWidth is 200, fitting four across smoothly while keeping labels single-line. +// +// gap is 2.5 (10px) — tight, cohesive summary strip. // --------------------------------------------------------------------------- -export function KpiGrid({ children, minWidth = 180, gap = 3, max }) { +export function KpiGrid({ children, minWidth = 200, gap = 2.5, max }) { const count = Array.isArray(children) ? children.filter(Boolean).length : 1; return ( diff --git a/src/components/nearle_components/StatCard.js b/src/components/nearle_components/StatCard.js index 64d9dd4..28422b5 100644 --- a/src/components/nearle_components/StatCard.js +++ b/src/components/nearle_components/StatCard.js @@ -1,101 +1,208 @@ +import React from 'react'; import PropTypes from 'prop-types'; import { Card } from '@astryxdesign/core/Card'; -import { HStack } from '@astryxdesign/core/HStack'; -import { VStack } from '@astryxdesign/core/VStack'; -import { Center } from '@astryxdesign/core/Center'; -import { Text } from '@astryxdesign/core/Text'; import { Skeleton } from '@astryxdesign/core/Skeleton'; -// ==============================|| STAT / KPI CARD (Doormile-style Modern) ||============================== // -// Modern, space-efficient KPI tile: -// - Left-aligned icon tile directly grouped with the metric stack to eliminate internal dead space. -// - Soft ambient accent gradient tint based on semantic status color. -// - Crisp typography and tight vertical footprint (~56px height) saving ~30% page height. -// - Micro-hover interaction (subtle lift + shadow depth). -// -// `icon` is a rendered node, e.g. icon={}. -// `color` is a hex accent (defaults to brand black); `caption` is optional supporting text. +import { DT, STATUS } from 'themes/dt/tokens'; + +// ==============================|| STAT / KPI CARD ||============================== // +// +// Ultra-compact, dense, space-efficient enterprise KPI component. +// Shared across Deliveries, Orders, Pricing, Tenants, Riders, Customers, Hubs, +// Vehicles, Tripsheets, Exceptions, App Users, Reports and Dispatch. +// +// --------------------------------------------------------------------------- +// ULTRA-COMPACT HORIZONTAL COMPOSITION (~48px target height) +// --------------------------------------------------------------------------- +// 1. Icon chip: 26px × 26px with 6px radius and subtle 7% tint (`${accent}12`). +// Glyph size normalized to 14px. +// 2. Value: 18px bold (`lineHeight: 1`, `fontVariantNumeric: 'tabular-nums'`). +// 3. Label: 12px / 500 in secondary ink (`#64748b`), aligned horizontally on +// the same baseline beside the value for maximum space efficiency and instant +// (<1s) visual scanning. +// 4. Tight elevation, flat white surface, 8px rounded corners, 7px 12px padding. +// 5. Interactive/clickable hover support with smooth 150ms transition. Static +// cards remain clean without hover distraction. +// --------------------------------------------------------------------------- + +const MIN_HEIGHT = 48; + +const VARIANTS = { + neutral: STATUS.muted, + brand: DT.brand, + blue: STATUS.info, + green: STATUS.delivered, + amber: STATUS.pending, + red: STATUS.cancelled +}; + +export default function StatCard({ + title, + value, + icon, + color, + variant = 'brand', + caption, + loading = false, + onClick, + clickable, + className = '', + style = {} +}) { + const accent = color || VARIANTS[variant] || DT.brand; + const isClickable = Boolean(onClick || clickable); -export default function StatCard({ title, value, icon, color = '#000000', caption, loading = false }) { return ( { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(e); + } + } + : undefined + } style={{ position: 'relative', - background: `linear-gradient(135deg, ${color}0A 0%, rgba(255, 255, 255, 0.98) 45%, #ffffff 100%)`, - border: `1px solid ${color}24`, - borderRadius: 'var(--radius-container, 6px)', - boxShadow: '0 1px 2px rgba(15, 23, 42, 0.04)', - cursor: 'default' + display: 'flex', + alignItems: 'center', + minHeight: MIN_HEIGHT, + maxHeight: 56, + padding: '7px 12px', + background: DT.surface, + border: `1px solid ${DT.borderSubtle}`, + borderRadius: 8, + boxShadow: '0 1px 2px rgba(15, 23, 42, 0.03)', + cursor: isClickable ? 'pointer' : 'default', + userSelect: isClickable ? 'none' : 'auto', + boxSizing: 'border-box', + overflow: 'hidden', + width: '100%', + ...style }} > - +
{icon && ( -
{icon} -
+
)} - - +
- {title} - - {loading ? ( - - ) : ( - + ) : ( + + {value} + + )} + + - {value} - - )} + {title} + +
+ {caption && ( - {caption} - + )} -
-
+ +
); } @@ -104,7 +211,14 @@ StatCard.propTypes = { title: PropTypes.node, value: PropTypes.node, icon: PropTypes.node, + /** Raw accent hex. Takes precedence over `variant`. */ color: PropTypes.string, + /** Semantic alias over the same palette, for call sites not importing STATUS. */ + variant: PropTypes.oneOf(['neutral', 'brand', 'blue', 'green', 'amber', 'red']), caption: PropTypes.node, - loading: PropTypes.bool + loading: PropTypes.bool, + onClick: PropTypes.func, + clickable: PropTypes.bool, + className: PropTypes.string, + style: PropTypes.object }; diff --git a/src/globalPolish.css b/src/globalPolish.css index ead39f4..7880383 100644 --- a/src/globalPolish.css +++ b/src/globalPolish.css @@ -1,9 +1,15 @@ /* Global visual polish that used to come from MUI's / in themes/index.js (ThemeCustomization). Kept as plain CSS now that every - page runs on Astryx — see root CLAUDE.md §6.6. Astryx's own reset + page runs on Astryx — see root CLAUDE.md §6. Astryx's own reset (@astryxdesign/core/reset.css, imported in index.js) covers the base - element resets; this file only carries the two enterprise-polish touches - that reset doesn't: crisp font rendering and a thin, unobtrusive scrollbar. */ + element resets; this file carries the handful of things a design token + cannot express: font rendering, scrollbar pseudo-elements, keyframes, the + focus treatment, and the global reduced-motion rule. + + NOTE ON COLOUR: nothing here introduces a colour. The console keeps its own + palette (themes/dt/tokens.js) — the KROW work adopted KROW's geometry and + type treatment only. Where a rule below needs an accent it reads + `var(--color-accent)` rather than naming one. */ body { /* Astryx sets --font-family-body on :root (from themes/astryx.js's @@ -19,11 +25,58 @@ body { text-rendering: optimizeLegibility; } -/* Scrollbars are chrome, not content — they should recede until used. - The thumb was rgba(0,0,0,0.26) at 10px, which drew a solid dark bar down the - right edge of every scrollable table and read as a heavy border rather than - as a scrollbar. Lightened and narrowed; it still darkens on hover so it stays - findable. */ +/* -------------------------------------------------------------------------- + Motion vocabulary. + + One set of durations and one easing curve for the whole app, so a hover, a + row highlight and a card entrance all feel like the same product. Three + steps only: `fast` for colour-only changes (anything slower feels laggy + under a moving cursor), `base` for the default, `slow` for layout and + entrances. + + Prefixed --dt-* deliberately. These live on :root, and Dispatch.css ALSO + writes :root from a lazy-loaded chunk — an unprefixed name there would win + or lose purely on bundle order rather than on intent. + -------------------------------------------------------------------------- */ +:root { + --dt-ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --dt-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --dt-duration-fast: 120ms; + --dt-duration-base: 200ms; + --dt-duration-slow: 320ms; +} + +/* -------------------------------------------------------------------------- + Focus. + + Focus is never the browser default. One ring, one width, everywhere. The + colour is the app's own accent token, not a value chosen here, so the ring + follows the brand automatically. + + :focus-visible, not :focus — a mouse click on a button should not leave a + ring behind it. + -------------------------------------------------------------------------- */ +.dt-focus-ring:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px var(--color-accent); +} + +/* The inset variant, for a control flush inside a row or table cell where an + offset ring would be clipped by the overflow container. */ +.dt-focus-ring-inset:focus-visible { + outline: none; + box-shadow: inset 0 0 0 2px var(--color-accent); +} + +/* -------------------------------------------------------------------------- + Scrollbars. + + Chrome, not content — they should recede until used. The thumb was + rgba(0,0,0,0.26) at 10px, which drew a solid dark bar down the right edge of + every scrollable table and read as a heavy border rather than as a + scrollbar. Lightened and narrowed; it still darkens on hover so it stays + findable. + -------------------------------------------------------------------------- */ * { scrollbar-width: thin; /* Firefox */ scrollbar-color: rgba(0, 0, 0, 0.16) transparent; @@ -40,7 +93,7 @@ body { *::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.16); - border-radius: 5px; + border-radius: 999px; border: 2px solid transparent; background-clip: padding-box; } @@ -53,21 +106,104 @@ body { background-color: transparent; } -/* KPI / StatCard sleek interactions & polish */ +/* -------------------------------------------------------------------------- + Keyframes. + + A skeleton SHIMMERS, it does not pulse: a pulsing block reads as a + rendering glitch, while a travelling highlight reads as progress. Astryx's + own keeps its built-in treatment; this is for the hand-rolled + placeholders. + -------------------------------------------------------------------------- */ +@keyframes dt-shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.dt-shimmer { + background-image: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.65) 50%, transparent 100%); + background-size: 200% 100%; + animation: dt-shimmer 1.6s var(--dt-ease-in-out) infinite; +} + +@keyframes dt-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes dt-slide-up { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.dt-fade-in { + animation: dt-fade-in var(--dt-duration-base) var(--dt-ease-out); +} + +.dt-slide-up { + animation: dt-slide-up var(--dt-duration-slow) var(--dt-ease-out); +} + +/* -------------------------------------------------------------------------- + KPI / StatCard (Compact Enterprise Metric Strip). + + Static tiles stay quiet and static. Clickable/interactive tiles (e.g. on + Tenants page) get a subtle 150ms hover lift and border emphasis. + -------------------------------------------------------------------------- */ .dt-stat-card { - transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.18s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.18s ease !important; + transition: box-shadow 150ms ease, border-color 150ms ease, transform 150ms ease !important; } -.dt-stat-card:hover { +/* Interactive hover state ONLY when clickable or wrapped in button role */ +.dt-stat-card.dt-stat-card-clickable:hover, +.dt-stat-card[data-clickable='true']:hover, +[role='button'] .dt-stat-card:hover, +[role='button'] > .dt-stat-card:hover, +button .dt-stat-card:hover { + border-color: var(--color-border-emphasized, #cbd5e1) !important; + box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08) !important; transform: translateY(-1px); - box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.04) !important; } -.dt-stat-card .dt-stat-icon { - transition: transform 0.18s ease; +.dt-stat-card.dt-stat-card-clickable:active, +.dt-stat-card[data-clickable='true']:active, +[role='button'] .dt-stat-card:active { + transform: translateY(0); } -.dt-stat-card:hover .dt-stat-icon { - transform: scale(1.06); +/* Standardized 14px icon size inside the 26px chip across all call sites */ +.dt-stat-card .dt-stat-icon svg { + width: 14px !important; + height: 14px !important; + font-size: 14px !important; } +/* -------------------------------------------------------------------------- + Reduced motion. + + Handled ONCE, globally, rather than per component — a per-component opt-out + is a rule that only covers the components somebody remembered. + -------------------------------------------------------------------------- */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/src/pages/nearle/login.js b/src/pages/nearle/login.js index c30383a..ca7dd38 100644 --- a/src/pages/nearle/login.js +++ b/src/pages/nearle/login.js @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; -import Loader from 'components/Loader'; +import { MdCheck, MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md'; import logo from 'assets/images/doormile-logo.png'; import { useDispatch } from 'react-redux'; @@ -8,71 +8,400 @@ import { OpenToast } from 'components/third-party/OpenToast'; import { setLoginUser } from 'store/reducers/loginUserSlice'; import { markSessionStart } from 'utils/session'; import { loginAdmin } from 'pages/api/doormileApi'; -import { DT } from 'themes/dt/tokens'; -// Astryx design system — see themes/astryx.js for the Doormile brand theme -// and CLAUDE.md's block for the CLI workflow. -// NOTE: custom CSS (xstyle/stylex.create()) isn't wired up yet — see the -// comment in config-overrides.js. Everything below uses Astryx component -// props only; the brand gradient panel and the two logo images are plain -// native elements with inline `style` for that reason. +// Astryx design system — see themes/astryx.js for the Doormile theme. +// +// Two things about how this page is built: +// +// 1. The brand panel and the password toggle are plain elements with a scoped +// stylesheet, not Astryx layout components. Astryx has no breakpoint props, +// no gradient/decoration surface and no end-slot on TextInput, so those +// three needs cannot be expressed through component props at all. The +// stylesheet below is the same escape hatch `themes/dt/primitives.js` +// (TableScroll) and `PageLayout.js` (PageShell) already use. +// +// 2. EVERY selector in that stylesheet is prefixed `.dm-login`. The brand red +// lives on this page only — the console's own palette (black accent, slate +// ink) is deliberately untouched, so nothing here leaks into the operator +// pages. Do not lift these colours into themes/astryx.js. import { AppShell } from '@astryxdesign/core/AppShell'; import { Theme } from '@astryxdesign/core/theme'; -import { HStack } from '@astryxdesign/core/HStack'; import { VStack } from '@astryxdesign/core/VStack'; -import { Center } from '@astryxdesign/core/Center'; import { Card } from '@astryxdesign/core/Card'; -import { Heading } from '@astryxdesign/core/Heading'; -import { Text } from '@astryxdesign/core/Text'; import { TextInput } from '@astryxdesign/core/TextInput'; import { Button } from '@astryxdesign/core/Button'; import { Link } from '@astryxdesign/core/Link'; import { Divider } from '@astryxdesign/core/Divider'; import { doormileTheme } from 'themes/astryx'; -const brandPanelStyle = { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - position: 'relative', - overflow: 'hidden', - width: '46%', - height: '100vh', - color: '#fff', - padding: 48, - background: `linear-gradient(150deg, ${DT.brand} 0%, #3A3A3A 100%)` -}; - -const logoLockupStyle = { position: 'absolute', top: 48, left: 48, maxHeight: 60 }; - -const bulletDotStyle = { - width: 22, - height: 22, - borderRadius: '50%', - backgroundColor: 'rgba(255, 255, 255, 0.18)', - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: 13, - fontWeight: 700, - flexShrink: 0 -}; - -// doormile-logo.png is a white asset; recolour to brand red for this -// white-background card (the brand-panel logo stays white as-is). -const formLogoStyle = { - maxHeight: 48, - filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)' -}; +// Doormile brand red. Stated once; every rule below reads the custom property +// so the two tones can never drift apart. +const BRAND = '#C8152D'; +const BRAND_DEEP = '#A90F24'; const BULLETS = ['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control']; +// --------------------------------------------------------------------------- +// Scoped stylesheet. +// +// Astryx exposes stable class hooks (`astryx-card`, `astryx-text-input`, +// `astryx-button`, `astryx-field-label`, `astryx-link`) alongside its generated +// StyleX classes, and a two-class descendant selector out-specifies them — +// verified in the browser rather than assumed. That is what lets this restyle +// real Astryx components instead of re-implementing them. +// --------------------------------------------------------------------------- +const LOGIN_CSS = ` +.dm-login { + --dm-brand: ${BRAND}; + --dm-brand-deep: ${BRAND_DEEP}; + --dm-ring: rgba(200, 21, 45, 0.10); + display: flex; + min-height: 100vh; + width: 100%; +} + +/* ---- Left brand panel --------------------------------------------------- */ +.dm-login__brand { + flex: 0 0 44%; + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + padding: 56px; + color: #fff; + /* Depth without decoration: one light lift from the top-left, one shadow + into the bottom-right, over a restrained two-stop brand ramp. The panel + should read as lit, not as gradient-y. */ + background: + radial-gradient(115% 85% at 10% 6%, rgba(255, 255, 255, 0.12) 0%, transparent 55%), + radial-gradient(95% 85% at 95% 100%, rgba(0, 0, 0, 0.22) 0%, transparent 62%), + linear-gradient(155deg, var(--dm-brand) 0%, #B31228 55%, var(--dm-brand-deep) 100%); +} + +/* A single oversized arc, barely visible — the "logistics curve". One shape, + two hairlines, no pattern library. */ +.dm-login__brand::before { + content: ''; + position: absolute; + right: -240px; + bottom: -280px; + width: 640px; + height: 640px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.14); + box-shadow: 0 0 0 72px rgba(255, 255, 255, 0.035); + pointer-events: none; +} + +/* Faint route lines, masked so they fade out rather than tile edge to edge. */ +.dm-login__brand::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background-image: repeating-linear-gradient(118deg, rgba(255, 255, 255, 0.055) 0 1px, transparent 1px 78px); + -webkit-mask-image: radial-gradient(130% 100% at 85% 12%, #000 0%, transparent 68%); + mask-image: radial-gradient(130% 100% at 85% 12%, #000 0%, transparent 68%); +} + +.dm-login__logo { + position: absolute; + top: 56px; + left: 56px; + height: 46px; + width: auto; + z-index: 1; +} + +.dm-login__copy { + position: relative; + z-index: 1; + max-width: 460px; +} + +.dm-login__headline { + margin: 0; + font-family: var(--font-family-heading); + font-size: 34px; + line-height: 1.18; + letter-spacing: -0.02em; + font-weight: 700; + color: #fff; +} + +.dm-login__lede { + margin: 18px 0 0; + font-size: 16px; + line-height: 1.65; + color: rgba(255, 255, 255, 0.82); +} + +.dm-login__features { + margin: 34px 0 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 16px; +} + +.dm-login__feature { + display: flex; + align-items: center; + gap: 12px; + font-size: 15px; + line-height: 1.4; + color: rgba(255, 255, 255, 0.94); +} + +.dm-login__tick { + flex: 0 0 auto; + width: 22px; + height: 22px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.16); + border: 1px solid rgba(255, 255, 255, 0.22); + color: #fff; +} + +/* ---- Right form panel --------------------------------------------------- */ +.dm-login__form { + flex: 1 1 auto; + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + background: #f8f9fb; + padding: 48px 24px; +} + +.dm-login__stack { + width: 100%; + max-width: 436px; +} + +.dm-login .astryx-card { + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 16px; + /* Elevated, not floating. */ + box-shadow: 0 12px 35px rgba(15, 23, 42, 0.08); +} + +.dm-login__title { + margin: 0; + font-family: var(--font-family-heading); + font-size: 26px; + line-height: 1.25; + letter-spacing: -0.01em; + font-weight: 700; + color: #101828; +} + +.dm-login__subtitle { + margin: 6px 0 0; + font-size: 14px; + line-height: 1.5; + color: #667085; +} + +/* ---- Fields ------------------------------------------------------------- */ +.dm-login .astryx-field-label { + font-size: 13px; + font-weight: 600; + color: #344054; +} + +.dm-login .astryx-text-input { + height: 48px; + border-radius: 10px; + border-color: #d1d5db; + background: #ffffff; + padding-inline: 14px; + transition: border-color 160ms ease, box-shadow 160ms ease; +} + +.dm-login .astryx-text-input input { + font-size: 15px; +} + +/* One focus treatment for the whole form. :focus-within so the ring follows + the field, not the bare that sits inside Astryx's bordered wrapper. + + Plain two-class specificity is enough: the resting rule above already + out-specifies Astryx's own StyleX rules on this element, and this selector + is strictly more specific than that one. If a future Astryx version starts + winning here, the escape hatch is StyleX's own repeated-:not(#id) idiom — + but write it with IDs that simply never exist rather than copying StyleX's + escaped-hash form, because a backslash inside THIS template literal is + consumed by JS before the CSS parser sees it and the rule is then dropped + silently. */ +.dm-login .astryx-text-input:focus-within { + border-color: var(--dm-brand); + box-shadow: 0 0 0 3px var(--dm-ring); +} + +/* Chrome paints autofilled inputs pale blue, which is the "browser-looking" + field the design brief calls out. An inset shadow the size of the box is the + only way to repaint it. */ +.dm-login .astryx-text-input input:-webkit-autofill, +.dm-login .astryx-text-input input:-webkit-autofill:hover, +.dm-login .astryx-text-input input:-webkit-autofill:focus { + -webkit-box-shadow: 0 0 0 40px #ffffff inset; + box-shadow: 0 0 0 40px #ffffff inset; + -webkit-text-fill-color: #101828; +} + +/* ---- Password visibility toggle ----------------------------------------- */ +.dm-login__pw { + position: relative; +} + +/* Keep typed characters clear of the toggle. */ +.dm-login__pw .astryx-text-input { + padding-right: 46px; +} + +.dm-login__pw-toggle { + position: absolute; + right: 4px; + bottom: 0; + height: 48px; + width: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + padding: 0; + background: transparent; + color: #667085; + border-radius: 8px; + cursor: pointer; + transition: color 160ms ease, background-color 160ms ease; +} + +.dm-login__pw-toggle:hover { + color: #344054; + background: #f2f4f7; +} + +.dm-login__pw-toggle:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--dm-ring); + color: var(--dm-brand); +} + +/* ---- Primary action ----------------------------------------------------- */ +.dm-login .astryx-button { + height: 48px; + border-radius: 10px; + background: var(--dm-brand); + border-color: var(--dm-brand); + color: #ffffff; + font-size: 15px; + font-weight: 600; + transition: background-color 160ms ease, transform 120ms ease, box-shadow 160ms ease; +} + +.dm-login .astryx-button:hover:not([disabled]):not([aria-disabled='true']) { + background: var(--dm-brand-deep); + border-color: var(--dm-brand-deep); +} + +.dm-login .astryx-button:active:not([disabled]):not([aria-disabled='true']) { + transform: translateY(1px); + background: #970d20; +} + +.dm-login .astryx-button:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--dm-ring); +} + +.dm-login .astryx-button[disabled], +.dm-login .astryx-button[aria-disabled='true'] { + opacity: 0.7; + cursor: progress; +} + +/* ---- Footer ------------------------------------------------------------- */ +.dm-login__footer { + margin-top: 28px; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 8px 20px; + font-size: 13px; + color: #98a2b3; +} + +.dm-login__footer .astryx-link { + font-size: 13px; + color: #667085; +} + +.dm-login__footer .astryx-link:hover { + color: var(--dm-brand); +} + +/* ---- Tablet ------------------------------------------------------------- */ +@media (max-width: 1100px) { + .dm-login__brand { flex-basis: 42%; padding: 40px; } + .dm-login__logo { top: 40px; left: 40px; height: 40px; } + .dm-login__headline { font-size: 28px; } + .dm-login__lede { font-size: 15px; margin-top: 14px; } + .dm-login__features { margin-top: 26px; gap: 13px; } +} + +/* ---- Mobile ------------------------------------------------------------- */ +/* The brand panel COLLAPSES to a logo strip rather than being hidden and the + logo re-rendered above the card. That keeps exactly one of the logo in + the DOM at every breakpoint — a duplicate that is merely display:none is + still a duplicate to a screen reader and to anyone reading the markup. */ +@media (max-width: 860px) { + .dm-login { flex-direction: column; min-height: 100dvh; } + + .dm-login__brand { + flex: 0 0 auto; + align-items: center; + justify-content: center; + padding: 22px 24px; + } + .dm-login__brand::before, + .dm-login__brand::after { display: none; } + + .dm-login__logo { position: static; height: 34px; } + .dm-login__copy { display: none; } + + .dm-login__form { + flex: 1 1 auto; + align-items: flex-start; + padding: 32px 20px 40px; + } + .dm-login__title { font-size: 23px; } + .dm-login__footer { margin-top: 22px; gap: 8px 16px; } +} + +@media (max-width: 400px) { + .dm-login__form { padding-inline: 16px; } +} +`; + const Login = () => { const dispatch = useDispatch(); - const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const [loading, setLoading] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + + const emailRef = useRef(null); + const passwordRef = useRef(null); useEffect(() => { // 'authname' is the AUTH_PRESENCE_KEY (utils/session.js) — App.js and the @@ -83,10 +412,24 @@ const Login = () => { } }, []); + // Astryx's TextInput has no `autoComplete` prop and BaseProps declares only + // `xstyle`, so an unknown prop would be dropped rather than forwarded. It + // does forward `ref` to the real , which is the supported way to + // reach the element — password managers need these attributes to offer a + // saved credential at all. Re-applied when the toggle swaps `type`. + useEffect(() => { + emailRef.current?.setAttribute('autocomplete', 'email'); + passwordRef.current?.setAttribute('autocomplete', 'current-password'); + }, [showPassword]); + const opentoast = (message, variant = 'error') => OpenToast(message, variant, 2000); const handleSubmit = async (e) => { e.preventDefault(); + // Guard against a second submit while the first is still in flight — + // the button is disabled while loading, but Enter in a field is not. + if (loading) return; + if (!email || !password) { opentoast('Enter your email and password'); return; @@ -123,83 +466,105 @@ const Login = () => { return ( - {loading && } - - {/* ---- Left brand panel (plain element: see the note at the top of - this file about custom styling not going through Astryx yet) ---- */} -
- Doormile + - - - Operate your dispatch, end to end. - - +
+ {/* ---- Brand panel. The ONLY Doormile logo on this page. ---- */} +
+ - {/* ---- Right form panel ---- */} -
- - - - - Doormile - Welcome back - Sign in to the Doormile Express console - + {/* ---- Form panel ---- */} +
+
+ + +
+

Welcome back

+

Sign in to the Doormile Express console

+
setEmail(value.toLocaleLowerCase())} /> - + setPassword(value)} + /> + +
+ +
- +
+ + +
); diff --git a/src/pages/nearle/orders/OrdersPreview.js b/src/pages/nearle/orders/OrdersPreview.js index 57954b6..cb30bc6 100644 --- a/src/pages/nearle/orders/OrdersPreview.js +++ b/src/pages/nearle/orders/OrdersPreview.js @@ -38,7 +38,7 @@ import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } f import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import PageHeader from 'components/nearle_components/PageHeader'; import { PageShell, KpiGrid, DataCard } from 'components/nearle_components/PageLayout'; -import HoverSocialCard from 'components/cards/statistics/HoverSocialCard'; +import StatCard from 'components/nearle_components/StatCard'; import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from '../../api/api'; import { OpenToast } from 'components/third-party/OpenToast'; @@ -561,17 +561,16 @@ const OrdersPreview = () => { } /> + {/* Was four s — solid-colour tiles that were the only + KPI design in the app not coming from StatCard, so this row looked + nothing like the KPI row on the other fifteen pages. Same data, + same semantic colours, shared tile. */} {aiMode == 1 && ( - } - color={STATUS.delivered} - /> - } color={STATUS.pending} /> - } color="#0ea5e9" /> - } color={STATUS.cancelled} /> + } color={STATUS.delivered} /> + } color={STATUS.pending} /> + } color={STATUS.info} /> + } color={STATUS.cancelled} /> )} diff --git a/src/themes/astryx.js b/src/themes/astryx.js index 0f63858..b5a8508 100644 --- a/src/themes/astryx.js +++ b/src/themes/astryx.js @@ -2,6 +2,9 @@ // brand colour so migrated surfaces match the existing DT token palette // (see themes/dt/tokens.js). Runtime-injected (not `astryx theme build`) // since this repo doesn't do SSR — see `astryx docs theme`. +// +// The KROW work touched exactly one thing in this file: the radius block. +// The colour tokens below are Doormile's own and are unchanged. import { defineTheme } from '@astryxdesign/core/theme'; import { neutralTheme } from '@astryxdesign/theme-neutral'; import { DT } from 'themes/dt/tokens'; @@ -42,6 +45,9 @@ export const doormileTheme = defineTheme({ // Astryx explicitly does not load fonts, it only names them. Change one // without the other and you get a silent fallback. // + // Sora headings + Inter body is also exactly the pairing KROW specifies, so + // adopting KROW's type treatment needed no change here. + // // base 14, not 16: this is a dense operator console and 14px is what the // existing tables already assume. ratio 1.2 keeps the heading steps tight // so an h1 doesn't tower over a data-heavy page. @@ -59,13 +65,22 @@ export const doormileTheme = defineTheme({ // Setting it per-component would leave the ones nobody remembered to // list still rounded. // + // These were a flat 5px across all five roles. They are now KROW's scale, + // mapped onto those roles: `page` is a page-level surface (20px), + // `container` is a card (16px), and `element`/`inner`/`chat` are the + // controls and inner chrome that sit inside one (12px). Keeping element + // and container DIFFERENT is the whole point — a 16px card holding 16px + // inputs has no visual nesting. + // // `--radius-full` is deliberately untouched: it drives avatars, status // dots and pill chips, which are meant to be round. - '--radius-page': '5px', - '--radius-container': '5px', - '--radius-element': '5px', - '--radius-inner': '5px', - '--radius-chat': '5px', + '--radius-page': `${DT.radiusPage}px`, + '--radius-container': `${DT.radiusCard}px`, + '--radius-element': `${DT.radiusField}px`, + '--radius-inner': `${DT.radiusInner}px`, + '--radius-chat': `${DT.radiusInner}px`, + + // ---- Accent (UNCHANGED) -------------------------------------------- '--color-accent': [BRAND_ON_LIGHT, BRAND_ON_DARK], '--color-icon-accent': [BRAND_ON_LIGHT, BRAND_ON_DARK], '--color-text-accent': [BRAND_ON_LIGHT, BRAND_ON_DARK], @@ -78,6 +93,6 @@ export const doormileTheme = defineTheme({ }, components: { card: { base: { borderRadius: `${DT.radiusCard}px` } }, - 'text-input': { base: { borderRadius: `${DT.radiusInner}px` } } + 'text-input': { base: { borderRadius: `${DT.radiusField}px` } } } }); diff --git a/src/themes/dt/primitives.js b/src/themes/dt/primitives.js index b666060..9bf6b99 100644 --- a/src/themes/dt/primitives.js +++ b/src/themes/dt/primitives.js @@ -26,14 +26,30 @@ export const softPopupStyle = { overflow: 'hidden' }; -// Scroll container for a table. Astryx's Table renders a bare with no -// sticky-header or max-height affordance (MUI's TableContainer used to provide that), -// so pages wrap it in this. The brand-red scrollbar can't be expressed inline -// — ::-webkit-scrollbar-* are pseudo-elements — hence the one-time
{children}
diff --git a/src/themes/dt/tokens.js b/src/themes/dt/tokens.js index b46fdd6..3f9e6f9 100644 --- a/src/themes/dt/tokens.js +++ b/src/themes/dt/tokens.js @@ -5,11 +5,30 @@ // redefining its own DT object, so radius/shadow/color decisions stay in one // place. See CLAUDE.md §6 for the full page-anatomy convention these tokens // support. +// +// --------------------------------------------------------------------------- +// SCOPE OF THE KROW WORK +// --------------------------------------------------------------------------- +// The console adopted KROW's *structural* design language — its radius scale, +// its type treatment, its page anatomy. It deliberately did NOT adopt KROW's +// palette: the colours below are Doormile's own and are unchanged. The brand +// is still black, the ink scale is still the slate family, and the lifecycle +// hues are untouched. +// +// So when reading KROW's spec against this file: take the geometry and the +// typography, ignore the blue. // ============================================================================ export const DT = { - // Corner radius is uniform across the console. Boxes were 10-14px, which - // reads soft and consumer-ish on dense operator screens; 5px is tight - // enough for table density while still reading as a rounded surface. + // ---- Corner radius ------------------------------------------------- + // Was a flat 5px on every surface, which read as "tight console" but also + // meant a card, the inputs inside it and the chips inside those all had the + // same corner — so nothing nested visually. + // + // Now KROW's scale, which derives from a 12px root: 8 / 12 / 16 / 20. Two + // of those steps are both correct and mean different things — 20px is a + // page-level surface, 16px is a dense console card. This IS a dense operator + // console, so cards take 16 and inputs and inner chrome take 12, which is + // what gives a card holding fields a visible hierarchy. // // These are the shared values — change them here, not per page. The Astryx // components read the matching `--radius-*` tokens set in themes/astryx.js, @@ -19,13 +38,28 @@ export const DT = { // shaped like pills or circles (status chips, avatars, dots). Squaring those // doesn't make them consistent, it makes them look broken. radiusPill: 999, - radiusCard: 5, - radiusInner: 5, - radiusField: 5, - // Restrained, low-contrast elevation — corporate (Linear/Stripe), not flashy. - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', + radiusPage: 20, + radiusCard: 16, + radiusInner: 12, + radiusField: 12, + radiusSm: 8, + + // ---- Elevation ----------------------------------------------------- + // Restrained, low-contrast — corporate (Linear/Stripe), not flashy. + // UNCHANGED by the KROW work; these are the console's own shadows. + // + // The three original names are the canonical ones. `shadowXs`/`Sm`/`Lg` are + // aliases added so newer components can be written against a conventional + // xs/sm/lg naming without introducing a second, differently-valued scale. + // They resolve to exactly the same three values. shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', + shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', + shadowXs: '0 1px 3px rgba(15, 23, 42, 0.06)', // alias of shadowMd + shadowSm: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', // alias of shadowSoft + shadowLg: '0 12px 32px rgba(15, 23, 42, 0.12)', // alias of shadowPop + + // ---- Palette (UNCHANGED) ------------------------------------------- textPrimary: '#0f172a', textSecondary: '#64748b', textMuted: '#94a3b8',