@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
@@ -0,0 +1,179 @@
1
+ import { useId } from 'react';
2
+ import {
3
+ Area,
4
+ AreaChart,
5
+ CartesianGrid,
6
+ Legend,
7
+ Line,
8
+ LineChart,
9
+ ResponsiveContainer,
10
+ Tooltip,
11
+ XAxis,
12
+ YAxis,
13
+ } from 'recharts';
14
+ import { chartTokens, seriesColor } from './chartTokens';
15
+ import { ChartEmpty, ChartTooltip } from './ChartShell';
16
+
17
+ export interface TrendSeries {
18
+ /** Key into each data row. */
19
+ dataKey: string;
20
+ /** Legend / tooltip label. */
21
+ name: string;
22
+ /** Explicit color (design-token string). Defaults to the categorical palette. */
23
+ color?: string;
24
+ }
25
+
26
+ interface TrendChartProps {
27
+ /** Row objects keyed by `xKey` + each series `dataKey`. */
28
+ data: Array<Record<string, string | number>>;
29
+ series: TrendSeries[];
30
+ /** Row key for the X axis (e.g. "date"). */
31
+ xKey: string;
32
+ variant?: 'area' | 'line';
33
+ height?: number;
34
+ className?: string;
35
+ showLegend?: boolean;
36
+ showGrid?: boolean;
37
+ emptyMessage?: string;
38
+ /** Optional Y/tooltip value formatter. */
39
+ valueFormatter?: (value: number | string, name: string) => string;
40
+ }
41
+
42
+ /**
43
+ * TrendChart — multi-series area/line chart for trends over time. Fully
44
+ * responsive (ResponsiveContainer), token-colored, with an honest empty state.
45
+ */
46
+ export function TrendChart({
47
+ data,
48
+ series,
49
+ xKey,
50
+ variant = 'area',
51
+ height = 300,
52
+ className,
53
+ showLegend = true,
54
+ showGrid = true,
55
+ emptyMessage = 'No trend data yet',
56
+ valueFormatter,
57
+ }: TrendChartProps) {
58
+ const rawId = useId().replace(/:/g, '');
59
+
60
+ if (!data || data.length === 0 || series.length === 0) {
61
+ return <ChartEmpty message={emptyMessage} height={height} className={className} />;
62
+ }
63
+
64
+ const axisTick = { fill: chartTokens.axis, fontSize: 12 };
65
+ const colorFor = (s: TrendSeries, i: number) => s.color ?? seriesColor(i);
66
+
67
+ return (
68
+ <div className={className} style={{ width: '100%', height }}>
69
+ <ResponsiveContainer width="100%" height="100%">
70
+ {variant === 'area' ? (
71
+ <AreaChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: -8 }}>
72
+ <defs>
73
+ {series.map((s, i) => (
74
+ <linearGradient
75
+ key={s.dataKey}
76
+ id={`${rawId}-${s.dataKey}`}
77
+ x1="0"
78
+ y1="0"
79
+ x2="0"
80
+ y2="1"
81
+ >
82
+ <stop offset="0%" stopColor={colorFor(s, i)} stopOpacity={0.28} />
83
+ <stop offset="100%" stopColor={colorFor(s, i)} stopOpacity={0.02} />
84
+ </linearGradient>
85
+ ))}
86
+ </defs>
87
+ {showGrid && (
88
+ <CartesianGrid strokeDasharray="3 3" stroke={chartTokens.grid} vertical={false} />
89
+ )}
90
+ <XAxis
91
+ dataKey={xKey}
92
+ tick={axisTick}
93
+ tickLine={false}
94
+ axisLine={{ stroke: chartTokens.grid }}
95
+ minTickGap={24}
96
+ />
97
+ <YAxis
98
+ tick={axisTick}
99
+ tickLine={false}
100
+ axisLine={false}
101
+ width={44}
102
+ tickFormatter={
103
+ valueFormatter ? (v: number) => valueFormatter(v, '') : undefined
104
+ }
105
+ />
106
+ <Tooltip
107
+ cursor={{ stroke: chartTokens.axis, strokeOpacity: 0.3 }}
108
+ content={<ChartTooltip formatter={valueFormatter} />}
109
+ />
110
+ {showLegend && (
111
+ <Legend
112
+ iconType="circle"
113
+ wrapperStyle={{ fontSize: 12, color: chartTokens.textSecondary }}
114
+ />
115
+ )}
116
+ {series.map((s, i) => (
117
+ <Area
118
+ key={s.dataKey}
119
+ type="monotone"
120
+ dataKey={s.dataKey}
121
+ name={s.name}
122
+ stroke={colorFor(s, i)}
123
+ strokeWidth={2.5}
124
+ fill={`url(#${rawId}-${s.dataKey})`}
125
+ isAnimationActive={false}
126
+ activeDot={{ r: 4, strokeWidth: 0 }}
127
+ />
128
+ ))}
129
+ </AreaChart>
130
+ ) : (
131
+ <LineChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: -8 }}>
132
+ {showGrid && (
133
+ <CartesianGrid strokeDasharray="3 3" stroke={chartTokens.grid} vertical={false} />
134
+ )}
135
+ <XAxis
136
+ dataKey={xKey}
137
+ tick={axisTick}
138
+ tickLine={false}
139
+ axisLine={{ stroke: chartTokens.grid }}
140
+ minTickGap={24}
141
+ />
142
+ <YAxis
143
+ tick={axisTick}
144
+ tickLine={false}
145
+ axisLine={false}
146
+ width={44}
147
+ tickFormatter={
148
+ valueFormatter ? (v: number) => valueFormatter(v, '') : undefined
149
+ }
150
+ />
151
+ <Tooltip
152
+ cursor={{ stroke: chartTokens.axis, strokeOpacity: 0.3 }}
153
+ content={<ChartTooltip formatter={valueFormatter} />}
154
+ />
155
+ {showLegend && (
156
+ <Legend
157
+ iconType="circle"
158
+ wrapperStyle={{ fontSize: 12, color: chartTokens.textSecondary }}
159
+ />
160
+ )}
161
+ {series.map((s, i) => (
162
+ <Line
163
+ key={s.dataKey}
164
+ type="monotone"
165
+ dataKey={s.dataKey}
166
+ name={s.name}
167
+ stroke={colorFor(s, i)}
168
+ strokeWidth={2.5}
169
+ dot={false}
170
+ isAnimationActive={false}
171
+ activeDot={{ r: 4, strokeWidth: 0 }}
172
+ />
173
+ ))}
174
+ </LineChart>
175
+ )}
176
+ </ResponsiveContainer>
177
+ </div>
178
+ );
179
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ Bar,
3
+ BarChart,
4
+ CartesianGrid,
5
+ Legend,
6
+ ResponsiveContainer,
7
+ Tooltip,
8
+ XAxis,
9
+ YAxis,
10
+ } from 'recharts';
11
+ import { chartTokens, seriesColor } from './chartTokens';
12
+ import { ChartEmpty, ChartTooltip } from './ChartShell';
13
+
14
+ export interface VolumeSeries {
15
+ dataKey: string;
16
+ name: string;
17
+ color?: string;
18
+ }
19
+
20
+ interface VolumeBarProps {
21
+ data: Array<Record<string, string | number>>;
22
+ series: VolumeSeries[];
23
+ xKey: string;
24
+ height?: number;
25
+ className?: string;
26
+ showLegend?: boolean;
27
+ stacked?: boolean;
28
+ emptyMessage?: string;
29
+ valueFormatter?: (value: number | string, name: string) => string;
30
+ }
31
+
32
+ /**
33
+ * VolumeBar — grouped/stacked bar chart (e.g. weekly order volume, revenue by
34
+ * month). Responsive, token-colored, rounded bars, honest empty state.
35
+ */
36
+ export function VolumeBar({
37
+ data,
38
+ series,
39
+ xKey,
40
+ height = 280,
41
+ className,
42
+ showLegend = true,
43
+ stacked = false,
44
+ emptyMessage = 'No volume data yet',
45
+ valueFormatter,
46
+ }: VolumeBarProps) {
47
+ if (!data || data.length === 0 || series.length === 0) {
48
+ return <ChartEmpty message={emptyMessage} height={height} className={className} />;
49
+ }
50
+
51
+ const axisTick = { fill: chartTokens.axis, fontSize: 12 };
52
+
53
+ return (
54
+ <div className={className} style={{ width: '100%', height }}>
55
+ <ResponsiveContainer width="100%" height="100%">
56
+ <BarChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: -8 }} barGap={4}>
57
+ <CartesianGrid strokeDasharray="3 3" stroke={chartTokens.grid} vertical={false} />
58
+ <XAxis
59
+ dataKey={xKey}
60
+ tick={axisTick}
61
+ tickLine={false}
62
+ axisLine={{ stroke: chartTokens.grid }}
63
+ />
64
+ <YAxis
65
+ tick={axisTick}
66
+ tickLine={false}
67
+ axisLine={false}
68
+ width={44}
69
+ tickFormatter={valueFormatter ? (v: number) => valueFormatter(v, '') : undefined}
70
+ />
71
+ <Tooltip
72
+ cursor={{ fill: chartTokens.axis, fillOpacity: 0.08 }}
73
+ content={<ChartTooltip formatter={valueFormatter} />}
74
+ />
75
+ {showLegend && (
76
+ <Legend
77
+ iconType="circle"
78
+ wrapperStyle={{ fontSize: 12, color: chartTokens.textSecondary }}
79
+ />
80
+ )}
81
+ {series.map((s, i) => (
82
+ <Bar
83
+ key={s.dataKey}
84
+ dataKey={s.dataKey}
85
+ name={s.name}
86
+ fill={s.color ?? seriesColor(i)}
87
+ radius={stacked ? [0, 0, 0, 0] : [4, 4, 0, 0]}
88
+ stackId={stacked ? 'stack' : undefined}
89
+ maxBarSize={48}
90
+ isAnimationActive={false}
91
+ />
92
+ ))}
93
+ </BarChart>
94
+ </ResponsiveContainer>
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Chart color tokens.
3
+ *
4
+ * Recharts needs explicit color strings on its series props (fill/stroke) where
5
+ * Tailwind token classes cannot reach the SVG. We source every one of those
6
+ * colors from the fe-libs design tokens via `var(--color-*)` so charts stay
7
+ * theme-aware (light/dark + product themes) and never introduce raw palette
8
+ * colors. The CSS variables resolve to full color values in the app-shell.
9
+ */
10
+
11
+ export const chartTokens = {
12
+ /** Product brand orange — primary accent / first data series. */
13
+ brandPrimary: 'var(--color-brand-primary)',
14
+ /** Brand secondary (orange-hover) — used sparingly for gradients. */
15
+ brandSecondary: 'var(--color-brand-secondary)',
16
+ /** Brand blue — the natural counterpart series to the orange. */
17
+ brandBlue: 'var(--color-chart-1)',
18
+ /** Categorical series palette (theme-aware). */
19
+ series: [
20
+ 'var(--color-brand-primary)',
21
+ 'var(--color-chart-1)',
22
+ 'var(--color-chart-2)',
23
+ 'var(--color-chart-3)',
24
+ 'var(--color-chart-4)',
25
+ 'var(--color-chart-5)',
26
+ 'var(--color-chart-6)',
27
+ 'var(--color-chart-7)',
28
+ 'var(--color-chart-8)',
29
+ ],
30
+ grid: 'var(--color-chart-grid)',
31
+ axis: 'var(--color-chart-axis)',
32
+ positive: 'var(--color-chart-positive)',
33
+ negative: 'var(--color-chart-negative)',
34
+ surface: 'var(--color-bg-surface)',
35
+ border: 'var(--color-border-seam)',
36
+ textSecondary: 'var(--color-text-secondary)',
37
+ textPrimary: 'var(--color-text-primary)',
38
+ } as const;
39
+
40
+ /** Resolve a categorical color by index, wrapping around the palette. */
41
+ export function seriesColor(index: number): string {
42
+ return chartTokens.series[index % chartTokens.series.length];
43
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Chart primitives (recharts) — token-colored, responsive, honest empty states.
3
+ */
4
+ export { Sparkline, type SparklinePoint } from './Sparkline';
5
+ export { TrendChart, type TrendSeries } from './TrendChart';
6
+ export { VolumeBar, type VolumeSeries } from './VolumeBar';
7
+ export { StatusDonut, type DonutSlice } from './StatusDonut';
8
+ export { ChartEmpty, ChartTooltip } from './ChartShell';
9
+ export { chartTokens, seriesColor } from './chartTokens';
@@ -11,7 +11,50 @@ export * from './ui';
11
11
  export { PageLayout, PageSection, EmptyState, LoadingState, ErrorState } from './PageLayout';
12
12
 
13
13
  // Metric components
14
- export { MetricCard, MetricCardSkeleton, MetricGrid } from './MetricCard';
14
+ export {
15
+ MetricCard,
16
+ MetricCardSkeleton,
17
+ MetricGrid,
18
+ type MetricAccent,
19
+ type MetricDelta,
20
+ } from './MetricCard';
21
+
22
+ // Surface primitive
23
+ export { GlassPanel } from './GlassPanel';
24
+
25
+ // Charts (recharts)
26
+ export {
27
+ Sparkline,
28
+ TrendChart,
29
+ VolumeBar,
30
+ StatusDonut,
31
+ ChartEmpty,
32
+ ChartTooltip,
33
+ chartTokens,
34
+ seriesColor,
35
+ type SparklinePoint,
36
+ type TrendSeries,
37
+ type VolumeSeries,
38
+ type DonutSlice,
39
+ } from './charts';
40
+
41
+ // Maps (react-leaflet + OpenStreetMap)
42
+ export {
43
+ LogisticsMap,
44
+ RouteMap,
45
+ TrackingMap,
46
+ MapCanvas,
47
+ createPinIcon,
48
+ createStopIcon,
49
+ createCurrentIcon,
50
+ isRealCoord,
51
+ type LogisticsMarker,
52
+ type LogisticsRoute,
53
+ type RouteStop,
54
+ type TrackPoint,
55
+ type TrackEndpoint,
56
+ type MapMarkerStatus,
57
+ } from './maps';
15
58
 
16
59
  // Status components
17
60
  export {
@@ -0,0 +1,94 @@
1
+ import { useMemo } from 'react';
2
+ import { Marker, Polyline, Popup } from 'react-leaflet';
3
+ import { MapCanvas } from './MapCanvas';
4
+ import { createPinIcon, isRealCoord, type MapMarkerStatus } from './leaflet-setup';
5
+
6
+ export interface LogisticsMarker {
7
+ id: string;
8
+ lat: number;
9
+ lng: number;
10
+ label: string;
11
+ status?: MapMarkerStatus;
12
+ sublabel?: string;
13
+ }
14
+
15
+ export interface LogisticsRoute {
16
+ points: Array<[number, number]>;
17
+ color?: string;
18
+ }
19
+
20
+ interface LogisticsMapProps {
21
+ markers: LogisticsMarker[];
22
+ routes?: LogisticsRoute[];
23
+ height?: number | string;
24
+ className?: string;
25
+ fitToMarkers?: boolean;
26
+ emptyMessage?: string;
27
+ }
28
+
29
+ /**
30
+ * LogisticsMap — an OSM map of brand-styled vehicle/asset markers plus optional
31
+ * route polylines. Auto-fits to the visible markers. Coordinates at null-island
32
+ * (0,0) or non-finite values are treated as "no location" and filtered out, so
33
+ * the map is never dumped in the ocean. Empty markers → honest empty state.
34
+ */
35
+ export function LogisticsMap({
36
+ markers,
37
+ routes = [],
38
+ height = 420,
39
+ className,
40
+ fitToMarkers = true,
41
+ emptyMessage = 'No live locations yet',
42
+ }: LogisticsMapProps) {
43
+ const validMarkers = useMemo(
44
+ () => markers.filter((m) => isRealCoord(m.lat, m.lng)),
45
+ [markers]
46
+ );
47
+
48
+ const validRoutes = useMemo(
49
+ () =>
50
+ routes
51
+ .map((r) => ({
52
+ ...r,
53
+ points: r.points.filter(([lat, lng]) => isRealCoord(lat, lng)),
54
+ }))
55
+ .filter((r) => r.points.length >= 2),
56
+ [routes]
57
+ );
58
+
59
+ const fitPoints = useMemo<Array<[number, number]>>(() => {
60
+ const pts: Array<[number, number]> = validMarkers.map((m) => [m.lat, m.lng]);
61
+ validRoutes.forEach((r) => pts.push(...r.points));
62
+ return pts;
63
+ }, [validMarkers, validRoutes]);
64
+
65
+ return (
66
+ <MapCanvas
67
+ points={fitPoints}
68
+ height={height}
69
+ className={className}
70
+ fitToMarkers={fitToMarkers}
71
+ emptyState={<p className="text-sm text-text-secondary">{emptyMessage}</p>}
72
+ >
73
+ {validRoutes.map((route, i) => (
74
+ <Polyline
75
+ key={i}
76
+ positions={route.points}
77
+ pathOptions={{
78
+ color: route.color ?? 'var(--color-chart-1)',
79
+ weight: 4,
80
+ opacity: 0.85,
81
+ }}
82
+ />
83
+ ))}
84
+ {validMarkers.map((m) => (
85
+ <Marker key={m.id} position={[m.lat, m.lng]} icon={createPinIcon(m.status ?? 'default')}>
86
+ <Popup>
87
+ <span className="font-semibold">{m.label}</span>
88
+ {m.sublabel && <span className="block text-text-secondary">{m.sublabel}</span>}
89
+ </Popup>
90
+ </Marker>
91
+ ))}
92
+ </MapCanvas>
93
+ );
94
+ }
@@ -0,0 +1,118 @@
1
+ import { useEffect, useState, type ReactNode } from 'react';
2
+ import L from 'leaflet';
3
+ import { MapContainer, TileLayer, useMap } from 'react-leaflet';
4
+ import { cn } from '../../utils/cn';
5
+ import { OSM_ATTRIBUTION, OSM_TILE_URL } from './leaflet-setup';
6
+
7
+ /** SSR/lazy safety: only mount Leaflet (which touches the DOM) after mount. */
8
+ export function useIsMounted(): boolean {
9
+ const [mounted, setMounted] = useState(false);
10
+ useEffect(() => setMounted(true), []);
11
+ return mounted;
12
+ }
13
+
14
+ /**
15
+ * Imperatively fit the map to a set of [lat,lng] points whenever they change.
16
+ * Invalidates size on mount so tiles render correctly inside flex/grid parents.
17
+ */
18
+ export function FitBounds({
19
+ points,
20
+ padding = 48,
21
+ maxZoom = 13,
22
+ }: {
23
+ points: Array<[number, number]>;
24
+ padding?: number;
25
+ maxZoom?: number;
26
+ }) {
27
+ const map = useMap();
28
+
29
+ useEffect(() => {
30
+ const t = setTimeout(() => map.invalidateSize(), 0);
31
+ return () => clearTimeout(t);
32
+ }, [map]);
33
+
34
+ useEffect(() => {
35
+ if (points.length === 0) return;
36
+ if (points.length === 1) {
37
+ map.setView(points[0], Math.min(maxZoom, 12));
38
+ return;
39
+ }
40
+ const bounds = L.latLngBounds(points);
41
+ map.fitBounds(bounds, { padding: [padding, padding], maxZoom });
42
+ }, [map, points, padding, maxZoom]);
43
+
44
+ return null;
45
+ }
46
+
47
+ interface MapCanvasProps {
48
+ points: Array<[number, number]>;
49
+ height?: number | string;
50
+ className?: string;
51
+ fitToMarkers?: boolean;
52
+ /** Fallback center when there are no points but we still render the canvas. */
53
+ center?: [number, number];
54
+ zoom?: number;
55
+ children?: ReactNode;
56
+ /** Rendered instead of the map when there are no points. */
57
+ emptyState?: ReactNode;
58
+ }
59
+
60
+ /**
61
+ * MapCanvas — the shared OSM MapContainer used by every map component. Handles
62
+ * mount-safety, container sizing, rounded/overflow styling, tile layer, and
63
+ * auto-fit. Renders `emptyState` when there is nothing to plot.
64
+ */
65
+ export function MapCanvas({
66
+ points,
67
+ height = 420,
68
+ className,
69
+ fitToMarkers = true,
70
+ center,
71
+ zoom = 5,
72
+ children,
73
+ emptyState,
74
+ }: MapCanvasProps) {
75
+ const mounted = useIsMounted();
76
+
77
+ const wrapperClass = cn(
78
+ 'relative overflow-hidden rounded-2xl border border-border-seam bg-bg-sunken',
79
+ className
80
+ );
81
+ const style = { height, width: '100%' };
82
+
83
+ if (points.length === 0 && emptyState) {
84
+ return (
85
+ <div className={wrapperClass} style={style}>
86
+ <div className="flex h-full w-full items-center justify-center p-6 text-center">
87
+ {emptyState}
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ if (!mounted) {
94
+ return (
95
+ <div className={wrapperClass} style={style} aria-busy="true">
96
+ <div className="h-full w-full motion-safe:animate-vc-shimmer bg-bg-sunken" />
97
+ </div>
98
+ );
99
+ }
100
+
101
+ const initialCenter: [number, number] = center ?? points[0] ?? [20, 0];
102
+
103
+ return (
104
+ <div className={wrapperClass} style={style}>
105
+ <MapContainer
106
+ center={initialCenter}
107
+ zoom={points.length > 0 ? zoom : 2}
108
+ scrollWheelZoom={false}
109
+ style={{ height: '100%', width: '100%' }}
110
+ className="z-0"
111
+ >
112
+ <TileLayer url={OSM_TILE_URL} attribution={OSM_ATTRIBUTION} />
113
+ {fitToMarkers && <FitBounds points={points} />}
114
+ {children}
115
+ </MapContainer>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,71 @@
1
+ import { useMemo } from 'react';
2
+ import { Marker, Polyline, Popup } from 'react-leaflet';
3
+ import { MapCanvas } from './MapCanvas';
4
+ import { createStopIcon, isRealCoord } from './leaflet-setup';
5
+
6
+ export interface RouteStop {
7
+ lat: number;
8
+ lng: number;
9
+ label: string;
10
+ sequence: number;
11
+ }
12
+
13
+ interface RouteMapProps {
14
+ stops: RouteStop[];
15
+ height?: number | string;
16
+ className?: string;
17
+ /** Polyline color (design-token string). Defaults to brand blue. */
18
+ routeColor?: string;
19
+ emptyMessage?: string;
20
+ }
21
+
22
+ /**
23
+ * RouteMap — numbered stop markers in sequence connected by a polyline. Filters
24
+ * out stops without a real coordinate and orders the line by `sequence`.
25
+ */
26
+ export function RouteMap({
27
+ stops,
28
+ height = 420,
29
+ className,
30
+ routeColor = 'var(--color-chart-1)',
31
+ emptyMessage = 'No stops with coordinates yet',
32
+ }: RouteMapProps) {
33
+ const ordered = useMemo(
34
+ () => [...stops].filter((s) => isRealCoord(s.lat, s.lng)).sort((a, b) => a.sequence - b.sequence),
35
+ [stops]
36
+ );
37
+
38
+ const line = useMemo<Array<[number, number]>>(
39
+ () => ordered.map((s) => [s.lat, s.lng]),
40
+ [ordered]
41
+ );
42
+
43
+ return (
44
+ <MapCanvas
45
+ points={line}
46
+ height={height}
47
+ className={className}
48
+ emptyState={<p className="text-sm text-text-secondary">{emptyMessage}</p>}
49
+ >
50
+ {line.length >= 2 && (
51
+ <Polyline
52
+ positions={line}
53
+ pathOptions={{ color: routeColor, weight: 4, opacity: 0.85 }}
54
+ />
55
+ )}
56
+ {ordered.map((s) => (
57
+ <Marker
58
+ key={`${s.sequence}-${s.lat}-${s.lng}`}
59
+ position={[s.lat, s.lng]}
60
+ icon={createStopIcon(s.sequence)}
61
+ >
62
+ <Popup>
63
+ <span className="font-semibold">
64
+ {s.sequence}. {s.label}
65
+ </span>
66
+ </Popup>
67
+ </Marker>
68
+ ))}
69
+ </MapCanvas>
70
+ );
71
+ }