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