@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,67 @@
1
+ import { cn } from '../lib/utils';
2
+ import { Card, CardContent } from '../components/ui/card';
3
+ import { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar';
4
+
5
+ export interface DriverCardData {
6
+ id: string;
7
+ name: string;
8
+ licenseNumber?: string;
9
+ phone?: string;
10
+ avatar?: string;
11
+ status?: 'active' | 'inactive' | 'on-trip';
12
+ vehicleRegistration?: string;
13
+ href?: string;
14
+ }
15
+
16
+ const STATUS_LABELS: Record<string, { label: string; className: string }> = {
17
+ active: { label: 'Active', className: 'text-success' },
18
+ inactive: { label: 'Inactive', className: 'text-muted-foreground' },
19
+ 'on-trip':{ label: 'On Trip', className: 'text-primary' },
20
+ };
21
+
22
+ function getInitials(name: string): string {
23
+ return name.split(' ').map((n) => n[0]).slice(0, 2).join('').toUpperCase();
24
+ }
25
+
26
+ interface DriverCardProps {
27
+ driver: DriverCardData;
28
+ className?: string;
29
+ onClick?: (driver: DriverCardData) => void;
30
+ }
31
+
32
+ export function DriverCard({ driver, className, onClick }: DriverCardProps) {
33
+ const statusConf = driver.status ? STATUS_LABELS[driver.status] : null;
34
+ const content = (
35
+ <Card className={cn('transition-shadow hover:shadow-md cursor-pointer', className)} onClick={() => onClick?.(driver)}>
36
+ <CardContent className="p-4">
37
+ <div className="flex items-center gap-3">
38
+ <Avatar className="size-10 shrink-0">
39
+ {driver.avatar && <AvatarImage src={driver.avatar} alt={driver.name} />}
40
+ <AvatarFallback className="text-xs font-medium">{getInitials(driver.name)}</AvatarFallback>
41
+ </Avatar>
42
+ <div className="flex-1 min-w-0">
43
+ <div className="flex items-center gap-2">
44
+ <p className="font-medium text-mono text-sm truncate">{driver.name}</p>
45
+ {statusConf && (
46
+ <span className={cn('text-xs font-medium shrink-0', statusConf.className)}>
47
+ {statusConf.label}
48
+ </span>
49
+ )}
50
+ </div>
51
+ {driver.licenseNumber && (
52
+ <p className="text-xs text-muted-foreground mt-0.5">Lic: {driver.licenseNumber}</p>
53
+ )}
54
+ {driver.vehicleRegistration && (
55
+ <p className="text-xs text-muted-foreground">Vehicle: {driver.vehicleRegistration}</p>
56
+ )}
57
+ {driver.phone && (
58
+ <p className="text-xs text-muted-foreground">{driver.phone}</p>
59
+ )}
60
+ </div>
61
+ </div>
62
+ </CardContent>
63
+ </Card>
64
+ );
65
+ if (driver.href) return <a href={driver.href} className="block no-underline">{content}</a>;
66
+ return content;
67
+ }
@@ -0,0 +1,64 @@
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 ForgotPasswordFormData {
7
+ email: string;
8
+ }
9
+
10
+ export interface ForgotPasswordFormErrors {
11
+ email?: string;
12
+ }
13
+
14
+ interface Props {
15
+ data: ForgotPasswordFormData;
16
+ errors: ForgotPasswordFormErrors;
17
+ processing?: boolean;
18
+ status?: string;
19
+ loginUrl?: string;
20
+ onChange: (field: keyof ForgotPasswordFormData, value: string) => void;
21
+ onSubmit: (e: FormEvent) => void;
22
+ }
23
+
24
+ export function ForgotPasswordForm({
25
+ data,
26
+ errors,
27
+ processing = false,
28
+ status,
29
+ loginUrl = '/login',
30
+ onChange,
31
+ onSubmit,
32
+ }: Props) {
33
+ return (
34
+ <form onSubmit={onSubmit} className="space-y-4">
35
+ <p className="text-sm text-muted-foreground">
36
+ Enter your email and we'll send you a password reset link.
37
+ </p>
38
+
39
+ {status && <p className="text-sm text-green-600">{status}</p>}
40
+
41
+ <FormField label="Email" htmlFor="forgot-email">
42
+ <Input
43
+ id="forgot-email"
44
+ type="email"
45
+ value={data.email}
46
+ onChange={e => onChange('email', e.target.value)}
47
+ placeholder="you@example.com"
48
+ error={errors.email}
49
+ required
50
+ autoFocus
51
+ autoComplete="email"
52
+ />
53
+ </FormField>
54
+
55
+ <Button type="submit" loading={processing} fullWidth>
56
+ Send reset link
57
+ </Button>
58
+
59
+ <p className="text-center text-sm text-muted-foreground">
60
+ <a href={loginUrl} className="text-primary hover:underline">← Back to sign in</a>
61
+ </p>
62
+ </form>
63
+ );
64
+ }
@@ -0,0 +1,67 @@
1
+ import { cn } from '../lib/utils';
2
+ import { Card, CardContent } from '../components/ui/card';
3
+ import { AlertTriangle, AlertCircle, Info } from 'lucide-react';
4
+
5
+ export type IncidentSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';
6
+
7
+ export interface IncidentCardData {
8
+ id: string;
9
+ title: string;
10
+ description?: string;
11
+ severity: IncidentSeverity;
12
+ vehicle?: string;
13
+ driver?: string;
14
+ location?: string;
15
+ timestamp?: string;
16
+ resolved?: boolean;
17
+ href?: string;
18
+ }
19
+
20
+ const SEVERITY_CONFIG: Record<IncidentSeverity, { icon: typeof AlertTriangle; iconClass: string; borderClass: string; bgClass: string }> = {
21
+ critical: { icon: AlertTriangle, iconClass: 'text-destructive', borderClass: 'border-destructive/30', bgClass: 'bg-destructive/5' },
22
+ high: { icon: AlertCircle, iconClass: 'text-orange-500', borderClass: 'border-orange-300', bgClass: 'bg-orange-50' },
23
+ medium: { icon: AlertTriangle, iconClass: 'text-yellow-500', borderClass: 'border-yellow-300', bgClass: 'bg-yellow-50' },
24
+ low: { icon: Info, iconClass: 'text-blue-500', borderClass: 'border-blue-200', bgClass: 'bg-blue-50' },
25
+ info: { icon: Info, iconClass: 'text-muted-foreground', borderClass: 'border-border', bgClass: 'bg-muted/50' },
26
+ };
27
+
28
+ interface IncidentCardProps {
29
+ incident: IncidentCardData;
30
+ className?: string;
31
+ onClick?: (incident: IncidentCardData) => void;
32
+ }
33
+
34
+ export function IncidentCard({ incident, className, onClick }: IncidentCardProps) {
35
+ const conf = SEVERITY_CONFIG[incident.severity];
36
+ const Icon = conf.icon;
37
+ const content = (
38
+ <Card className={cn('transition-shadow hover:shadow-md cursor-pointer border', conf.borderClass, incident.resolved && 'opacity-60', className)} onClick={() => onClick?.(incident)}>
39
+ <CardContent className={cn('p-4', conf.bgClass)}>
40
+ <div className="flex items-start gap-3">
41
+ <Icon className={cn('size-4 shrink-0 mt-0.5', conf.iconClass)} />
42
+ <div className="flex-1 min-w-0">
43
+ <div className="flex items-start justify-between gap-2">
44
+ <p className="font-medium text-mono text-sm">{incident.title}</p>
45
+ {incident.resolved && (
46
+ <span className="text-xs text-success font-medium shrink-0">Resolved</span>
47
+ )}
48
+ </div>
49
+ {incident.description && (
50
+ <p className="text-xs text-muted-foreground mt-1">{incident.description}</p>
51
+ )}
52
+ <div className="flex flex-wrap gap-x-3 gap-y-0.5 mt-2">
53
+ {incident.vehicle && <span className="text-xs text-muted-foreground">Vehicle: {incident.vehicle}</span>}
54
+ {incident.driver && <span className="text-xs text-muted-foreground">Driver: {incident.driver}</span>}
55
+ {incident.location && <span className="text-xs text-muted-foreground">{incident.location}</span>}
56
+ </div>
57
+ {incident.timestamp && (
58
+ <p className="text-xs text-muted-foreground mt-1">{incident.timestamp}</p>
59
+ )}
60
+ </div>
61
+ </div>
62
+ </CardContent>
63
+ </Card>
64
+ );
65
+ if (incident.href) return <a href={incident.href} className="block no-underline">{content}</a>;
66
+ return content;
67
+ }
@@ -0,0 +1,100 @@
1
+ import type { FormEvent } from 'react';
2
+ import { Button } from '../controls/Button';
3
+ import { Checkbox } from '../controls/Checkbox';
4
+ import { FormField } from '../controls/FormField';
5
+ import { Input } from '../controls/Input';
6
+ import { PasswordInput } from '../controls/PasswordInput';
7
+
8
+ export interface LoginFormData {
9
+ email: string;
10
+ password: string;
11
+ remember: boolean;
12
+ }
13
+
14
+ export interface LoginFormErrors {
15
+ email?: string;
16
+ password?: string;
17
+ }
18
+
19
+ interface Props {
20
+ data: LoginFormData;
21
+ errors: LoginFormErrors;
22
+ processing?: boolean;
23
+ status?: string;
24
+ canResetPassword?: boolean;
25
+ canRegister?: boolean;
26
+ forgotPasswordUrl?: string;
27
+ registerUrl?: string;
28
+ onChange: (field: keyof LoginFormData, value: string | boolean) => void;
29
+ onSubmit: (e: FormEvent) => void;
30
+ }
31
+
32
+ export function LoginForm({
33
+ data,
34
+ errors,
35
+ processing = false,
36
+ status,
37
+ canResetPassword = true,
38
+ canRegister = true,
39
+ forgotPasswordUrl = '/forgot-password',
40
+ registerUrl = '/register',
41
+ onChange,
42
+ onSubmit,
43
+ }: Props) {
44
+ return (
45
+ <form onSubmit={onSubmit} className="space-y-4">
46
+ {status && <p className="text-sm text-green-600">{status}</p>}
47
+
48
+ <FormField label="Email" htmlFor="login-email">
49
+ <Input
50
+ id="login-email"
51
+ type="email"
52
+ value={data.email}
53
+ onChange={e => onChange('email', e.target.value)}
54
+ placeholder="you@example.com"
55
+ error={errors.email}
56
+ required
57
+ autoFocus
58
+ autoComplete="email"
59
+ />
60
+ </FormField>
61
+
62
+ <FormField label="Password" htmlFor="login-password">
63
+ <PasswordInput
64
+ id="login-password"
65
+ value={data.password}
66
+ onChange={e => onChange('password', e.target.value)}
67
+ placeholder="••••••••"
68
+ error={errors.password}
69
+ required
70
+ autoComplete="current-password"
71
+ />
72
+ </FormField>
73
+
74
+ <div className="flex items-center justify-between pt-1">
75
+ <Checkbox
76
+ id="login-remember"
77
+ label="Remember me"
78
+ checked={data.remember}
79
+ onChange={(e: { target: HTMLInputElement }) => onChange('remember', e.target.checked)}
80
+ />
81
+ {canResetPassword && (
82
+ <a href={forgotPasswordUrl} className="text-sm text-primary hover:underline">
83
+ Forgot password?
84
+ </a>
85
+ )}
86
+ </div>
87
+
88
+ <Button type="submit" loading={processing} fullWidth>
89
+ Sign in
90
+ </Button>
91
+
92
+ {canRegister && (
93
+ <p className="text-center text-sm text-muted-foreground">
94
+ No account?{' '}
95
+ <a href={registerUrl} className="text-primary hover:underline">Register</a>
96
+ </p>
97
+ )}
98
+ </form>
99
+ );
100
+ }
@@ -0,0 +1,71 @@
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 OtpFormErrors {
7
+ code?: string;
8
+ }
9
+
10
+ interface Props {
11
+ code: string;
12
+ errors: OtpFormErrors;
13
+ processing?: boolean;
14
+ status?: string;
15
+ recovery?: boolean;
16
+ onCodeChange: (value: string) => void;
17
+ onSubmit: (e: FormEvent) => void;
18
+ onToggleRecovery?: () => void;
19
+ }
20
+
21
+ export function OtpForm({
22
+ code,
23
+ errors,
24
+ processing = false,
25
+ status,
26
+ recovery = false,
27
+ onCodeChange,
28
+ onSubmit,
29
+ onToggleRecovery,
30
+ }: Props) {
31
+ return (
32
+ <form onSubmit={onSubmit} className="space-y-4">
33
+ {status && <p className="text-sm text-green-600">{status}</p>}
34
+
35
+ <p className="text-sm text-muted-foreground">
36
+ {recovery
37
+ ? 'Enter one of your emergency recovery codes to regain access.'
38
+ : 'Enter the 6-digit verification code sent to your phone.'}
39
+ </p>
40
+
41
+ <FormField label={recovery ? 'Recovery code' : 'Verification code'} htmlFor="otp-code">
42
+ <Input
43
+ id="otp-code"
44
+ type="text"
45
+ inputMode={recovery ? 'text' : 'numeric'}
46
+ value={code}
47
+ onChange={e => onCodeChange(e.target.value)}
48
+ placeholder={recovery ? 'xxxx-xxxx-xxxx' : '000000'}
49
+ maxLength={recovery ? undefined : 6}
50
+ error={errors.code}
51
+ autoFocus
52
+ autoComplete="one-time-code"
53
+ required
54
+ className="text-center tracking-[0.4em] placeholder:tracking-normal"
55
+ />
56
+ </FormField>
57
+
58
+ <Button type="submit" loading={processing} fullWidth>
59
+ Verify
60
+ </Button>
61
+
62
+ {onToggleRecovery && (
63
+ <p className="text-center text-sm text-muted-foreground">
64
+ <button type="button" onClick={onToggleRecovery} className="text-primary hover:underline">
65
+ {recovery ? 'Use verification code instead' : 'Use a recovery code'}
66
+ </button>
67
+ </p>
68
+ )}
69
+ </form>
70
+ );
71
+ }
@@ -0,0 +1,150 @@
1
+ import type { FormEvent, ReactNode } from 'react';
2
+ import { Button } from '../controls/Button';
3
+ import { FormField } from '../controls/FormField';
4
+ import { Input } from '../controls/Input';
5
+ import { PasswordInput } from '../controls/PasswordInput';
6
+ import { Select } from '../controls/Select';
7
+
8
+ export interface PhoneCountry {
9
+ id: number;
10
+ iso_code: string;
11
+ name: string;
12
+ country_code: string;
13
+ }
14
+
15
+ export interface RegisterFormData {
16
+ name: string;
17
+ email: string;
18
+ password: string;
19
+ password_confirmation: string;
20
+ phone?: string;
21
+ mobile_country_iso?: string;
22
+ }
23
+
24
+ export interface RegisterFormErrors {
25
+ name?: string;
26
+ email?: string;
27
+ password?: string;
28
+ password_confirmation?: string;
29
+ phone?: string;
30
+ }
31
+
32
+ interface Props {
33
+ data: RegisterFormData;
34
+ errors: RegisterFormErrors;
35
+ processing?: boolean;
36
+ loginUrl?: string;
37
+ phoneCountries?: PhoneCountry[];
38
+ banner?: ReactNode;
39
+ onChange: (field: keyof RegisterFormData, value: string) => void;
40
+ onSubmit: (e: FormEvent) => void;
41
+ }
42
+
43
+ export function RegisterForm({
44
+ data,
45
+ errors,
46
+ processing = false,
47
+ loginUrl = '/login',
48
+ phoneCountries,
49
+ banner,
50
+ onChange,
51
+ onSubmit,
52
+ }: Props) {
53
+ const hasPhone = phoneCountries && phoneCountries.length > 0;
54
+
55
+ return (
56
+ <form onSubmit={onSubmit} className="space-y-4">
57
+ {banner}
58
+
59
+ <FormField label="Full name" htmlFor="reg-name">
60
+ <Input
61
+ id="reg-name"
62
+ type="text"
63
+ value={data.name}
64
+ onChange={e => onChange('name', e.target.value)}
65
+ placeholder="Your full name"
66
+ error={errors.name}
67
+ required
68
+ autoFocus
69
+ autoComplete="name"
70
+ />
71
+ </FormField>
72
+
73
+ <FormField label="Email" htmlFor="reg-email">
74
+ <Input
75
+ id="reg-email"
76
+ type="email"
77
+ value={data.email}
78
+ onChange={e => onChange('email', e.target.value)}
79
+ placeholder="you@example.com"
80
+ error={errors.email}
81
+ required
82
+ autoComplete="email"
83
+ />
84
+ </FormField>
85
+
86
+ {hasPhone && (
87
+ <FormField label="Phone number" htmlFor="reg-phone" hint="Used for SMS verification and 2FA.">
88
+ <div className="flex gap-2">
89
+ <Select
90
+ value={data.mobile_country_iso ?? phoneCountries[0]?.iso_code}
91
+ onChange={e => onChange('mobile_country_iso', e.target.value)}
92
+ aria-label="Country dial code"
93
+ className="w-28 flex-none"
94
+ >
95
+ {phoneCountries.map(c => (
96
+ <option key={c.id} value={c.iso_code}>
97
+ {c.iso_code} {c.country_code}
98
+ </option>
99
+ ))}
100
+ </Select>
101
+ <Input
102
+ id="reg-phone"
103
+ type="tel"
104
+ value={data.phone ?? ''}
105
+ onChange={e => onChange('phone', e.target.value)}
106
+ placeholder="300 1234567"
107
+ error={errors.phone}
108
+ required
109
+ autoComplete="tel"
110
+ className="flex-1"
111
+ />
112
+ </div>
113
+ </FormField>
114
+ )}
115
+
116
+ <FormField label="Password" htmlFor="reg-password">
117
+ <PasswordInput
118
+ id="reg-password"
119
+ value={data.password}
120
+ onChange={e => onChange('password', e.target.value)}
121
+ placeholder="••••••••"
122
+ error={errors.password}
123
+ required
124
+ autoComplete="new-password"
125
+ />
126
+ </FormField>
127
+
128
+ <FormField label="Confirm password" htmlFor="reg-confirm">
129
+ <PasswordInput
130
+ id="reg-confirm"
131
+ value={data.password_confirmation}
132
+ onChange={e => onChange('password_confirmation', e.target.value)}
133
+ placeholder="••••••••"
134
+ error={errors.password_confirmation}
135
+ required
136
+ autoComplete="new-password"
137
+ />
138
+ </FormField>
139
+
140
+ <Button type="submit" loading={processing} fullWidth>
141
+ Create account
142
+ </Button>
143
+
144
+ <p className="text-center text-sm text-muted-foreground">
145
+ Already have an account?{' '}
146
+ <a href={loginUrl} className="text-primary hover:underline">Sign in</a>
147
+ </p>
148
+ </form>
149
+ );
150
+ }
@@ -0,0 +1,72 @@
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
+ import { PasswordInput } from '../controls/PasswordInput';
6
+
7
+ export interface ResetPasswordFormData {
8
+ email: string;
9
+ password: string;
10
+ password_confirmation: string;
11
+ }
12
+
13
+ export interface ResetPasswordFormErrors {
14
+ email?: string;
15
+ password?: string;
16
+ password_confirmation?: string;
17
+ }
18
+
19
+ interface Props {
20
+ data: ResetPasswordFormData;
21
+ errors: ResetPasswordFormErrors;
22
+ processing?: boolean;
23
+ onChange: (field: keyof ResetPasswordFormData, value: string) => void;
24
+ onSubmit: (e: FormEvent) => void;
25
+ }
26
+
27
+ export function ResetPasswordForm({ data, errors, processing = false, onChange, onSubmit }: Props) {
28
+ return (
29
+ <form onSubmit={onSubmit} className="space-y-4">
30
+ <FormField label="Email" htmlFor="reset-email">
31
+ <Input
32
+ id="reset-email"
33
+ type="email"
34
+ value={data.email}
35
+ readOnly
36
+ tabIndex={-1}
37
+ error={errors.email}
38
+ className="bg-muted text-muted-foreground"
39
+ />
40
+ </FormField>
41
+
42
+ <FormField label="New password" htmlFor="reset-password">
43
+ <PasswordInput
44
+ id="reset-password"
45
+ value={data.password}
46
+ onChange={e => onChange('password', e.target.value)}
47
+ placeholder="••••••••"
48
+ error={errors.password}
49
+ required
50
+ autoFocus
51
+ autoComplete="new-password"
52
+ />
53
+ </FormField>
54
+
55
+ <FormField label="Confirm new password" htmlFor="reset-confirm">
56
+ <PasswordInput
57
+ id="reset-confirm"
58
+ value={data.password_confirmation}
59
+ onChange={e => onChange('password_confirmation', e.target.value)}
60
+ placeholder="••••••••"
61
+ error={errors.password_confirmation}
62
+ required
63
+ autoComplete="new-password"
64
+ />
65
+ </FormField>
66
+
67
+ <Button type="submit" loading={processing} fullWidth>
68
+ Reset password
69
+ </Button>
70
+ </form>
71
+ );
72
+ }