@burdenoff/microfe-movethewheels 2026.703.4 → 2026.703.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AIAssistantPage-3YOnHO9Y.js → AIAssistantPage-BmILdHgO.js} +16 -15
- package/dist/AnalyticsPage-BSlsiSDE.js +194 -0
- package/dist/{CreateOrderPage-DRAgGo9i.js → CreateOrderPage-DaCgVaO3.js} +40 -40
- package/dist/{CustomerDetailsPage-BCeMmOFe.js → CustomerDetailsPage-DErpbafW.js} +71 -71
- package/dist/CustomersPage-C1lRU6xs.js +205 -0
- package/dist/DashboardPage-Q5-5EKAU.js +542 -0
- package/dist/{DataTable-XvUcTjwi.js → DataTable-KjzbwnRL.js} +31 -31
- package/dist/{DocumentsCard-U2lSe-cn.js → DocumentsCard-T9y6cv4-.js} +25 -25
- package/dist/{DriverDetailsPage-BQrYd_Lp.js → DriverDetailsPage-DnHGkDC7.js} +64 -64
- package/dist/DriversPage-C0ujWVOn.js +221 -0
- package/dist/FinancePage-CA2XEgsQ.js +175 -0
- package/dist/{FleetPage-3ZHsB-TN.js → FleetPage-DrvXW7wK.js} +22 -22
- package/dist/{FormError-cuLV2jVD.js → FormError-CRsMP7TQ.js} +1 -1
- package/dist/{ImportExportPage-C6f7DqhM.js → ImportExportPage-Cac9HjFx.js} +12 -12
- package/dist/InventoryPage-Ezf4gqKI.js +259 -0
- package/dist/LiveTrackingPage-CzbVwDqK.js +429 -0
- package/dist/{MarketplacePage-Bh_2ns4v.js → MarketplacePage-BHDmSQmo.js} +116 -116
- package/dist/MetricCard-DQlDNqmK.js +191 -0
- package/dist/{Notice-CSoDYKw9.js → Notice-GmA7JDR3.js} +2 -2
- package/dist/{OrderDetailsPage-zCj0lW0j.js → OrderDetailsPage-CR0Wc1aN.js} +58 -58
- package/dist/OrdersListPage-C8Bu6wXM.js +386 -0
- package/dist/{PageLayout-D4LDHJJq.js → PageLayout-DX1Ou6V6.js} +164 -134
- package/dist/{ProductDetailsPage-wsxAQkA0.js → ProductDetailsPage-B_ns2MOe.js} +55 -55
- package/dist/ProductsPage-c92BsEoA.js +186 -0
- package/dist/ReportsPage-CPLXtLT2.js +377 -0
- package/dist/{RouteDetailsPage-DyMJE9AS.js → RouteDetailsPage-DOfcRiFv.js} +208 -181
- package/dist/{RoutesPage-Dtc669yc.js → RoutesPage-BZGublby.js} +12 -12
- package/dist/{SettingsPage-CpH9xhQg.js → SettingsPage-5cweX_xh.js} +15 -15
- package/dist/Sparkline-zSXvG6vA.js +16729 -0
- package/dist/{StatusBadge-C87mDpDP.js → StatusBadge-oZjownr1.js} +7 -7
- package/dist/TrackingPage-Ln9qVflK.js +368 -0
- package/dist/{VehicleDetailsPage-DldYNsYV.js → VehicleDetailsPage-CrkzE0dN.js} +53 -53
- package/dist/VehiclesPage-CTQWH7af.js +184 -0
- package/dist/{WarehouseDetailsPage-CMG7or6G.js → WarehouseDetailsPage-CwDLaG65.js} +64 -64
- package/dist/WarehousesPage-KW02DvZg.js +182 -0
- package/dist/{arrow-left-n48Y-Wis.js → arrow-left-DJDw_Yqv.js} +1 -1
- package/dist/{arrow-right-Q3XQpTPR.js → arrow-right-Cb2fLTPJ.js} +1 -1
- package/dist/{arrow-up-xNF1zJUG.js → arrow-up-PxaOIyiT.js} +1 -1
- package/dist/{box-Eu05PXDh.js → box-hSJVtvGf.js} +1 -1
- package/dist/boxes-DDnxEkT5.js +54 -0
- package/dist/{chart-column-ouzgxIv_.js → chart-column-xXgAZvRl.js} +1 -1
- package/dist/charts-CfCwQg6o.js +2018 -0
- package/dist/{chevron-left-B2_r0Elx.js → chevron-left-dhEGJYdh.js} +1 -1
- package/dist/{chevron-right-eV4LjaSi.js → chevron-right-Cud2NDvm.js} +1 -1
- package/dist/chunk-BdsCSvBP.js +20 -0
- package/dist/{circle-alert-Bzh2DBSF.js → circle-alert-Rx0p3D5w.js} +1 -1
- package/dist/circle-check-BCxRFdFo.js +13 -0
- package/dist/{circle-check-big-CGMSpQmD.js → circle-check-big-BcIJ8xjx.js} +1 -1
- package/dist/{clock-Bl3ptexL.js → clock-B2_YxJgB.js} +1 -1
- package/dist/{dollar-sign-CAVU95SO.js → dollar-sign-CQmAICv4.js} +1 -1
- package/dist/{download-C5NbvVDc.js → download-CG4Tponq.js} +1 -1
- package/dist/{file-text-D-1uvLZG.js → file-text-CzH-zMub.js} +1 -1
- package/dist/{filter-DUfUg6BO.js → filter-C0u2Lwfn.js} +1 -1
- package/dist/{fuel-B6_lnKDs.js → fuel-C2KELcM-.js} +1 -1
- package/dist/index.css +2 -0
- package/dist/index.js +214 -225
- package/dist/{map-C-M-FQLR.js → map-FuPu5aTw.js} +1 -1
- package/dist/{map-pin-Dznl5l40.js → map-pin-czpLv9uy.js} +1 -1
- package/dist/mapMarkers-NCV9MRKR.js +28 -0
- package/dist/maps-D9m7PKrs.js +5070 -0
- package/dist/{navigation-BJSJl-VG.js → navigation-D5_pdCSS.js} +1 -1
- package/dist/{package-CcRttV2N.js → package-B0sdi6lU.js} +1 -1
- package/dist/{phone-DVSZHt_j.js → phone-V2nuzPu9.js} +1 -1
- package/dist/{plus-C_XZDwBz.js → plus-BqQxkfoU.js} +1 -1
- package/dist/{refresh-cw-CfgN67l3.js → refresh-cw-CEOAPEcV.js} +1 -1
- package/dist/route-CZWcW-Dw.js +22 -0
- package/dist/{save-DJE8Fpdv.js → save-DDFvrxWl.js} +1 -1
- package/dist/{search-D_VyGcZT.js → search-D7Ya6jxj.js} +1 -1
- package/dist/{square-pen-D-6JYrVQ.js → square-pen-DoZZS4Lw.js} +1 -1
- package/dist/{star-BpRlUZuU.js → star-B7PgVu5j.js} +1 -1
- package/dist/{store-B-8HMuyZ.js → store-YVzcIG4A.js} +1 -1
- package/dist/{trash-2-8vmv-XV_.js → trash-2-BegnBzwI.js} +1 -1
- package/dist/{trending-up-Dz6PqPRj.js → trending-up-DBd3XNdb.js} +1 -1
- package/dist/{triangle-alert-D7Y3J_4z.js → triangle-alert-Bxk3Kgs6.js} +1 -1
- package/dist/{truck-D-y2IDzv.js → truck-Clrn0oHp.js} +1 -1
- package/dist/{upload-CHZofOqb.js → upload-Df5JmMwH.js} +1 -1
- package/dist/{useAssistant-DVW4QgQb.js → useAssistant-Cb8nnIVC.js} +11 -28
- package/dist/{useCustomers-BPW4__PR.js → useCustomers-Dq2IR2HQ.js} +1 -1
- package/dist/{useFleet-ChBkxAd6.js → useFleet-CapnWBcA.js} +1 -1
- package/dist/{useImportExport-DrPDHEDK.js → useImportExport-BoGzrlJy.js} +1 -1
- package/dist/{useInventory-CAAVMRp6.js → useInventory-Ci6M2d9f.js} +1 -1
- package/dist/{useOrders-B465S2nt.js → useOrders-BuQU1OHX.js} +1 -1
- package/dist/{useReports-jQ3GpNOm.js → useReports-DIx8W7CO.js} +1 -1
- package/dist/{useRoutes-hS3d5-xr.js → useRoutes-D7EvI_pm.js} +1 -1
- package/dist/{useSettings-BB8X2RLh.js → useSettings-BhX251UQ.js} +6 -6
- package/dist/{useTracking-RjGz9PZG.js → useTracking-C40jmZ6W.js} +3 -3
- package/dist/{user-DzfTn4dE.js → user-BlcJrbtE.js} +1 -1
- package/dist/{user-check-BEKRCi7v.js → user-check-DtnQmimP.js} +1 -1
- package/dist/{users-CCrq-w0C.js → users-Cvwko-Dx.js} +1 -1
- package/dist/{warehouse-CaqfmMKh.js → warehouse-LWLVTk6G.js} +1 -1
- package/dist/{wrench-q_rgJfaP.js → wrench-Cme0RbyR.js} +1 -1
- package/dist/{x-tMeDMcCw.js → x-D8C9Pg5V.js} +1 -1
- package/package.json +5 -1
- package/src/movethewheels/components/GlassPanel.tsx +71 -0
- package/src/movethewheels/components/MetricCard.tsx +171 -73
- package/src/movethewheels/components/StatusBadge.tsx +7 -6
- package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
- package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
- package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
- package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
- package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
- package/src/movethewheels/components/charts/chartTokens.ts +43 -0
- package/src/movethewheels/components/charts/index.ts +9 -0
- package/src/movethewheels/components/index.ts +44 -1
- package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
- package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
- package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
- package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
- package/src/movethewheels/components/maps/index.ts +16 -0
- package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
- package/src/movethewheels/components/maps/map-markers.css +121 -0
- package/src/movethewheels/hooks/useAnalytics.ts +14 -4
- package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
- package/src/movethewheels/pages/CreateOrderPage.tsx +4 -4
- package/src/movethewheels/pages/CustomersPage.tsx +29 -20
- package/src/movethewheels/pages/DashboardPage.tsx +238 -12
- package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/DriversPage.tsx +35 -24
- package/src/movethewheels/pages/FinancePage.tsx +68 -30
- package/src/movethewheels/pages/InventoryPage.tsx +15 -3
- package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
- package/src/movethewheels/pages/OrdersListPage.tsx +46 -38
- package/src/movethewheels/pages/ProductsPage.tsx +28 -24
- package/src/movethewheels/pages/ReportsPage.tsx +23 -0
- package/src/movethewheels/pages/RouteDetailsPage.tsx +46 -7
- package/src/movethewheels/pages/SettingsPage.tsx +2 -2
- package/src/movethewheels/pages/TrackingPage.tsx +76 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/VehiclesPage.tsx +34 -24
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/WarehousesPage.tsx +28 -22
- package/src/movethewheels/types/index.ts +1 -1
- package/src/movethewheels/utils/graphqlClient.ts +81 -31
- package/src/movethewheels/utils/mapMarkers.ts +58 -0
- package/dist/AnalyticsPage-CMpI1070.js +0 -215
- package/dist/CustomersPage-CaNBMXmM.js +0 -172
- package/dist/DashboardPage-DIKlXde3.js +0 -387
- package/dist/DriversPage-PYmdFiby.js +0 -176
- package/dist/FinancePage-Cwh9Bw4M.js +0 -136
- package/dist/InventoryPage-DpiqVu1v.js +0 -253
- package/dist/LiveTrackingPage-FAzzlVnX.js +0 -379
- package/dist/MetricCard-CVBVpu7o.js +0 -146
- package/dist/OrdersListPage-CAMsuUck.js +0 -384
- package/dist/ProductsPage-DC5qDLfz.js +0 -180
- package/dist/ReportsPage-BwUJ37eO.js +0 -356
- package/dist/TrackingPage-DaNEFG_h.js +0 -325
- package/dist/VehiclesPage-BFw13iMt.js +0 -176
- package/dist/WarehousesPage-Wkytnt86.js +0 -173
- package/dist/useAnalytics-D7HW2XRb.js +0 -347
- /package/dist/{formatters-BQr-aR5W.js → formatters-FWJDwTFv.js} +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { GlassCard } from '@burdenoff/fe-libs/ui';
|
|
3
3
|
import { cn } from '../utils/cn';
|
|
4
|
+
import { Sparkline } from './charts/Sparkline';
|
|
5
|
+
|
|
6
|
+
/** Semantic accent used to tint the sparkline, delta chip, and icon chip. */
|
|
7
|
+
export type MetricAccent = 'brand' | 'blue' | 'success' | 'warning' | 'error' | 'neutral';
|
|
8
|
+
|
|
9
|
+
export interface MetricDelta {
|
|
10
|
+
/** Formatted delta text, e.g. "+12%", "-5%", "+128". */
|
|
11
|
+
value: string;
|
|
12
|
+
/** Direction drives the arrow + semantic color. */
|
|
13
|
+
direction: 'up' | 'down' | 'flat';
|
|
14
|
+
/**
|
|
15
|
+
* When true, a downward direction is treated as GOOD (green) and upward as
|
|
16
|
+
* bad — for metrics where lower is better (e.g. failed deliveries, cost).
|
|
17
|
+
*/
|
|
18
|
+
invert?: boolean;
|
|
19
|
+
}
|
|
4
20
|
|
|
5
21
|
interface MetricCardProps {
|
|
6
22
|
/** Main value to display */
|
|
@@ -9,13 +25,13 @@ interface MetricCardProps {
|
|
|
9
25
|
label: string;
|
|
10
26
|
/** Optional icon */
|
|
11
27
|
icon?: ReactNode;
|
|
12
|
-
/** Change from previous period (e.g., "+12%", "-5%") */
|
|
28
|
+
/** Change from previous period (e.g., "+12%", "-5%") — legacy inline form. */
|
|
13
29
|
change?: string;
|
|
14
|
-
/** Whether the change is positive, negative, or neutral */
|
|
30
|
+
/** Whether the change is positive, negative, or neutral (legacy). */
|
|
15
31
|
changeType?: 'positive' | 'negative' | 'neutral';
|
|
16
32
|
/** Optional subtitle or additional info */
|
|
17
33
|
subtitle?: string;
|
|
18
|
-
/** Optional trend indicator */
|
|
34
|
+
/** Optional trend indicator (legacy inline form). */
|
|
19
35
|
trend?: 'up' | 'down' | 'flat';
|
|
20
36
|
/** Card size variant */
|
|
21
37
|
size?: 'sm' | 'md' | 'lg';
|
|
@@ -27,31 +43,98 @@ interface MetricCardProps {
|
|
|
27
43
|
treatment?: 'metric' | 'glass';
|
|
28
44
|
/** Featured/active accent glow (used for the single hero metric per page). */
|
|
29
45
|
glow?: boolean;
|
|
46
|
+
/** Premium: a mini sparkline rendered in the card footer. Plain numbers. */
|
|
47
|
+
sparklineData?: number[];
|
|
48
|
+
/** Premium: a colored delta chip (preferred over `change`/`trend`). */
|
|
49
|
+
delta?: MetricDelta;
|
|
50
|
+
/** Accent color for the sparkline / icon chip / glow tint. Defaults to brand. */
|
|
51
|
+
accent?: MetricAccent;
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
const sizeClasses = {
|
|
33
|
-
sm: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
sm: { card: 'p-3', value: 'text-xl font-bold', label: 'text-xs', icon: 'h-8 w-8' },
|
|
56
|
+
md: { card: 'p-4', value: 'text-2xl font-bold', label: 'text-sm', icon: 'h-10 w-10' },
|
|
57
|
+
lg: { card: 'p-5', value: 'text-3xl font-bold', label: 'text-sm', icon: 'h-11 w-11' },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const accentSparkColor: Record<MetricAccent, string> = {
|
|
61
|
+
brand: 'var(--color-brand-primary)',
|
|
62
|
+
blue: 'var(--color-chart-1)',
|
|
63
|
+
success: 'var(--color-status-success-text)',
|
|
64
|
+
warning: 'var(--color-status-warning-text)',
|
|
65
|
+
error: 'var(--color-status-error-text)',
|
|
66
|
+
neutral: 'var(--color-text-secondary)',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const accentChipClasses: Record<MetricAccent, string> = {
|
|
70
|
+
brand: 'bg-brand-primary/10 text-brand-primary',
|
|
71
|
+
blue: 'bg-chart-1/10 text-chart-1',
|
|
72
|
+
success: 'bg-status-success-bg-subtle text-status-success-text',
|
|
73
|
+
warning: 'bg-status-warning-bg-subtle text-status-warning-text',
|
|
74
|
+
error: 'bg-status-error-bg-subtle text-status-error-text',
|
|
75
|
+
neutral: 'bg-bg-sunken text-text-secondary',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function ArrowIcon({ direction }: { direction: 'up' | 'down' | 'flat' }) {
|
|
79
|
+
if (direction === 'flat') {
|
|
80
|
+
return (
|
|
81
|
+
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden>
|
|
82
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 12h14" />
|
|
83
|
+
</svg>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
<svg
|
|
88
|
+
className={cn('h-3.5 w-3.5', direction === 'down' && 'rotate-180')}
|
|
89
|
+
fill="none"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
stroke="currentColor"
|
|
92
|
+
aria-hidden
|
|
93
|
+
>
|
|
94
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 19V5m0 0l-6 6m6-6l6 6" />
|
|
95
|
+
</svg>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Delta chip whose color is semantic (good = success, bad = error). */
|
|
100
|
+
function DeltaChip({ delta }: { delta: MetricDelta }) {
|
|
101
|
+
const isGood =
|
|
102
|
+
delta.direction === 'flat'
|
|
103
|
+
? null
|
|
104
|
+
: delta.invert
|
|
105
|
+
? delta.direction === 'down'
|
|
106
|
+
: delta.direction === 'up';
|
|
107
|
+
|
|
108
|
+
const tone =
|
|
109
|
+
isGood == null
|
|
110
|
+
? 'bg-bg-sunken text-text-secondary'
|
|
111
|
+
: isGood
|
|
112
|
+
? 'bg-status-success-bg-subtle text-status-success-text'
|
|
113
|
+
: 'bg-status-error-bg-subtle text-status-error-text';
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<span
|
|
117
|
+
className={cn(
|
|
118
|
+
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums',
|
|
119
|
+
tone
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
<ArrowIcon direction={delta.direction} />
|
|
123
|
+
{delta.value}
|
|
124
|
+
</span>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const legacyTrendIcons = {
|
|
129
|
+
up: 'M5 10l7-7m0 0l7 7m-7-7v18',
|
|
130
|
+
down: 'M19 14l-7 7m0 0l-7-7m7 7V3',
|
|
131
|
+
flat: 'M5 12h14',
|
|
51
132
|
};
|
|
52
133
|
|
|
53
134
|
/**
|
|
54
|
-
* MetricCard
|
|
135
|
+
* MetricCard — a premium stat card: big value, label, an optional colored delta
|
|
136
|
+
* chip, an optional inline sparkline, and a token-tinted icon chip. Backward
|
|
137
|
+
* compatible with the legacy `change`/`changeType`/`trend` props.
|
|
55
138
|
*/
|
|
56
139
|
export function MetricCard({
|
|
57
140
|
value,
|
|
@@ -66,49 +149,26 @@ export function MetricCard({
|
|
|
66
149
|
className,
|
|
67
150
|
treatment = 'metric',
|
|
68
151
|
glow = false,
|
|
152
|
+
sparklineData,
|
|
153
|
+
delta,
|
|
154
|
+
accent = 'brand',
|
|
69
155
|
}: MetricCardProps) {
|
|
70
156
|
const styles = sizeClasses[size];
|
|
71
157
|
|
|
72
|
-
const
|
|
158
|
+
const legacyChangeColors = {
|
|
73
159
|
positive: 'text-status-success-text',
|
|
74
160
|
negative: 'text-status-error-text',
|
|
75
161
|
neutral: 'text-text-secondary',
|
|
76
162
|
};
|
|
77
163
|
|
|
78
|
-
const
|
|
79
|
-
up: (
|
|
80
|
-
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
81
|
-
<path
|
|
82
|
-
strokeLinecap="round"
|
|
83
|
-
strokeLinejoin="round"
|
|
84
|
-
strokeWidth={2}
|
|
85
|
-
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
86
|
-
/>
|
|
87
|
-
</svg>
|
|
88
|
-
),
|
|
89
|
-
down: (
|
|
90
|
-
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
91
|
-
<path
|
|
92
|
-
strokeLinecap="round"
|
|
93
|
-
strokeLinejoin="round"
|
|
94
|
-
strokeWidth={2}
|
|
95
|
-
d="M19 14l-7 7m0 0l-7-7m7 7V3"
|
|
96
|
-
/>
|
|
97
|
-
</svg>
|
|
98
|
-
),
|
|
99
|
-
flat: (
|
|
100
|
-
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
101
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" />
|
|
102
|
-
</svg>
|
|
103
|
-
),
|
|
104
|
-
};
|
|
164
|
+
const hasSpark = Array.isArray(sparklineData) && sparklineData.length >= 2;
|
|
105
165
|
|
|
106
166
|
return (
|
|
107
167
|
<GlassCard
|
|
108
168
|
treatment={treatment}
|
|
109
169
|
glow={glow}
|
|
110
170
|
className={cn(
|
|
111
|
-
'overflow-hidden border-border-seam',
|
|
171
|
+
'group relative overflow-hidden border-border-seam',
|
|
112
172
|
onClick &&
|
|
113
173
|
'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base',
|
|
114
174
|
className
|
|
@@ -127,30 +187,28 @@ export function MetricCard({
|
|
|
127
187
|
}
|
|
128
188
|
: {})}
|
|
129
189
|
>
|
|
190
|
+
{/* Accent hairline along the top edge — a calm brand accent, no movement. */}
|
|
191
|
+
<span
|
|
192
|
+
aria-hidden
|
|
193
|
+
className="absolute inset-x-0 top-0 h-0.5 opacity-70"
|
|
194
|
+
style={{
|
|
195
|
+
background: `linear-gradient(90deg, ${accentSparkColor[accent]}, transparent 85%)`,
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
130
198
|
<div className={styles.card}>
|
|
131
|
-
<div className="flex items-start justify-between">
|
|
132
|
-
<div className="space-y-1">
|
|
199
|
+
<div className="flex items-start justify-between gap-3">
|
|
200
|
+
<div className="min-w-0 space-y-1">
|
|
133
201
|
<p className={cn('text-text-secondary', styles.label)}>{label}</p>
|
|
134
|
-
<p className={cn('
|
|
202
|
+
<p className={cn('tracking-tight tabular-nums text-text-primary', styles.value)}>
|
|
135
203
|
{typeof value === 'number' ? value.toLocaleString() : value}
|
|
136
204
|
</p>
|
|
137
205
|
{subtitle && <p className="text-xs text-text-secondary">{subtitle}</p>}
|
|
138
|
-
{change && (
|
|
139
|
-
<div
|
|
140
|
-
className={cn(
|
|
141
|
-
'flex items-center gap-1 text-sm tabular-nums',
|
|
142
|
-
changeColors[changeType]
|
|
143
|
-
)}
|
|
144
|
-
>
|
|
145
|
-
{trend && trendIcons[trend]}
|
|
146
|
-
<span>{change}</span>
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
206
|
</div>
|
|
150
207
|
{icon && (
|
|
151
208
|
<div
|
|
152
209
|
className={cn(
|
|
153
|
-
'flex
|
|
210
|
+
'flex shrink-0 items-center justify-center rounded-xl',
|
|
211
|
+
accentChipClasses[accent],
|
|
154
212
|
styles.icon
|
|
155
213
|
)}
|
|
156
214
|
>
|
|
@@ -158,6 +216,48 @@ export function MetricCard({
|
|
|
158
216
|
</div>
|
|
159
217
|
)}
|
|
160
218
|
</div>
|
|
219
|
+
|
|
220
|
+
{(delta || hasSpark || change) && (
|
|
221
|
+
<div className="mt-3 flex items-end justify-between gap-3">
|
|
222
|
+
<div className="flex items-center gap-2">
|
|
223
|
+
{delta ? (
|
|
224
|
+
<DeltaChip delta={delta} />
|
|
225
|
+
) : (
|
|
226
|
+
change && (
|
|
227
|
+
<span
|
|
228
|
+
className={cn(
|
|
229
|
+
'flex items-center gap-1 text-sm tabular-nums',
|
|
230
|
+
legacyChangeColors[changeType]
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
{trend && (
|
|
234
|
+
<svg
|
|
235
|
+
className="h-4 w-4"
|
|
236
|
+
fill="none"
|
|
237
|
+
viewBox="0 0 24 24"
|
|
238
|
+
stroke="currentColor"
|
|
239
|
+
aria-hidden
|
|
240
|
+
>
|
|
241
|
+
<path
|
|
242
|
+
strokeLinecap="round"
|
|
243
|
+
strokeLinejoin="round"
|
|
244
|
+
strokeWidth={2}
|
|
245
|
+
d={legacyTrendIcons[trend]}
|
|
246
|
+
/>
|
|
247
|
+
</svg>
|
|
248
|
+
)}
|
|
249
|
+
<span>{change}</span>
|
|
250
|
+
</span>
|
|
251
|
+
)
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
{hasSpark && (
|
|
255
|
+
<div className="w-24 shrink-0 sm:w-28">
|
|
256
|
+
<Sparkline data={sparklineData} color={accentSparkColor[accent]} height={34} />
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
161
261
|
</div>
|
|
162
262
|
</GlassCard>
|
|
163
263
|
);
|
|
@@ -173,14 +273,12 @@ export function MetricCardSkeleton({ size = 'md' }: { size?: 'sm' | 'md' | 'lg'
|
|
|
173
273
|
<GlassCard treatment="metric" className="overflow-hidden border-border-seam">
|
|
174
274
|
<div className={styles.card}>
|
|
175
275
|
<div className="flex items-start justify-between">
|
|
176
|
-
<div className="space-y-2
|
|
177
|
-
<div className="h-4 w-24 bg-bg-sunken motion-safe:animate-vc-shimmer
|
|
178
|
-
<div className="h-8 w-32 bg-bg-sunken motion-safe:animate-vc-shimmer
|
|
179
|
-
<div className="h-3 w-20 bg-bg-sunken motion-safe:animate-vc-shimmer
|
|
276
|
+
<div className="flex-1 space-y-2">
|
|
277
|
+
<div className="h-4 w-24 rounded bg-bg-sunken motion-safe:animate-vc-shimmer" />
|
|
278
|
+
<div className="h-8 w-32 rounded bg-bg-sunken motion-safe:animate-vc-shimmer" />
|
|
279
|
+
<div className="h-3 w-20 rounded bg-bg-sunken motion-safe:animate-vc-shimmer" />
|
|
180
280
|
</div>
|
|
181
|
-
<div
|
|
182
|
-
className={cn('bg-bg-sunken motion-safe:animate-vc-shimmer rounded-lg', styles.icon)}
|
|
183
|
-
/>
|
|
281
|
+
<div className={cn('rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer', styles.icon)} />
|
|
184
282
|
</div>
|
|
185
283
|
</div>
|
|
186
284
|
</GlassCard>
|
|
@@ -66,7 +66,8 @@ export function StatusBadge({
|
|
|
66
66
|
<Badge
|
|
67
67
|
variant="outline"
|
|
68
68
|
className={cn(
|
|
69
|
-
'
|
|
69
|
+
'inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border-0 font-medium capitalize',
|
|
70
|
+
'ring-1 ring-inset ring-border-seam/60',
|
|
70
71
|
colors.bg,
|
|
71
72
|
colors.text,
|
|
72
73
|
sizeClasses[size],
|
|
@@ -77,7 +78,7 @@ export function StatusBadge({
|
|
|
77
78
|
<span
|
|
78
79
|
aria-hidden="true"
|
|
79
80
|
className={cn(
|
|
80
|
-
'
|
|
81
|
+
'h-1.5 w-1.5 flex-shrink-0 rounded-full motion-safe:animate-vc-glow-pulse',
|
|
81
82
|
colors.text.replace('text-', 'bg-')
|
|
82
83
|
)}
|
|
83
84
|
/>
|
|
@@ -105,7 +106,7 @@ export function PriorityBadge({
|
|
|
105
106
|
<Badge
|
|
106
107
|
variant="outline"
|
|
107
108
|
className={cn(
|
|
108
|
-
'font-medium capitalize border-0 whitespace-nowrap',
|
|
109
|
+
'rounded-full font-medium capitalize border-0 whitespace-nowrap ring-1 ring-inset ring-border-seam/60',
|
|
109
110
|
colors.bg,
|
|
110
111
|
colors.text,
|
|
111
112
|
sizeClasses[size],
|
|
@@ -140,7 +141,7 @@ export function TypeBadge({
|
|
|
140
141
|
<Badge
|
|
141
142
|
variant="outline"
|
|
142
143
|
className={cn(
|
|
143
|
-
'font-medium uppercase border-0 whitespace-nowrap',
|
|
144
|
+
'rounded-full font-medium uppercase border-0 whitespace-nowrap ring-1 ring-inset ring-border-seam/60',
|
|
144
145
|
colors.bg,
|
|
145
146
|
colors.text,
|
|
146
147
|
sizeClasses[size],
|
|
@@ -177,7 +178,7 @@ export function PaymentStatusBadge({
|
|
|
177
178
|
<Badge
|
|
178
179
|
variant="outline"
|
|
179
180
|
className={cn(
|
|
180
|
-
'font-medium capitalize border-0 whitespace-nowrap',
|
|
181
|
+
'rounded-full font-medium capitalize border-0 whitespace-nowrap ring-1 ring-inset ring-border-seam/60',
|
|
181
182
|
colors.bg,
|
|
182
183
|
colors.text,
|
|
183
184
|
sizeClasses[size],
|
|
@@ -213,7 +214,7 @@ export function VehicleTypeBadge({
|
|
|
213
214
|
<Badge
|
|
214
215
|
variant="outline"
|
|
215
216
|
className={cn(
|
|
216
|
-
'font-medium capitalize whitespace-nowrap inline-flex items-center',
|
|
217
|
+
'rounded-full font-medium capitalize whitespace-nowrap inline-flex items-center ring-1 ring-inset ring-border-seam/60',
|
|
217
218
|
sizeClasses[size],
|
|
218
219
|
className
|
|
219
220
|
)}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../../utils/cn';
|
|
3
|
+
import { chartTokens } from './chartTokens';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Small on-brand empty state used by every chart when there is no data.
|
|
7
|
+
* Honest: we never fabricate a placeholder series.
|
|
8
|
+
*/
|
|
9
|
+
export function ChartEmpty({
|
|
10
|
+
message = 'No data yet',
|
|
11
|
+
height,
|
|
12
|
+
className,
|
|
13
|
+
}: {
|
|
14
|
+
message?: string;
|
|
15
|
+
height?: number | string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={cn(
|
|
21
|
+
'flex items-center justify-center rounded-2xl border border-dashed border-border-seam bg-bg-sunken/40 text-sm text-text-secondary',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
style={{ height: height ?? '100%', minHeight: 120 }}
|
|
25
|
+
>
|
|
26
|
+
{message}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ChartTooltipPayloadItem {
|
|
32
|
+
name?: string | number;
|
|
33
|
+
value?: string | number;
|
|
34
|
+
color?: string;
|
|
35
|
+
dataKey?: string | number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Shared, token-styled recharts tooltip. Pass an optional value formatter.
|
|
40
|
+
*/
|
|
41
|
+
export function ChartTooltip({
|
|
42
|
+
active,
|
|
43
|
+
payload,
|
|
44
|
+
label,
|
|
45
|
+
formatter,
|
|
46
|
+
}: {
|
|
47
|
+
active?: boolean;
|
|
48
|
+
payload?: ChartTooltipPayloadItem[];
|
|
49
|
+
label?: ReactNode;
|
|
50
|
+
formatter?: (value: number | string, name: string) => ReactNode;
|
|
51
|
+
}) {
|
|
52
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="rounded-xl border border-border-seam bg-bg-surface/95 px-3 py-2 shadow-elevation-2 backdrop-blur-sm">
|
|
56
|
+
{label != null && label !== '' && (
|
|
57
|
+
<p className="mb-1 text-xs font-medium text-text-secondary">{label}</p>
|
|
58
|
+
)}
|
|
59
|
+
<div className="space-y-1">
|
|
60
|
+
{payload.map((item, i) => (
|
|
61
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
62
|
+
<span
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
className="h-2 w-2 shrink-0 rounded-full"
|
|
65
|
+
style={{ background: item.color ?? chartTokens.brandPrimary }}
|
|
66
|
+
/>
|
|
67
|
+
<span className="text-text-secondary">{item.name}</span>
|
|
68
|
+
<span className="ml-auto font-semibold tabular-nums text-text-primary">
|
|
69
|
+
{formatter && item.value != null
|
|
70
|
+
? formatter(item.value, String(item.name ?? ''))
|
|
71
|
+
: item.value}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
import { Area, AreaChart, Line, LineChart, ResponsiveContainer, YAxis } from 'recharts';
|
|
3
|
+
import { chartTokens } from './chartTokens';
|
|
4
|
+
|
|
5
|
+
export interface SparklinePoint {
|
|
6
|
+
value: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SparklineProps {
|
|
10
|
+
/** Series values — plain numbers or `{ value }` points. */
|
|
11
|
+
data: Array<number | SparklinePoint>;
|
|
12
|
+
/** Chart color (defaults to brand orange). Sourced from a design token. */
|
|
13
|
+
color?: string;
|
|
14
|
+
/** Render style. */
|
|
15
|
+
variant?: 'area' | 'line';
|
|
16
|
+
height?: number;
|
|
17
|
+
width?: number | string;
|
|
18
|
+
className?: string;
|
|
19
|
+
strokeWidth?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sparkline — a tiny, axis-less trend chart for stat cards. Responsive within
|
|
24
|
+
* its container; renders nothing (an empty spacer) when there is no data so the
|
|
25
|
+
* card layout stays stable.
|
|
26
|
+
*/
|
|
27
|
+
export function Sparkline({
|
|
28
|
+
data,
|
|
29
|
+
color = chartTokens.brandPrimary,
|
|
30
|
+
variant = 'area',
|
|
31
|
+
height = 40,
|
|
32
|
+
width = '100%',
|
|
33
|
+
className,
|
|
34
|
+
strokeWidth = 2,
|
|
35
|
+
}: SparklineProps) {
|
|
36
|
+
const gradientId = useId().replace(/:/g, '');
|
|
37
|
+
|
|
38
|
+
const points = data.map((d, i) => ({
|
|
39
|
+
i,
|
|
40
|
+
value: typeof d === 'number' ? d : d.value,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
if (points.length < 2) {
|
|
44
|
+
return <div style={{ height, width }} className={className} aria-hidden="true" />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ height, width }} className={className}>
|
|
49
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
50
|
+
{variant === 'area' ? (
|
|
51
|
+
<AreaChart data={points} margin={{ top: 2, right: 0, bottom: 2, left: 0 }}>
|
|
52
|
+
<defs>
|
|
53
|
+
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
|
54
|
+
<stop offset="0%" stopColor={color} stopOpacity={0.35} />
|
|
55
|
+
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
|
56
|
+
</linearGradient>
|
|
57
|
+
</defs>
|
|
58
|
+
<YAxis hide domain={['dataMin', 'dataMax']} />
|
|
59
|
+
<Area
|
|
60
|
+
type="monotone"
|
|
61
|
+
dataKey="value"
|
|
62
|
+
stroke={color}
|
|
63
|
+
strokeWidth={strokeWidth}
|
|
64
|
+
fill={`url(#${gradientId})`}
|
|
65
|
+
isAnimationActive={false}
|
|
66
|
+
dot={false}
|
|
67
|
+
/>
|
|
68
|
+
</AreaChart>
|
|
69
|
+
) : (
|
|
70
|
+
<LineChart data={points} margin={{ top: 2, right: 0, bottom: 2, left: 0 }}>
|
|
71
|
+
<YAxis hide domain={['dataMin', 'dataMax']} />
|
|
72
|
+
<Line
|
|
73
|
+
type="monotone"
|
|
74
|
+
dataKey="value"
|
|
75
|
+
stroke={color}
|
|
76
|
+
strokeWidth={strokeWidth}
|
|
77
|
+
isAnimationActive={false}
|
|
78
|
+
dot={false}
|
|
79
|
+
/>
|
|
80
|
+
</LineChart>
|
|
81
|
+
)}
|
|
82
|
+
</ResponsiveContainer>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
|
2
|
+
import { cn } from '../../utils/cn';
|
|
3
|
+
import { seriesColor } from './chartTokens';
|
|
4
|
+
import { ChartEmpty, ChartTooltip } from './ChartShell';
|
|
5
|
+
|
|
6
|
+
export interface DonutSlice {
|
|
7
|
+
name: string;
|
|
8
|
+
value: number;
|
|
9
|
+
/** Optional explicit color (design-token string). */
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface StatusDonutProps {
|
|
14
|
+
data: DonutSlice[];
|
|
15
|
+
height?: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
/** Big number rendered in the donut hole (e.g. a total). */
|
|
18
|
+
centerLabel?: string | number;
|
|
19
|
+
centerSubLabel?: string;
|
|
20
|
+
showLegend?: boolean;
|
|
21
|
+
emptyMessage?: string;
|
|
22
|
+
valueFormatter?: (value: number) => string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* StatusDonut — donut chart with a centered total and a token-styled legend.
|
|
27
|
+
* Responsive; renders an honest empty state when every slice is zero/absent.
|
|
28
|
+
*/
|
|
29
|
+
export function StatusDonut({
|
|
30
|
+
data,
|
|
31
|
+
height = 240,
|
|
32
|
+
className,
|
|
33
|
+
centerLabel,
|
|
34
|
+
centerSubLabel,
|
|
35
|
+
showLegend = true,
|
|
36
|
+
emptyMessage = 'No breakdown yet',
|
|
37
|
+
valueFormatter,
|
|
38
|
+
}: StatusDonutProps) {
|
|
39
|
+
const slices = (data ?? []).filter((d) => d.value > 0);
|
|
40
|
+
const total = slices.reduce((sum, d) => sum + d.value, 0);
|
|
41
|
+
|
|
42
|
+
if (slices.length === 0 || total === 0) {
|
|
43
|
+
return <ChartEmpty message={emptyMessage} height={height} className={className} />;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const colorOf = (slice: DonutSlice, i: number) => slice.color ?? seriesColor(i);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={cn('flex flex-col gap-4 sm:flex-row sm:items-center', className)}>
|
|
50
|
+
<div className="relative mx-auto" style={{ width: height, height, maxWidth: '100%' }}>
|
|
51
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
52
|
+
<PieChart>
|
|
53
|
+
<Pie
|
|
54
|
+
data={slices}
|
|
55
|
+
dataKey="value"
|
|
56
|
+
nameKey="name"
|
|
57
|
+
innerRadius="62%"
|
|
58
|
+
outerRadius="100%"
|
|
59
|
+
paddingAngle={2}
|
|
60
|
+
stroke="none"
|
|
61
|
+
isAnimationActive={false}
|
|
62
|
+
>
|
|
63
|
+
{slices.map((slice, i) => (
|
|
64
|
+
<Cell key={slice.name} fill={colorOf(slice, i)} />
|
|
65
|
+
))}
|
|
66
|
+
</Pie>
|
|
67
|
+
<Tooltip
|
|
68
|
+
content={
|
|
69
|
+
<ChartTooltip
|
|
70
|
+
formatter={(v) =>
|
|
71
|
+
valueFormatter ? valueFormatter(Number(v)) : String(v)
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
</PieChart>
|
|
77
|
+
</ResponsiveContainer>
|
|
78
|
+
{centerLabel != null && (
|
|
79
|
+
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center">
|
|
80
|
+
<span className="text-2xl font-bold tabular-nums text-text-primary">
|
|
81
|
+
{centerLabel}
|
|
82
|
+
</span>
|
|
83
|
+
{centerSubLabel && (
|
|
84
|
+
<span className="text-xs text-text-secondary">{centerSubLabel}</span>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{showLegend && (
|
|
91
|
+
<ul className="flex flex-1 flex-col gap-2">
|
|
92
|
+
{slices.map((slice, i) => {
|
|
93
|
+
const pct = total > 0 ? Math.round((slice.value / total) * 100) : 0;
|
|
94
|
+
return (
|
|
95
|
+
<li key={slice.name} className="flex items-center gap-2 text-sm">
|
|
96
|
+
<span
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
99
|
+
style={{ background: colorOf(slice, i) }}
|
|
100
|
+
/>
|
|
101
|
+
<span className="text-text-secondary">{slice.name}</span>
|
|
102
|
+
<span className="ml-auto font-semibold tabular-nums text-text-primary">
|
|
103
|
+
{valueFormatter ? valueFormatter(slice.value) : slice.value}
|
|
104
|
+
</span>
|
|
105
|
+
<span className="w-10 text-right text-xs tabular-nums text-text-secondary">
|
|
106
|
+
{pct}%
|
|
107
|
+
</span>
|
|
108
|
+
</li>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</ul>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|