@trackany-device/components 1.0.0

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 (289) hide show
  1. package/package.json +185 -0
  2. package/src/assets/logo.png +0 -0
  3. package/src/assets/map/arrows/map-arrow-blue.png +0 -0
  4. package/src/assets/map/arrows/map-arrow-green.png +0 -0
  5. package/src/assets/map/arrows/map-arrow-purple.png +0 -0
  6. package/src/assets/map/arrows/map-arrow-red.png +0 -0
  7. package/src/assets/map/flags/flag-blue.png +0 -0
  8. package/src/assets/map/flags/flag-green.png +0 -0
  9. package/src/assets/map/flags/flag-red.png +0 -0
  10. package/src/assets/map/flags/flag-yellow.png +0 -0
  11. package/src/assets/map/pins/map-pin-blue.png +0 -0
  12. package/src/assets/map/pins/map-pin-green.png +0 -0
  13. package/src/assets/map/pins/map-pin-purple.png +0 -0
  14. package/src/assets/map/pins/map-pin-red.png +0 -0
  15. package/src/components/Card.tsx +9 -0
  16. package/src/components/alert-error.tsx +24 -0
  17. package/src/components/app-content.tsx +22 -0
  18. package/src/components/app-header.tsx +153 -0
  19. package/src/components/app-logo-icon.tsx +13 -0
  20. package/src/components/app-logo.tsx +21 -0
  21. package/src/components/app-shell.tsx +19 -0
  22. package/src/components/app-sidebar-header.tsx +68 -0
  23. package/src/components/app-sidebar.tsx +106 -0
  24. package/src/components/appearance-tabs.tsx +46 -0
  25. package/src/components/breadcrumbs.tsx +50 -0
  26. package/src/components/cms/blurred-image.tsx +111 -0
  27. package/src/components/cms/section-bg.tsx +473 -0
  28. package/src/components/cms/section-button.tsx +127 -0
  29. package/src/components/cms/sections/banner-5050-section.tsx +135 -0
  30. package/src/components/cms/sections/blogs-listing-section.tsx +270 -0
  31. package/src/components/cms/sections/cards-grid-section.tsx +185 -0
  32. package/src/components/cms/sections/contact-form-section.tsx +157 -0
  33. package/src/components/cms/sections/cta-section.tsx +101 -0
  34. package/src/components/cms/sections/featured-blog-slider-section.tsx +256 -0
  35. package/src/components/cms/sections/featured-products-grid-section.tsx +173 -0
  36. package/src/components/cms/sections/featured-solutions-grid-section.tsx +183 -0
  37. package/src/components/cms/sections/hero-section.tsx +180 -0
  38. package/src/components/cms/sections/solutions-with-filter-section.tsx +234 -0
  39. package/src/components/cms/sections/text-section.tsx +77 -0
  40. package/src/components/cutout-image.tsx +228 -0
  41. package/src/components/devices/devices-mini-map.tsx +275 -0
  42. package/src/components/docs/docs-shell.tsx +280 -0
  43. package/src/components/fleet-hero-animated.tsx +383 -0
  44. package/src/components/input-error.tsx +17 -0
  45. package/src/components/keenicons/assets/duotone/Read Me.txt +7 -0
  46. package/src/components/keenicons/assets/duotone/demo-files/demo.css +160 -0
  47. package/src/components/keenicons/assets/duotone/demo-files/demo.js +32 -0
  48. package/src/components/keenicons/assets/duotone/demo.html +12424 -0
  49. package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.svg +1109 -0
  50. package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.ttf +0 -0
  51. package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.woff +0 -0
  52. package/src/components/keenicons/assets/duotone/selection.json +17313 -0
  53. package/src/components/keenicons/assets/duotone/style.css +4931 -0
  54. package/src/components/keenicons/assets/filled/Read Me.txt +7 -0
  55. package/src/components/keenicons/assets/filled/demo-files/demo.css +160 -0
  56. package/src/components/keenicons/assets/filled/demo-files/demo.js +32 -0
  57. package/src/components/keenicons/assets/filled/demo.html +12370 -0
  58. package/src/components/keenicons/assets/filled/fonts/keenicons-filled.svg +1082 -0
  59. package/src/components/keenicons/assets/filled/fonts/keenicons-filled.ttf +0 -0
  60. package/src/components/keenicons/assets/filled/fonts/keenicons-filled.woff +0 -0
  61. package/src/components/keenicons/assets/filled/selection.json +17096 -0
  62. package/src/components/keenicons/assets/filled/style.css +4769 -0
  63. package/src/components/keenicons/assets/outline/Read Me.txt +7 -0
  64. package/src/components/keenicons/assets/outline/demo-files/demo.css +160 -0
  65. package/src/components/keenicons/assets/outline/demo-files/demo.js +32 -0
  66. package/src/components/keenicons/assets/outline/demo.html +11356 -0
  67. package/src/components/keenicons/assets/outline/fonts/keenicons-outline.svg +575 -0
  68. package/src/components/keenicons/assets/outline/fonts/keenicons-outline.ttf +0 -0
  69. package/src/components/keenicons/assets/outline/fonts/keenicons-outline.woff +0 -0
  70. package/src/components/keenicons/assets/outline/selection.json +13054 -0
  71. package/src/components/keenicons/assets/outline/style.css +1721 -0
  72. package/src/components/keenicons/assets/solid/Read Me.txt +7 -0
  73. package/src/components/keenicons/assets/solid/demo-files/demo.css +160 -0
  74. package/src/components/keenicons/assets/solid/demo-files/demo.js +32 -0
  75. package/src/components/keenicons/assets/solid/demo.html +11356 -0
  76. package/src/components/keenicons/assets/solid/fonts/keenicons-solid.svg +575 -0
  77. package/src/components/keenicons/assets/solid/fonts/keenicons-solid.ttf +0 -0
  78. package/src/components/keenicons/assets/solid/fonts/keenicons-solid.woff +0 -0
  79. package/src/components/keenicons/assets/solid/selection.json +13048 -0
  80. package/src/components/keenicons/assets/solid/style.css +1721 -0
  81. package/src/components/keenicons/assets/styles.css +4 -0
  82. package/src/components/keenicons/index.ts +2 -0
  83. package/src/components/keenicons/keenicons.tsx +16 -0
  84. package/src/components/keenicons/types.ts +7 -0
  85. package/src/components/nav-footer.tsx +49 -0
  86. package/src/components/nav-main.tsx +53 -0
  87. package/src/components/nav-user.tsx +59 -0
  88. package/src/components/notification-bell.tsx +190 -0
  89. package/src/components/products/product-card.tsx +159 -0
  90. package/src/components/text-link.tsx +23 -0
  91. package/src/components/ui/accordion-menu.tsx +322 -0
  92. package/src/components/ui/accordion.tsx +133 -0
  93. package/src/components/ui/alert-dialog.tsx +82 -0
  94. package/src/components/ui/alert.tsx +63 -0
  95. package/src/components/ui/avatar-group.tsx +129 -0
  96. package/src/components/ui/avatar.tsx +67 -0
  97. package/src/components/ui/badge.tsx +230 -0
  98. package/src/components/ui/breadcrumb.tsx +88 -0
  99. package/src/components/ui/button.tsx +412 -0
  100. package/src/components/ui/calendar.tsx +56 -0
  101. package/src/components/ui/card.tsx +147 -0
  102. package/src/components/ui/chart.tsx +290 -0
  103. package/src/components/ui/checkbox.tsx +47 -0
  104. package/src/components/ui/code.tsx +45 -0
  105. package/src/components/ui/collapsible.tsx +31 -0
  106. package/src/components/ui/command-palette.tsx +189 -0
  107. package/src/components/ui/command.tsx +138 -0
  108. package/src/components/ui/cookie-banner.tsx +220 -0
  109. package/src/components/ui/copy-button.tsx +60 -0
  110. package/src/components/ui/data-grid-column-filter.tsx +124 -0
  111. package/src/components/ui/data-grid-column-header.tsx +284 -0
  112. package/src/components/ui/data-grid-column-visibility.tsx +38 -0
  113. package/src/components/ui/data-grid-pagination.tsx +206 -0
  114. package/src/components/ui/data-grid-table-dnd-rows.tsx +147 -0
  115. package/src/components/ui/data-grid-table-dnd.tsx +175 -0
  116. package/src/components/ui/data-grid-table.tsx +500 -0
  117. package/src/components/ui/data-grid.tsx +193 -0
  118. package/src/components/ui/data-list.tsx +76 -0
  119. package/src/components/ui/datefield.tsx +91 -0
  120. package/src/components/ui/dialog.tsx +139 -0
  121. package/src/components/ui/divider.tsx +41 -0
  122. package/src/components/ui/drawer.tsx +59 -0
  123. package/src/components/ui/dropdown-menu.tsx +224 -0
  124. package/src/components/ui/empty-state.tsx +54 -0
  125. package/src/components/ui/file-upload.tsx +152 -0
  126. package/src/components/ui/form.tsx +88 -0
  127. package/src/components/ui/icon.tsx +14 -0
  128. package/src/components/ui/input-otp.tsx +71 -0
  129. package/src/components/ui/input.tsx +155 -0
  130. package/src/components/ui/kbd.tsx +26 -0
  131. package/src/components/ui/label.tsx +31 -0
  132. package/src/components/ui/navigation-menu.tsx +168 -0
  133. package/src/components/ui/pagination.tsx +37 -0
  134. package/src/components/ui/placeholder-pattern.tsx +21 -0
  135. package/src/components/ui/popover.tsx +50 -0
  136. package/src/components/ui/progress.tsx +65 -0
  137. package/src/components/ui/radio-group.tsx +73 -0
  138. package/src/components/ui/resizable.tsx +39 -0
  139. package/src/components/ui/scroll-area.tsx +50 -0
  140. package/src/components/ui/select.tsx +234 -0
  141. package/src/components/ui/separator.tsx +24 -0
  142. package/src/components/ui/sheet.tsx +147 -0
  143. package/src/components/ui/sidebar.tsx +721 -0
  144. package/src/components/ui/skeleton.tsx +15 -0
  145. package/src/components/ui/slider.tsx +35 -0
  146. package/src/components/ui/sonner.tsx +28 -0
  147. package/src/components/ui/sortable.tsx +724 -0
  148. package/src/components/ui/spinner.tsx +17 -0
  149. package/src/components/ui/stat-card.tsx +82 -0
  150. package/src/components/ui/stepper.tsx +410 -0
  151. package/src/components/ui/switch.tsx +68 -0
  152. package/src/components/ui/table.tsx +42 -0
  153. package/src/components/ui/tabs.tsx +196 -0
  154. package/src/components/ui/timeline.tsx +90 -0
  155. package/src/components/ui/toggle-group.tsx +73 -0
  156. package/src/components/ui/toggle.tsx +45 -0
  157. package/src/components/ui/tooltip.tsx +55 -0
  158. package/src/components/user-info.tsx +33 -0
  159. package/src/components/user-menu-content.tsx +53 -0
  160. package/src/components/web/SiteFooter.tsx +154 -0
  161. package/src/components/web/SiteHeader.tsx +159 -0
  162. package/src/components/workflows/workflow-canvas.tsx +321 -0
  163. package/src/controls/Blockquote.tsx +25 -0
  164. package/src/controls/Button.tsx +101 -0
  165. package/src/controls/Checkbox.tsx +29 -0
  166. package/src/controls/DateField.tsx +37 -0
  167. package/src/controls/FormField.tsx +20 -0
  168. package/src/controls/Heading.tsx +28 -0
  169. package/src/controls/Input.tsx +21 -0
  170. package/src/controls/Label.tsx +18 -0
  171. package/src/controls/Paragraph.tsx +39 -0
  172. package/src/controls/PasswordInput.tsx +40 -0
  173. package/src/controls/RadioGroup.tsx +70 -0
  174. package/src/controls/Select.tsx +24 -0
  175. package/src/controls/Slider.tsx +33 -0
  176. package/src/controls/Switch.tsx +31 -0
  177. package/src/controls/Textarea.tsx +22 -0
  178. package/src/elements/ConfirmPasswordForm.tsx +43 -0
  179. package/src/elements/DeviceStatusBadge.tsx +38 -0
  180. package/src/elements/DriverCard.tsx +67 -0
  181. package/src/elements/ForgotPasswordForm.tsx +64 -0
  182. package/src/elements/IncidentCard.tsx +67 -0
  183. package/src/elements/LoginForm.tsx +100 -0
  184. package/src/elements/OtpForm.tsx +71 -0
  185. package/src/elements/RegisterForm.tsx +150 -0
  186. package/src/elements/ResetPasswordForm.tsx +72 -0
  187. package/src/elements/SmsChallengeForm.tsx +104 -0
  188. package/src/elements/VehicleCard.tsx +73 -0
  189. package/src/elements/VerifyEmailForm.tsx +39 -0
  190. package/src/hooks/use-appearance.tsx +117 -0
  191. package/src/hooks/use-applied-theme.ts +98 -0
  192. package/src/hooks/use-clipboard.ts +34 -0
  193. package/src/hooks/use-current-url.ts +83 -0
  194. package/src/hooks/use-dark-mode.ts +48 -0
  195. package/src/hooks/use-flash-toast.ts +29 -0
  196. package/src/hooks/use-initials.tsx +24 -0
  197. package/src/hooks/use-mobile-navigation.ts +12 -0
  198. package/src/hooks/use-mobile.tsx +38 -0
  199. package/src/index.ts +408 -0
  200. package/src/layouts/AppLayout.tsx +60 -0
  201. package/src/layouts/AuthLayout.tsx +32 -0
  202. package/src/layouts/SettingsLayout.tsx +21 -0
  203. package/src/layouts/app/AIChatLayout.tsx +73 -0
  204. package/src/layouts/app/AsideSidebarLayout.tsx +3 -0
  205. package/src/layouts/app/CalendarSidebarLayout.tsx +69 -0
  206. package/src/layouts/app/CommunitiesNavbarLayout.tsx +3 -0
  207. package/src/layouts/app/DualNavbarSidebarLayout.tsx +3 -0
  208. package/src/layouts/app/FocusSidebarLayout.tsx +75 -0
  209. package/src/layouts/app/MailLayout.tsx +69 -0
  210. package/src/layouts/app/MegaMenuHeaderLayout.tsx +3 -0
  211. package/src/layouts/app/MegaMenuLayout.tsx +81 -0
  212. package/src/layouts/app/MegaMenuNavbarLayout.tsx +88 -0
  213. package/src/layouts/app/MegaMenuSearchNavbarLayout.tsx +3 -0
  214. package/src/layouts/app/NavbarCollapsibleLayout.tsx +88 -0
  215. package/src/layouts/app/NavbarCollapsibleLinksLayout.tsx +3 -0
  216. package/src/layouts/app/NavbarMinimalLayout.tsx +3 -0
  217. package/src/layouts/app/NavbarMinimalSidebarLayout.tsx +3 -0
  218. package/src/layouts/app/NavbarSidebarDashboardLayout.tsx +3 -0
  219. package/src/layouts/app/NavbarSidebarLayout.tsx +92 -0
  220. package/src/layouts/app/NavbarSimpleSidebarLayout.tsx +3 -0
  221. package/src/layouts/app/NavbarTitledSidebarLayout.tsx +3 -0
  222. package/src/layouts/app/PanelSidebarLayout.tsx +3 -0
  223. package/src/layouts/app/SearchNavbarSidebarLayout.tsx +3 -0
  224. package/src/layouts/app/SidebarBreadcrumbLayout.tsx +3 -0
  225. package/src/layouts/app/SidebarCleanLayout.tsx +3 -0
  226. package/src/layouts/app/SidebarCommunitiesLayout.tsx +3 -0
  227. package/src/layouts/app/SidebarContentLayout.tsx +3 -0
  228. package/src/layouts/app/SidebarDualMenuLayout.tsx +104 -0
  229. package/src/layouts/app/SidebarFixedLayout.tsx +166 -0
  230. package/src/layouts/app/SidebarFooterNavbarLayout.tsx +3 -0
  231. package/src/layouts/app/SidebarHeaderMenuLayout.tsx +3 -0
  232. package/src/layouts/app/SidebarMegaMenuLayout.tsx +4 -0
  233. package/src/layouts/app/SidebarMinimalLayout.tsx +70 -0
  234. package/src/layouts/app/SidebarMobileSearchLayout.tsx +3 -0
  235. package/src/layouts/app/SidebarMultiPanelLayout.tsx +3 -0
  236. package/src/layouts/app/SidebarPrimarySecondaryLayout.tsx +3 -0
  237. package/src/layouts/app/SidebarSearchHeaderLayout.tsx +103 -0
  238. package/src/layouts/app/SidebarSearchToolbarLayout.tsx +3 -0
  239. package/src/layouts/app/SidebarTabsDualLayout.tsx +3 -0
  240. package/src/layouts/app/SidebarTabsLayout.tsx +98 -0
  241. package/src/layouts/app/SidebarTreeLayout.tsx +3 -0
  242. package/src/layouts/app/SplitNavbarLayout.tsx +3 -0
  243. package/src/layouts/app/SplitSidebarDashboardLayout.tsx +3 -0
  244. package/src/layouts/app/SplitSidebarLayout.tsx +99 -0
  245. package/src/layouts/app/TopNavLayout.tsx +105 -0
  246. package/src/layouts/app/TopNavLinksLayout.tsx +3 -0
  247. package/src/layouts/app/WorkspaceBreadcrumbLayout.tsx +3 -0
  248. package/src/layouts/app/WorkspaceCommunitiesLayout.tsx +3 -0
  249. package/src/layouts/app/WorkspaceNavbarLayout.tsx +3 -0
  250. package/src/layouts/app/WorkspaceSidebarLayout.tsx +98 -0
  251. package/src/layouts/app/WorkspaceSidebarTitleLayout.tsx +3 -0
  252. package/src/layouts/app/app-header-layout.tsx +45 -0
  253. package/src/layouts/app/app-sidebar-layout.tsx +56 -0
  254. package/src/layouts/app/layout-context.tsx +44 -0
  255. package/src/layouts/app/layout-types.ts +47 -0
  256. package/src/layouts/app/partials/Footer.tsx +35 -0
  257. package/src/layouts/app/partials/HeaderTopbar.tsx +96 -0
  258. package/src/layouts/app/partials/Navbar.tsx +85 -0
  259. package/src/layouts/app/partials/Toolbar.tsx +47 -0
  260. package/src/layouts/app-layout.tsx +29 -0
  261. package/src/layouts/auth/AuthBrandedLayout.tsx +58 -0
  262. package/src/layouts/auth/AuthCardLayout.tsx +31 -0
  263. package/src/layouts/auth/AuthCenteredLayout.tsx +41 -0
  264. package/src/layouts/auth/AuthClassicLayout.tsx +41 -0
  265. package/src/layouts/auth/AuthSimpleLayout.tsx +33 -0
  266. package/src/layouts/auth/AuthSplitLayout.tsx +89 -0
  267. package/src/layouts/web-app-layout.tsx +162 -0
  268. package/src/layouts/web-layout.tsx +23 -0
  269. package/src/lib/datetime.ts +188 -0
  270. package/src/lib/google-maps-loader.ts +99 -0
  271. package/src/lib/location.ts +127 -0
  272. package/src/lib/lucide-icon-map.ts +132 -0
  273. package/src/lib/map-markers.ts +124 -0
  274. package/src/lib/map-styles.ts +351 -0
  275. package/src/lib/utils.ts +11 -0
  276. package/src/platform/adapters/default.tsx +156 -0
  277. package/src/platform/adapters/inertia.tsx +88 -0
  278. package/src/platform/adapters/nextjs.ts +86 -0
  279. package/src/platform/context.tsx +106 -0
  280. package/src/platform/index.ts +27 -0
  281. package/src/platform/types.ts +105 -0
  282. package/src/styles/layouts/sidebar-fixed.css +161 -0
  283. package/src/styles/themes.css +583 -0
  284. package/src/types/assets.d.ts +5 -0
  285. package/src/types/auth.ts +25 -0
  286. package/src/types/global.d.ts +13 -0
  287. package/src/types/index.ts +9 -0
  288. package/src/types/navigation.ts +15 -0
  289. package/src/types/ui.ts +32 -0
@@ -0,0 +1,104 @@
1
+ import type { FormEvent } from 'react';
2
+ import { Button } from '../controls/Button';
3
+ import { FormField } from '../controls/FormField';
4
+ import { Input } from '../controls/Input';
5
+
6
+ export interface SmsChallengeFormErrors {
7
+ otp?: string;
8
+ }
9
+
10
+ interface Props {
11
+ otp: string;
12
+ errors: SmsChallengeFormErrors;
13
+ processing?: boolean;
14
+ resendProcessing?: boolean;
15
+ otpPhone?: string;
16
+ otpSent?: boolean;
17
+ status?: string;
18
+ sendError?: string;
19
+ logoutUrl?: string;
20
+ onOtpChange: (value: string) => void;
21
+ onSubmit: (e: FormEvent) => void;
22
+ onResend: () => void;
23
+ }
24
+
25
+ export function SmsChallengeForm({
26
+ otp,
27
+ errors,
28
+ processing = false,
29
+ resendProcessing = false,
30
+ otpPhone,
31
+ otpSent = true,
32
+ status,
33
+ sendError,
34
+ logoutUrl = '/logout',
35
+ onOtpChange,
36
+ onSubmit,
37
+ onResend,
38
+ }: Props) {
39
+ return (
40
+ <div className="space-y-5">
41
+ {status && (
42
+ <div className="rounded-lg bg-green-50 px-4 py-2.5 text-sm font-medium text-green-700">
43
+ {status}
44
+ </div>
45
+ )}
46
+ {sendError && (
47
+ <div className="rounded-lg bg-red-50 px-4 py-2.5 text-sm font-medium text-red-700">
48
+ {sendError}
49
+ </div>
50
+ )}
51
+ {otpSent && !sendError && otpPhone && (
52
+ <p className="text-sm text-muted-foreground">
53
+ A 6-digit code was sent to{' '}
54
+ <span className="font-medium text-foreground">{otpPhone}</span>.
55
+ Enter it below to continue.
56
+ </p>
57
+ )}
58
+
59
+ <form onSubmit={onSubmit} className="space-y-5">
60
+ <FormField label="Verification code" htmlFor="sms-otp">
61
+ <Input
62
+ id="sms-otp"
63
+ type="text"
64
+ inputMode="numeric"
65
+ pattern="[0-9]*"
66
+ maxLength={6}
67
+ value={otp}
68
+ onChange={e => onOtpChange(e.target.value.replace(/\D/g, '').slice(0, 6))}
69
+ placeholder="000000"
70
+ error={errors.otp}
71
+ autoFocus
72
+ autoComplete="one-time-code"
73
+ required
74
+ className="text-center font-mono text-xl tracking-[0.5em] placeholder:tracking-normal"
75
+ />
76
+ </FormField>
77
+
78
+ <Button type="submit" disabled={otp.length !== 6} loading={processing} fullWidth>
79
+ Verify
80
+ </Button>
81
+ </form>
82
+
83
+ <div className="flex flex-col items-center gap-2 text-center text-sm text-muted-foreground">
84
+ <span>
85
+ Didn't receive a code?{' '}
86
+ <button
87
+ type="button"
88
+ onClick={onResend}
89
+ disabled={resendProcessing}
90
+ className="font-medium text-primary hover:underline disabled:opacity-50"
91
+ >
92
+ {resendProcessing ? 'Sending…' : 'Resend'}
93
+ </button>
94
+ </span>
95
+ <a
96
+ href={logoutUrl}
97
+ className="text-xs text-muted-foreground/70 underline underline-offset-4 hover:text-muted-foreground"
98
+ >
99
+ Log out
100
+ </a>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,73 @@
1
+ import { cn } from '../lib/utils';
2
+ import { Card, CardContent } from '../components/ui/card';
3
+ import { Badge, badgeVariants } from '../components/ui/badge';
4
+
5
+ export type VehicleStatus = 'online' | 'offline' | 'idle' | 'moving' | 'stopped';
6
+
7
+ export interface VehicleCardData {
8
+ id: string;
9
+ registration: string;
10
+ make?: string;
11
+ model?: string;
12
+ status: VehicleStatus;
13
+ driver?: string;
14
+ lastSeen?: string;
15
+ location?: string;
16
+ speed?: number;
17
+ href?: string;
18
+ }
19
+
20
+ const STATUS_CONFIG: Record<VehicleStatus, { label: string; className: string }> = {
21
+ online: { label: 'Online', className: 'bg-success/10 text-success border-success/20' },
22
+ moving: { label: 'Moving', className: 'bg-primary/10 text-primary border-primary/20' },
23
+ idle: { label: 'Idle', className: 'bg-warning/10 text-warning border-warning/20' },
24
+ stopped: { label: 'Stopped', className: 'bg-muted text-muted-foreground border-border' },
25
+ offline: { label: 'Offline', className: 'bg-danger-subtle text-destructive border-destructive/20' },
26
+ };
27
+
28
+ interface VehicleCardProps {
29
+ vehicle: VehicleCardData;
30
+ className?: string;
31
+ onClick?: (vehicle: VehicleCardData) => void;
32
+ }
33
+
34
+ export function VehicleCard({ vehicle, className, onClick }: VehicleCardProps) {
35
+ const status = STATUS_CONFIG[vehicle.status];
36
+ const content = (
37
+ <Card className={cn('transition-shadow hover:shadow-md cursor-pointer', className)} onClick={() => onClick?.(vehicle)}>
38
+ <CardContent className="p-4">
39
+ <div className="flex items-start justify-between gap-3">
40
+ <div className="flex-1 min-w-0">
41
+ <div className="flex items-center gap-2 mb-1">
42
+ <span className="font-semibold text-mono text-sm">{vehicle.registration}</span>
43
+ <span className={cn('inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium', status.className)}>
44
+ {status.label}
45
+ </span>
46
+ </div>
47
+ {(vehicle.make || vehicle.model) && (
48
+ <p className="text-xs text-muted-foreground truncate">{[vehicle.make, vehicle.model].filter(Boolean).join(' ')}</p>
49
+ )}
50
+ {vehicle.driver && (
51
+ <p className="text-xs text-muted-foreground mt-1 truncate">Driver: {vehicle.driver}</p>
52
+ )}
53
+ {vehicle.location && (
54
+ <p className="text-xs text-muted-foreground mt-0.5 truncate">{vehicle.location}</p>
55
+ )}
56
+ </div>
57
+ {vehicle.speed !== undefined && (
58
+ <div className="text-right shrink-0">
59
+ <p className="text-lg font-bold text-mono">{vehicle.speed}</p>
60
+ <p className="text-xs text-muted-foreground">km/h</p>
61
+ </div>
62
+ )}
63
+ </div>
64
+ {vehicle.lastSeen && (
65
+ <p className="text-xs text-muted-foreground mt-2 border-t border-border pt-2">{vehicle.lastSeen}</p>
66
+ )}
67
+ </CardContent>
68
+ </Card>
69
+ );
70
+
71
+ if (vehicle.href) return <a href={vehicle.href} className="block no-underline">{content}</a>;
72
+ return content;
73
+ }
@@ -0,0 +1,39 @@
1
+ import type { FormEvent } from 'react';
2
+ import { Button } from '../controls/Button';
3
+
4
+ interface Props {
5
+ processing?: boolean;
6
+ status?: string;
7
+ logoutUrl?: string;
8
+ onSubmit: (e: FormEvent) => void;
9
+ }
10
+
11
+ export function VerifyEmailForm({ processing = false, status, logoutUrl = '/logout', onSubmit }: Props) {
12
+ return (
13
+ <div className="space-y-6 text-center">
14
+ <p className="text-sm text-muted-foreground">
15
+ Before continuing, could you verify your email address by clicking on the link we
16
+ just emailed to you? If you didn't receive the email, we will gladly send you another.
17
+ </p>
18
+
19
+ {status === 'verification-link-sent' && (
20
+ <p className="text-sm font-medium text-green-600">
21
+ A new verification link has been sent to the email address you provided during registration.
22
+ </p>
23
+ )}
24
+
25
+ <form onSubmit={onSubmit}>
26
+ <Button type="submit" loading={processing} variant="secondary">
27
+ Resend verification email
28
+ </Button>
29
+ </form>
30
+
31
+ <a
32
+ href={logoutUrl}
33
+ className="block text-sm text-muted-foreground hover:text-foreground underline underline-offset-4"
34
+ >
35
+ Log out
36
+ </a>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore } from 'react';
4
+
5
+ export type ResolvedAppearance = 'light' | 'dark';
6
+ export type Appearance = ResolvedAppearance | 'system';
7
+
8
+ export type UseAppearanceReturn = {
9
+ readonly appearance: Appearance;
10
+ readonly resolvedAppearance: ResolvedAppearance;
11
+ readonly updateAppearance: (mode: Appearance) => void;
12
+ };
13
+
14
+ const listeners = new Set<() => void>();
15
+ let currentAppearance: Appearance = 'system';
16
+
17
+ const prefersDark = (): boolean => {
18
+ if (typeof window === 'undefined') {
19
+ return false;
20
+ }
21
+
22
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
23
+ };
24
+
25
+ const setCookie = (name: string, value: string, days = 365): void => {
26
+ if (typeof document === 'undefined') {
27
+ return;
28
+ }
29
+
30
+ const maxAge = days * 24 * 60 * 60;
31
+ document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
32
+ };
33
+
34
+ const getStoredAppearance = (): Appearance => {
35
+ if (typeof window === 'undefined') {
36
+ return 'system';
37
+ }
38
+
39
+ return (localStorage.getItem('appearance') as Appearance) || 'system';
40
+ };
41
+
42
+ const isDarkMode = (appearance: Appearance): boolean => {
43
+ return appearance === 'dark' || (appearance === 'system' && prefersDark());
44
+ };
45
+
46
+ const applyTheme = (appearance: Appearance): void => {
47
+ if (typeof document === 'undefined') {
48
+ return;
49
+ }
50
+
51
+ const isDark = isDarkMode(appearance);
52
+
53
+ document.documentElement.classList.toggle('dark', isDark);
54
+ document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
55
+ };
56
+
57
+ const subscribe = (callback: () => void) => {
58
+ listeners.add(callback);
59
+
60
+ return () => listeners.delete(callback);
61
+ };
62
+
63
+ const notify = (): void => listeners.forEach((listener) => listener());
64
+
65
+ const mediaQuery = (): MediaQueryList | null => {
66
+ if (typeof window === 'undefined') {
67
+ return null;
68
+ }
69
+
70
+ return window.matchMedia('(prefers-color-scheme: dark)');
71
+ };
72
+
73
+ const handleSystemThemeChange = (): void => applyTheme(currentAppearance);
74
+
75
+ export function initializeTheme(): void {
76
+ if (typeof window === 'undefined') {
77
+ return;
78
+ }
79
+
80
+ if (!localStorage.getItem('appearance')) {
81
+ localStorage.setItem('appearance', 'system');
82
+ setCookie('appearance', 'system');
83
+ }
84
+
85
+ currentAppearance = getStoredAppearance();
86
+ applyTheme(currentAppearance);
87
+
88
+ // Set up system theme change listener
89
+ mediaQuery()?.addEventListener('change', handleSystemThemeChange);
90
+ }
91
+
92
+ export function useAppearance(): UseAppearanceReturn {
93
+ const appearance: Appearance = useSyncExternalStore(
94
+ subscribe,
95
+ () => currentAppearance,
96
+ () => 'system',
97
+ );
98
+
99
+ const resolvedAppearance: ResolvedAppearance = isDarkMode(appearance)
100
+ ? 'dark'
101
+ : 'light';
102
+
103
+ const updateAppearance = (mode: Appearance): void => {
104
+ currentAppearance = mode;
105
+
106
+ // Store in localStorage for client-side persistence...
107
+ localStorage.setItem('appearance', mode);
108
+
109
+ // Store in cookie for SSR...
110
+ setCookie('appearance', mode);
111
+
112
+ applyTheme(mode);
113
+ notify();
114
+ };
115
+
116
+ return { appearance, resolvedAppearance, updateAppearance } as const;
117
+ }
@@ -0,0 +1,98 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useAppliedTheme — controls the `data-theme` attribute on <html>.
5
+ *
6
+ * The server-side resolved scheme is set in app.blade.php from
7
+ * ThemeResolver. This hook lets the UI-Kit preview page (and only that
8
+ * page) override it client-side via `?theme=NAME`, persisting the
9
+ * override in localStorage so reloads keep the choice.
10
+ *
11
+ * Normal pages do NOT call apply() — they just render whatever the
12
+ * server provided. Only the UI-Kit page provides the switcher UI.
13
+ */
14
+ import * as React from 'react';
15
+
16
+ const STORAGE_KEY = 'ui-kit-theme-override';
17
+
18
+ declare global {
19
+ interface Window {
20
+ AppConfig?: {
21
+ theme: string;
22
+ appName: string;
23
+ logoUrl: string | null;
24
+ schemes: string[];
25
+ };
26
+ }
27
+ }
28
+
29
+ function knownSchemes(): string[] {
30
+ return window.AppConfig?.schemes ?? [];
31
+ }
32
+
33
+ export function initializeAppliedTheme(): void {
34
+ if (typeof window === 'undefined') {
35
+ return;
36
+ }
37
+
38
+ const params = new URLSearchParams(window.location.search);
39
+ const fromQuery = params.get('theme');
40
+ const fromStorage = window.localStorage.getItem(STORAGE_KEY);
41
+
42
+ const candidate = fromQuery ?? fromStorage;
43
+
44
+ if (!candidate) {
45
+ return;
46
+ }
47
+
48
+ if (knownSchemes().includes(candidate)) {
49
+ document.documentElement.dataset.theme = candidate;
50
+
51
+ if (fromQuery) {
52
+ window.localStorage.setItem(STORAGE_KEY, candidate);
53
+ }
54
+ }
55
+ }
56
+
57
+ export function useAppliedTheme(): {
58
+ theme: string;
59
+ schemes: string[];
60
+ apply: (name: string) => void;
61
+ reset: () => void;
62
+ } {
63
+ const [theme, setTheme] = React.useState<string>(() => {
64
+ if (typeof window === 'undefined') {
65
+ return 'default';
66
+ }
67
+
68
+ return (
69
+ document.documentElement.dataset.theme ??
70
+ window.AppConfig?.theme ??
71
+ 'default'
72
+ );
73
+ });
74
+
75
+ const schemes = React.useMemo(() => window.AppConfig?.schemes ?? [], []);
76
+
77
+ const apply = React.useCallback(
78
+ (name: string) => {
79
+ if (!schemes.includes(name)) {
80
+ return;
81
+ }
82
+
83
+ document.documentElement.dataset.theme = name;
84
+ window.localStorage.setItem(STORAGE_KEY, name);
85
+ setTheme(name);
86
+ },
87
+ [schemes],
88
+ );
89
+
90
+ const reset = React.useCallback(() => {
91
+ window.localStorage.removeItem(STORAGE_KEY);
92
+ const serverTheme = window.AppConfig?.theme ?? 'default';
93
+ document.documentElement.dataset.theme = serverTheme;
94
+ setTheme(serverTheme);
95
+ }, []);
96
+
97
+ return { theme, schemes, apply, reset };
98
+ }
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ // Credit: https://usehooks-ts.com/
4
+ import { useState } from 'react';
5
+
6
+ export type CopiedValue = string | null;
7
+ export type CopyFn = (text: string) => Promise<boolean>;
8
+ export type UseClipboardReturn = [CopiedValue, CopyFn];
9
+
10
+ export function useClipboard(): UseClipboardReturn {
11
+ const [copiedText, setCopiedText] = useState<CopiedValue>(null);
12
+
13
+ const copy: CopyFn = async (text) => {
14
+ if (!navigator?.clipboard) {
15
+ console.warn('Clipboard not supported');
16
+
17
+ return false;
18
+ }
19
+
20
+ try {
21
+ await navigator.clipboard.writeText(text);
22
+ setCopiedText(text);
23
+
24
+ return true;
25
+ } catch (error) {
26
+ console.warn('Copy failed', error);
27
+ setCopiedText(null);
28
+
29
+ return false;
30
+ }
31
+ };
32
+
33
+ return [copiedText, copy];
34
+ }
@@ -0,0 +1,83 @@
1
+ import type { PlatformLinkProps } from '../platform/types';
2
+ import { usePlatformUrl } from '../platform/context';
3
+ import { toUrl } from '../lib/utils';
4
+
5
+ export type IsCurrentUrlFn = (
6
+ urlToCheck: NonNullable<PlatformLinkProps['href']>,
7
+ currentUrl?: string,
8
+ startsWith?: boolean,
9
+ ) => boolean;
10
+
11
+ export type IsCurrentOrParentUrlFn = (
12
+ urlToCheck: NonNullable<PlatformLinkProps['href']>,
13
+ currentUrl?: string,
14
+ ) => boolean;
15
+
16
+ export type WhenCurrentUrlFn = <TIfTrue, TIfFalse = null>(
17
+ urlToCheck: NonNullable<PlatformLinkProps['href']>,
18
+ ifTrue: TIfTrue,
19
+ ifFalse?: TIfFalse,
20
+ ) => TIfTrue | TIfFalse;
21
+
22
+ export type UseCurrentUrlReturn = {
23
+ currentUrl: string;
24
+ isCurrentUrl: IsCurrentUrlFn;
25
+ isCurrentOrParentUrl: IsCurrentOrParentUrlFn;
26
+ whenCurrentUrl: WhenCurrentUrlFn;
27
+ };
28
+
29
+ export function useCurrentUrl(): UseCurrentUrlReturn {
30
+ const rawUrl = usePlatformUrl();
31
+ const currentUrlPath = new URL(
32
+ rawUrl,
33
+ typeof window !== 'undefined'
34
+ ? window.location.origin
35
+ : 'http://localhost',
36
+ ).pathname;
37
+
38
+ const isCurrentUrl: IsCurrentUrlFn = (
39
+ urlToCheck: NonNullable<PlatformLinkProps['href']>,
40
+ currentUrl?: string,
41
+ startsWith: boolean = false,
42
+ ) => {
43
+ const urlToCompare = currentUrl ?? currentUrlPath;
44
+ const urlString = toUrl(urlToCheck);
45
+
46
+ const comparePath = (path: string): boolean =>
47
+ startsWith ? urlToCompare.startsWith(path) : path === urlToCompare;
48
+
49
+ if (!urlString.startsWith('http')) {
50
+ return comparePath(urlString);
51
+ }
52
+
53
+ try {
54
+ const absoluteUrl = new URL(urlString);
55
+
56
+ return comparePath(absoluteUrl.pathname);
57
+ } catch {
58
+ return false;
59
+ }
60
+ };
61
+
62
+ const isCurrentOrParentUrl: IsCurrentOrParentUrlFn = (
63
+ urlToCheck: NonNullable<PlatformLinkProps['href']>,
64
+ currentUrl?: string,
65
+ ) => {
66
+ return isCurrentUrl(urlToCheck, currentUrl, true);
67
+ };
68
+
69
+ const whenCurrentUrl: WhenCurrentUrlFn = <TIfTrue, TIfFalse = null>(
70
+ urlToCheck: NonNullable<PlatformLinkProps['href']>,
71
+ ifTrue: TIfTrue,
72
+ ifFalse: TIfFalse = null as TIfFalse,
73
+ ): TIfTrue | TIfFalse => {
74
+ return isCurrentUrl(urlToCheck) ? ifTrue : ifFalse;
75
+ };
76
+
77
+ return {
78
+ currentUrl: currentUrlPath,
79
+ isCurrentUrl,
80
+ isCurrentOrParentUrl,
81
+ whenCurrentUrl,
82
+ };
83
+ }
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ const STORAGE_KEY = 'ui-dark-mode';
6
+
7
+ /**
8
+ * Controls the `dark` class on <html>. Persists the user's choice in
9
+ * localStorage, falling back to the system colour-scheme preference on
10
+ * first visit.
11
+ *
12
+ * Used by the public site header's day/night toggle. Keeps the choice
13
+ * tab-wide so it survives Inertia navigations.
14
+ */
15
+ export function useDarkMode() {
16
+ const [isDark, setIsDark] = React.useState<boolean>(() => {
17
+ if (typeof window === 'undefined') {
18
+ return false;
19
+ }
20
+
21
+ const stored = window.localStorage.getItem(STORAGE_KEY);
22
+
23
+ if (stored === 'true') {
24
+ return true;
25
+ }
26
+
27
+ if (stored === 'false') {
28
+ return false;
29
+ }
30
+
31
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
32
+ });
33
+
34
+ React.useEffect(() => {
35
+ if (typeof document === 'undefined') {
36
+ return;
37
+ }
38
+
39
+ document.documentElement.classList.toggle('dark', isDark);
40
+ window.localStorage.setItem(STORAGE_KEY, String(isDark));
41
+ }, [isDark]);
42
+
43
+ return {
44
+ isDark,
45
+ toggle: React.useCallback(() => setIsDark((v) => !v), []),
46
+ set: setIsDark,
47
+ };
48
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { toast } from 'sonner';
5
+ import type { FlashToast } from '../types/ui';
6
+
7
+ interface FlashRouter {
8
+ on: (event: string, callback: (e: unknown) => void) => () => void;
9
+ }
10
+
11
+ /**
12
+ * Listens for Inertia flash events and shows a Sonner toast.
13
+ * Pass Inertia's `router` from the consuming app:
14
+ *
15
+ * import { router } from '@inertiajs/react';
16
+ * useFlashToast(router);
17
+ *
18
+ * In non-Inertia environments omit the argument — the hook is a no-op.
19
+ */
20
+ export function useFlashToast(router?: FlashRouter): void {
21
+ useEffect(() => {
22
+ if (!router) return;
23
+ return router.on('flash', (event) => {
24
+ const flash = (event as CustomEvent).detail?.flash;
25
+ const data = flash?.toast as FlashToast | undefined;
26
+ if (data) toast[data.type](data.message);
27
+ });
28
+ }, [router]);
29
+ }
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+
5
+ export type GetInitialsFn = (fullName: string) => string;
6
+
7
+ export function useInitials(): GetInitialsFn {
8
+ return useCallback((fullName: string): string => {
9
+ const names = fullName.trim().split(' ');
10
+
11
+ if (names.length === 0) {
12
+ return '';
13
+ }
14
+
15
+ if (names.length === 1) {
16
+ return names[0].charAt(0).toUpperCase();
17
+ }
18
+
19
+ const firstInitial = names[0].charAt(0);
20
+ const lastInitial = names[names.length - 1].charAt(0);
21
+
22
+ return `${firstInitial}${lastInitial}`.toUpperCase();
23
+ }, []);
24
+ }
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+
5
+ export type CleanupFn = () => void;
6
+
7
+ export function useMobileNavigation(): CleanupFn {
8
+ return useCallback(() => {
9
+ // Remove pointer-events style from body...
10
+ document.body.style.removeProperty('pointer-events');
11
+ }, []);
12
+ }