@burdenoff/microfe-movethewheels 2026.703.3 → 2026.703.5

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 (173) hide show
  1. package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-D2gaEL1U.js} +16 -15
  2. package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
  3. package/dist/CreateOrderPage-CWHx8yys.js +490 -0
  4. package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
  5. package/dist/CustomersPage-Bbvwvn8k.js +205 -0
  6. package/dist/DashboardPage-DrEhNf7N.js +542 -0
  7. package/dist/DataTable-CqMUD4er.js +340 -0
  8. package/dist/DocumentsCard-edDkgnJj.js +169 -0
  9. package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
  10. package/dist/DriversPage-CHF2ZEzd.js +221 -0
  11. package/dist/FinancePage-Dn9GrW-1.js +175 -0
  12. package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
  13. package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
  14. package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
  15. package/dist/InventoryPage-l5A1f753.js +259 -0
  16. package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
  17. package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
  18. package/dist/MetricCard-y3bWHKUQ.js +191 -0
  19. package/dist/Notice-DOO-tjpV.js +46 -0
  20. package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
  21. package/dist/OrdersListPage-DhZJoQeI.js +386 -0
  22. package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
  23. package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
  24. package/dist/ProductsPage-C8sn-1tN.js +186 -0
  25. package/dist/ReportsPage-D1tb0dcz.js +377 -0
  26. package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
  27. package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
  28. package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
  29. package/dist/Sparkline-K2RMJ58u.js +16729 -0
  30. package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
  31. package/dist/TrackingPage-BZrg4RCu.js +368 -0
  32. package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
  33. package/dist/VehiclesPage-D5BELbtM.js +184 -0
  34. package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
  35. package/dist/WarehousesPage-C1Y1M867.js +182 -0
  36. package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
  37. package/dist/arrow-right-CmKOmIkh.js +11 -0
  38. package/dist/arrow-up-BJijRLAZ.js +17 -0
  39. package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
  40. package/dist/boxes-B_OGvQiW.js +54 -0
  41. package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
  42. package/dist/charts-Tj_PPTfA.js +2018 -0
  43. package/dist/chevron-left-B1rZyWWb.js +8 -0
  44. package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
  45. package/dist/chunk-BdsCSvBP.js +20 -0
  46. package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
  47. package/dist/circle-check-Cs1OVjtV.js +13 -0
  48. package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
  49. package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
  50. package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
  51. package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
  52. package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
  53. package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
  54. package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
  55. package/dist/index.css +2 -0
  56. package/dist/index.js +150 -172
  57. package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
  58. package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
  59. package/dist/mapMarkers-NCV9MRKR.js +28 -0
  60. package/dist/maps-yzLqiK_C.js +5070 -0
  61. package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
  62. package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
  63. package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
  64. package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
  65. package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
  66. package/dist/route-CWjPrtAN.js +22 -0
  67. package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
  68. package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
  69. package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
  70. package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
  71. package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
  72. package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
  73. package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
  74. package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
  75. package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
  76. package/dist/upload-DanJY1Vo.js +21 -0
  77. package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
  78. package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
  79. package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
  80. package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
  81. package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
  82. package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
  83. package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
  84. package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
  85. package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
  86. package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
  87. package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
  88. package/dist/user-check-IMhk-qkX.js +20 -0
  89. package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
  90. package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
  91. package/dist/wrench-Dg-sgH2e.js +8 -0
  92. package/dist/x-C4ObLVEa.js +11 -0
  93. package/package.json +5 -1
  94. package/src/movethewheels/components/DataTable.tsx +252 -84
  95. package/src/movethewheels/components/DocumentsCard.tsx +102 -0
  96. package/src/movethewheels/components/GlassPanel.tsx +71 -0
  97. package/src/movethewheels/components/MetricCard.tsx +171 -73
  98. package/src/movethewheels/components/Notice.tsx +61 -0
  99. package/src/movethewheels/components/StatusBadge.tsx +7 -6
  100. package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
  101. package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
  102. package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
  103. package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
  104. package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
  105. package/src/movethewheels/components/charts/chartTokens.ts +43 -0
  106. package/src/movethewheels/components/charts/index.ts +9 -0
  107. package/src/movethewheels/components/index.ts +51 -1
  108. package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
  109. package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
  110. package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
  111. package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
  112. package/src/movethewheels/components/maps/index.ts +16 -0
  113. package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
  114. package/src/movethewheels/components/maps/map-markers.css +121 -0
  115. package/src/movethewheels/hooks/index.ts +9 -0
  116. package/src/movethewheels/hooks/useAnalytics.ts +109 -4
  117. package/src/movethewheels/hooks/useCustomers.ts +11 -4
  118. package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
  119. package/src/movethewheels/hooks/useFleet.ts +26 -7
  120. package/src/movethewheels/hooks/useInventory.ts +70 -7
  121. package/src/movethewheels/hooks/useOrders.ts +32 -2
  122. package/src/movethewheels/hooks/useReports.ts +14 -4
  123. package/src/movethewheels/hooks/useRoutes.ts +20 -3
  124. package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
  125. package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
  126. package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
  127. package/src/movethewheels/pages/CustomersPage.tsx +53 -28
  128. package/src/movethewheels/pages/DashboardPage.tsx +238 -12
  129. package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
  130. package/src/movethewheels/pages/DriversPage.tsx +59 -31
  131. package/src/movethewheels/pages/FinancePage.tsx +103 -82
  132. package/src/movethewheels/pages/InventoryPage.tsx +15 -3
  133. package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
  134. package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
  135. package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
  136. package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
  137. package/src/movethewheels/pages/ProductsPage.tsx +52 -28
  138. package/src/movethewheels/pages/ReportsPage.tsx +88 -4
  139. package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
  140. package/src/movethewheels/pages/RoutesPage.tsx +22 -6
  141. package/src/movethewheels/pages/SettingsPage.tsx +5 -21
  142. package/src/movethewheels/pages/TrackingPage.tsx +76 -6
  143. package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
  144. package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
  145. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
  146. package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
  147. package/src/movethewheels/types/index.ts +9 -1
  148. package/src/movethewheels/utils/download.ts +62 -0
  149. package/src/movethewheels/utils/mapMarkers.ts +58 -0
  150. package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
  151. package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
  152. package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
  153. package/dist/CustomersPage-BKxMS1AV.js +0 -161
  154. package/dist/DashboardPage-BVjQi0EB.js +0 -387
  155. package/dist/DataTable-DmciIKKM.js +0 -249
  156. package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
  157. package/dist/DriversPage-CH9rjQC-.js +0 -165
  158. package/dist/FinancePage-DEDykSOP.js +0 -158
  159. package/dist/InventoryPage-e0ze63iX.js +0 -253
  160. package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
  161. package/dist/MarketplacePage-CIQ97_zp.js +0 -247
  162. package/dist/MetricCard-BY0QrMD4.js +0 -146
  163. package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
  164. package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
  165. package/dist/ProductsPage-QJfHHpqB.js +0 -169
  166. package/dist/ReportsPage-Zp53q3_8.js +0 -284
  167. package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
  168. package/dist/TrackingPage-W-jtdM3Y.js +0 -325
  169. package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
  170. package/dist/VehiclesPage-ApKThYeG.js +0 -165
  171. package/dist/WarehousesPage-DCbXXXWE.js +0 -162
  172. package/dist/useAnalytics-DS8n0xgg.js +0 -297
  173. /package/dist/{formatters-CF2S2l1m.js → formatters-FWJDwTFv.js} +0 -0
@@ -0,0 +1,71 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import { GlassCard } from '@burdenoff/fe-libs/ui';
3
+ import { cn } from '../utils/cn';
4
+
5
+ interface GlassPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
6
+ /** Optional panel title rendered in a compact header row. */
7
+ title?: ReactNode;
8
+ /** Optional right-aligned header slot (actions, filters, legend chips). */
9
+ action?: ReactNode;
10
+ /** Optional supporting text under the title. */
11
+ description?: ReactNode;
12
+ /** Inner padding. Defaults to `md`. Use `none` for edge-to-edge (maps/charts). */
13
+ padding?: 'none' | 'sm' | 'md' | 'lg';
14
+ treatment?: 'glass' | 'plain' | 'metric' | 'insight';
15
+ glow?: boolean;
16
+ children?: ReactNode;
17
+ /** Extra classes for the inner content wrapper (below the header). */
18
+ bodyClassName?: string;
19
+ }
20
+
21
+ const paddingClasses: Record<NonNullable<GlassPanelProps['padding']>, string> = {
22
+ none: 'p-0',
23
+ sm: 'p-3',
24
+ md: 'p-4 sm:p-5',
25
+ lg: 'p-6',
26
+ };
27
+
28
+ /**
29
+ * GlassPanel — a translucent, rounded surface wrapper with an optional header
30
+ * (title / description / action). The glassmorphism-lite building block for
31
+ * dashboard sections, map/chart cards, and side rails. Token-based, theme-aware,
32
+ * and follows the fe-libs no-movement hover policy.
33
+ */
34
+ export function GlassPanel({
35
+ title,
36
+ action,
37
+ description,
38
+ padding = 'md',
39
+ treatment = 'glass',
40
+ glow = false,
41
+ className,
42
+ bodyClassName,
43
+ children,
44
+ ...rest
45
+ }: GlassPanelProps) {
46
+ const hasHeader = title != null || action != null || description != null;
47
+
48
+ return (
49
+ <GlassCard
50
+ treatment={treatment}
51
+ glow={glow}
52
+ className={cn('overflow-hidden border-border-seam', className)}
53
+ {...rest}
54
+ >
55
+ {hasHeader && (
56
+ <div className="flex items-start justify-between gap-3 border-b border-border-seam/70 px-4 py-3 sm:px-5">
57
+ <div className="min-w-0 space-y-0.5">
58
+ {title != null && (
59
+ <h3 className="truncate text-sm font-semibold text-text-primary">{title}</h3>
60
+ )}
61
+ {description != null && (
62
+ <p className="text-xs text-text-secondary">{description}</p>
63
+ )}
64
+ </div>
65
+ {action != null && <div className="flex shrink-0 items-center gap-2">{action}</div>}
66
+ </div>
67
+ )}
68
+ <div className={cn(paddingClasses[padding], bodyClassName)}>{children}</div>
69
+ </GlassCard>
70
+ );
71
+ }
@@ -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>
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Notice — inline success / info feedback banner.
3
+ *
4
+ * The MoveTheWheels MFE does not mount its own sonner Toaster (mounting a second
5
+ * Toaster inside a host shell causes duplicate toasts), so action feedback is
6
+ * shown inline via this banner. Renders nothing when there is no message, so it
7
+ * can be dropped in unconditionally. Uses semantic status tokens so it themes
8
+ * correctly. Pair it with `useNotice` for auto-dismiss behaviour.
9
+ */
10
+
11
+ import { useCallback, useRef, useState } from 'react';
12
+ import { CheckCircle, Info } from 'lucide-react';
13
+
14
+ type NoticeVariant = 'success' | 'info';
15
+
16
+ interface NoticeProps {
17
+ message?: string | null;
18
+ variant?: NoticeVariant;
19
+ }
20
+
21
+ export function Notice({ message, variant = 'success' }: NoticeProps) {
22
+ if (!message) return null;
23
+ const isSuccess = variant === 'success';
24
+ return (
25
+ <div
26
+ role="status"
27
+ className={`flex items-start gap-2 rounded-lg border px-4 py-3 text-sm ${
28
+ isSuccess
29
+ ? 'border-status-success-border bg-status-success-bg-subtle text-status-success-text'
30
+ : 'border-status-info-border bg-status-info-bg-subtle text-status-info-text'
31
+ }`}
32
+ >
33
+ {isSuccess ? (
34
+ <CheckCircle size={18} className="mt-0.5 shrink-0" />
35
+ ) : (
36
+ <Info size={18} className="mt-0.5 shrink-0" />
37
+ )}
38
+ <span>{message}</span>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ /**
44
+ * useNotice — transient success/info message that auto-clears after a delay.
45
+ * Returns the current message plus a `notify` setter, mirroring a toast API.
46
+ */
47
+ export function useNotice(autoDismissMs = 4000) {
48
+ const [message, setMessage] = useState<string | null>(null);
49
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
50
+
51
+ const notify = useCallback(
52
+ (next: string) => {
53
+ setMessage(next);
54
+ if (timer.current) clearTimeout(timer.current);
55
+ timer.current = setTimeout(() => setMessage(null), autoDismissMs);
56
+ },
57
+ [autoDismissMs]
58
+ );
59
+
60
+ return { message, notify };
61
+ }
@@ -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
+ }