@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.
Files changed (150) hide show
  1. package/dist/{AIAssistantPage-3YOnHO9Y.js → AIAssistantPage-BmILdHgO.js} +16 -15
  2. package/dist/AnalyticsPage-BSlsiSDE.js +194 -0
  3. package/dist/{CreateOrderPage-DRAgGo9i.js → CreateOrderPage-DaCgVaO3.js} +40 -40
  4. package/dist/{CustomerDetailsPage-BCeMmOFe.js → CustomerDetailsPage-DErpbafW.js} +71 -71
  5. package/dist/CustomersPage-C1lRU6xs.js +205 -0
  6. package/dist/DashboardPage-Q5-5EKAU.js +542 -0
  7. package/dist/{DataTable-XvUcTjwi.js → DataTable-KjzbwnRL.js} +31 -31
  8. package/dist/{DocumentsCard-U2lSe-cn.js → DocumentsCard-T9y6cv4-.js} +25 -25
  9. package/dist/{DriverDetailsPage-BQrYd_Lp.js → DriverDetailsPage-DnHGkDC7.js} +64 -64
  10. package/dist/DriversPage-C0ujWVOn.js +221 -0
  11. package/dist/FinancePage-CA2XEgsQ.js +175 -0
  12. package/dist/{FleetPage-3ZHsB-TN.js → FleetPage-DrvXW7wK.js} +22 -22
  13. package/dist/{FormError-cuLV2jVD.js → FormError-CRsMP7TQ.js} +1 -1
  14. package/dist/{ImportExportPage-C6f7DqhM.js → ImportExportPage-Cac9HjFx.js} +12 -12
  15. package/dist/InventoryPage-Ezf4gqKI.js +259 -0
  16. package/dist/LiveTrackingPage-CzbVwDqK.js +429 -0
  17. package/dist/{MarketplacePage-Bh_2ns4v.js → MarketplacePage-BHDmSQmo.js} +116 -116
  18. package/dist/MetricCard-DQlDNqmK.js +191 -0
  19. package/dist/{Notice-CSoDYKw9.js → Notice-GmA7JDR3.js} +2 -2
  20. package/dist/{OrderDetailsPage-zCj0lW0j.js → OrderDetailsPage-CR0Wc1aN.js} +58 -58
  21. package/dist/OrdersListPage-C8Bu6wXM.js +386 -0
  22. package/dist/{PageLayout-D4LDHJJq.js → PageLayout-DX1Ou6V6.js} +164 -134
  23. package/dist/{ProductDetailsPage-wsxAQkA0.js → ProductDetailsPage-B_ns2MOe.js} +55 -55
  24. package/dist/ProductsPage-c92BsEoA.js +186 -0
  25. package/dist/ReportsPage-CPLXtLT2.js +377 -0
  26. package/dist/{RouteDetailsPage-DyMJE9AS.js → RouteDetailsPage-DOfcRiFv.js} +208 -181
  27. package/dist/{RoutesPage-Dtc669yc.js → RoutesPage-BZGublby.js} +12 -12
  28. package/dist/{SettingsPage-CpH9xhQg.js → SettingsPage-5cweX_xh.js} +15 -15
  29. package/dist/Sparkline-zSXvG6vA.js +16729 -0
  30. package/dist/{StatusBadge-C87mDpDP.js → StatusBadge-oZjownr1.js} +7 -7
  31. package/dist/TrackingPage-Ln9qVflK.js +368 -0
  32. package/dist/{VehicleDetailsPage-DldYNsYV.js → VehicleDetailsPage-CrkzE0dN.js} +53 -53
  33. package/dist/VehiclesPage-CTQWH7af.js +184 -0
  34. package/dist/{WarehouseDetailsPage-CMG7or6G.js → WarehouseDetailsPage-CwDLaG65.js} +64 -64
  35. package/dist/WarehousesPage-KW02DvZg.js +182 -0
  36. package/dist/{arrow-left-n48Y-Wis.js → arrow-left-DJDw_Yqv.js} +1 -1
  37. package/dist/{arrow-right-Q3XQpTPR.js → arrow-right-Cb2fLTPJ.js} +1 -1
  38. package/dist/{arrow-up-xNF1zJUG.js → arrow-up-PxaOIyiT.js} +1 -1
  39. package/dist/{box-Eu05PXDh.js → box-hSJVtvGf.js} +1 -1
  40. package/dist/boxes-DDnxEkT5.js +54 -0
  41. package/dist/{chart-column-ouzgxIv_.js → chart-column-xXgAZvRl.js} +1 -1
  42. package/dist/charts-CfCwQg6o.js +2018 -0
  43. package/dist/{chevron-left-B2_r0Elx.js → chevron-left-dhEGJYdh.js} +1 -1
  44. package/dist/{chevron-right-eV4LjaSi.js → chevron-right-Cud2NDvm.js} +1 -1
  45. package/dist/chunk-BdsCSvBP.js +20 -0
  46. package/dist/{circle-alert-Bzh2DBSF.js → circle-alert-Rx0p3D5w.js} +1 -1
  47. package/dist/circle-check-BCxRFdFo.js +13 -0
  48. package/dist/{circle-check-big-CGMSpQmD.js → circle-check-big-BcIJ8xjx.js} +1 -1
  49. package/dist/{clock-Bl3ptexL.js → clock-B2_YxJgB.js} +1 -1
  50. package/dist/{dollar-sign-CAVU95SO.js → dollar-sign-CQmAICv4.js} +1 -1
  51. package/dist/{download-C5NbvVDc.js → download-CG4Tponq.js} +1 -1
  52. package/dist/{file-text-D-1uvLZG.js → file-text-CzH-zMub.js} +1 -1
  53. package/dist/{filter-DUfUg6BO.js → filter-C0u2Lwfn.js} +1 -1
  54. package/dist/{fuel-B6_lnKDs.js → fuel-C2KELcM-.js} +1 -1
  55. package/dist/index.css +2 -0
  56. package/dist/index.js +214 -225
  57. package/dist/{map-C-M-FQLR.js → map-FuPu5aTw.js} +1 -1
  58. package/dist/{map-pin-Dznl5l40.js → map-pin-czpLv9uy.js} +1 -1
  59. package/dist/mapMarkers-NCV9MRKR.js +28 -0
  60. package/dist/maps-D9m7PKrs.js +5070 -0
  61. package/dist/{navigation-BJSJl-VG.js → navigation-D5_pdCSS.js} +1 -1
  62. package/dist/{package-CcRttV2N.js → package-B0sdi6lU.js} +1 -1
  63. package/dist/{phone-DVSZHt_j.js → phone-V2nuzPu9.js} +1 -1
  64. package/dist/{plus-C_XZDwBz.js → plus-BqQxkfoU.js} +1 -1
  65. package/dist/{refresh-cw-CfgN67l3.js → refresh-cw-CEOAPEcV.js} +1 -1
  66. package/dist/route-CZWcW-Dw.js +22 -0
  67. package/dist/{save-DJE8Fpdv.js → save-DDFvrxWl.js} +1 -1
  68. package/dist/{search-D_VyGcZT.js → search-D7Ya6jxj.js} +1 -1
  69. package/dist/{square-pen-D-6JYrVQ.js → square-pen-DoZZS4Lw.js} +1 -1
  70. package/dist/{star-BpRlUZuU.js → star-B7PgVu5j.js} +1 -1
  71. package/dist/{store-B-8HMuyZ.js → store-YVzcIG4A.js} +1 -1
  72. package/dist/{trash-2-8vmv-XV_.js → trash-2-BegnBzwI.js} +1 -1
  73. package/dist/{trending-up-Dz6PqPRj.js → trending-up-DBd3XNdb.js} +1 -1
  74. package/dist/{triangle-alert-D7Y3J_4z.js → triangle-alert-Bxk3Kgs6.js} +1 -1
  75. package/dist/{truck-D-y2IDzv.js → truck-Clrn0oHp.js} +1 -1
  76. package/dist/{upload-CHZofOqb.js → upload-Df5JmMwH.js} +1 -1
  77. package/dist/{useAssistant-DVW4QgQb.js → useAssistant-Cb8nnIVC.js} +11 -28
  78. package/dist/{useCustomers-BPW4__PR.js → useCustomers-Dq2IR2HQ.js} +1 -1
  79. package/dist/{useFleet-ChBkxAd6.js → useFleet-CapnWBcA.js} +1 -1
  80. package/dist/{useImportExport-DrPDHEDK.js → useImportExport-BoGzrlJy.js} +1 -1
  81. package/dist/{useInventory-CAAVMRp6.js → useInventory-Ci6M2d9f.js} +1 -1
  82. package/dist/{useOrders-B465S2nt.js → useOrders-BuQU1OHX.js} +1 -1
  83. package/dist/{useReports-jQ3GpNOm.js → useReports-DIx8W7CO.js} +1 -1
  84. package/dist/{useRoutes-hS3d5-xr.js → useRoutes-D7EvI_pm.js} +1 -1
  85. package/dist/{useSettings-BB8X2RLh.js → useSettings-BhX251UQ.js} +6 -6
  86. package/dist/{useTracking-RjGz9PZG.js → useTracking-C40jmZ6W.js} +3 -3
  87. package/dist/{user-DzfTn4dE.js → user-BlcJrbtE.js} +1 -1
  88. package/dist/{user-check-BEKRCi7v.js → user-check-DtnQmimP.js} +1 -1
  89. package/dist/{users-CCrq-w0C.js → users-Cvwko-Dx.js} +1 -1
  90. package/dist/{warehouse-CaqfmMKh.js → warehouse-LWLVTk6G.js} +1 -1
  91. package/dist/{wrench-q_rgJfaP.js → wrench-Cme0RbyR.js} +1 -1
  92. package/dist/{x-tMeDMcCw.js → x-D8C9Pg5V.js} +1 -1
  93. package/package.json +5 -1
  94. package/src/movethewheels/components/GlassPanel.tsx +71 -0
  95. package/src/movethewheels/components/MetricCard.tsx +171 -73
  96. package/src/movethewheels/components/StatusBadge.tsx +7 -6
  97. package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
  98. package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
  99. package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
  100. package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
  101. package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
  102. package/src/movethewheels/components/charts/chartTokens.ts +43 -0
  103. package/src/movethewheels/components/charts/index.ts +9 -0
  104. package/src/movethewheels/components/index.ts +44 -1
  105. package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
  106. package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
  107. package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
  108. package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
  109. package/src/movethewheels/components/maps/index.ts +16 -0
  110. package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
  111. package/src/movethewheels/components/maps/map-markers.css +121 -0
  112. package/src/movethewheels/hooks/useAnalytics.ts +14 -4
  113. package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
  114. package/src/movethewheels/pages/CreateOrderPage.tsx +4 -4
  115. package/src/movethewheels/pages/CustomersPage.tsx +29 -20
  116. package/src/movethewheels/pages/DashboardPage.tsx +238 -12
  117. package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
  118. package/src/movethewheels/pages/DriversPage.tsx +35 -24
  119. package/src/movethewheels/pages/FinancePage.tsx +68 -30
  120. package/src/movethewheels/pages/InventoryPage.tsx +15 -3
  121. package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
  122. package/src/movethewheels/pages/OrdersListPage.tsx +46 -38
  123. package/src/movethewheels/pages/ProductsPage.tsx +28 -24
  124. package/src/movethewheels/pages/ReportsPage.tsx +23 -0
  125. package/src/movethewheels/pages/RouteDetailsPage.tsx +46 -7
  126. package/src/movethewheels/pages/SettingsPage.tsx +2 -2
  127. package/src/movethewheels/pages/TrackingPage.tsx +76 -6
  128. package/src/movethewheels/pages/VehicleDetailsPage.tsx +1 -1
  129. package/src/movethewheels/pages/VehiclesPage.tsx +34 -24
  130. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
  131. package/src/movethewheels/pages/WarehousesPage.tsx +28 -22
  132. package/src/movethewheels/types/index.ts +1 -1
  133. package/src/movethewheels/utils/graphqlClient.ts +81 -31
  134. package/src/movethewheels/utils/mapMarkers.ts +58 -0
  135. package/dist/AnalyticsPage-CMpI1070.js +0 -215
  136. package/dist/CustomersPage-CaNBMXmM.js +0 -172
  137. package/dist/DashboardPage-DIKlXde3.js +0 -387
  138. package/dist/DriversPage-PYmdFiby.js +0 -176
  139. package/dist/FinancePage-Cwh9Bw4M.js +0 -136
  140. package/dist/InventoryPage-DpiqVu1v.js +0 -253
  141. package/dist/LiveTrackingPage-FAzzlVnX.js +0 -379
  142. package/dist/MetricCard-CVBVpu7o.js +0 -146
  143. package/dist/OrdersListPage-CAMsuUck.js +0 -384
  144. package/dist/ProductsPage-DC5qDLfz.js +0 -180
  145. package/dist/ReportsPage-BwUJ37eO.js +0 -356
  146. package/dist/TrackingPage-DaNEFG_h.js +0 -325
  147. package/dist/VehiclesPage-BFw13iMt.js +0 -176
  148. package/dist/WarehousesPage-Wkytnt86.js +0 -173
  149. package/dist/useAnalytics-D7HW2XRb.js +0 -347
  150. /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
- card: 'p-3',
35
- value: 'text-xl font-bold',
36
- label: 'text-xs',
37
- icon: 'w-8 h-8',
38
- },
39
- md: {
40
- card: 'p-4',
41
- value: 'text-2xl font-bold',
42
- label: 'text-sm',
43
- icon: 'w-10 h-10',
44
- },
45
- lg: {
46
- card: 'p-6',
47
- value: 'text-3xl font-bold',
48
- label: 'text-base',
49
- icon: 'w-12 h-12',
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 - Displays a key metric with optional change indicator
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 changeColors = {
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 trendIcons = {
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('text-text-primary tracking-tight tabular-nums', styles.value)}>
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-shrink-0 p-2 rounded-lg bg-action-primary-bg/10 text-action-primary-bg',
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 flex-1">
177
- <div className="h-4 w-24 bg-bg-sunken motion-safe:animate-vc-shimmer rounded" />
178
- <div className="h-8 w-32 bg-bg-sunken motion-safe:animate-vc-shimmer rounded" />
179
- <div className="h-3 w-20 bg-bg-sunken motion-safe:animate-vc-shimmer rounded" />
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
- 'font-medium capitalize border-0 whitespace-nowrap inline-flex items-center',
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
- 'w-1.5 h-1.5 rounded-full mr-1.5 flex-shrink-0 motion-safe:animate-vc-glow-pulse',
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
+ }