@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,101 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import * as React from 'react';
4
+
5
+ import { cn } from '../lib/utils';
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
14
+ // Alias: 'primary' maps to the default shadcn look
15
+ primary:
16
+ 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
17
+ destructive:
18
+ 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
19
+ outline:
20
+ 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
21
+ secondary:
22
+ 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
23
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
24
+ link: 'text-primary underline-offset-4 hover:underline',
25
+ },
26
+ size: {
27
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
28
+ // Legacy aliases used by existing auth forms
29
+ sm: 'h-8 rounded-md px-3 has-[>svg]:px-2.5',
30
+ md: 'h-9 px-4 py-2 has-[>svg]:px-3',
31
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
32
+ icon: 'size-9',
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: 'default',
37
+ size: 'default',
38
+ },
39
+ },
40
+ );
41
+
42
+ interface ButtonProps
43
+ extends React.ComponentProps<'button'>,
44
+ VariantProps<typeof buttonVariants> {
45
+ asChild?: boolean;
46
+ loading?: boolean;
47
+ fullWidth?: boolean;
48
+ }
49
+
50
+ function Button({
51
+ className,
52
+ variant,
53
+ size,
54
+ asChild = false,
55
+ loading = false,
56
+ fullWidth = false,
57
+ disabled,
58
+ children,
59
+ ...props
60
+ }: ButtonProps) {
61
+ const Comp = asChild ? Slot : 'button';
62
+
63
+ return (
64
+ <Comp
65
+ data-slot="button"
66
+ disabled={disabled || loading}
67
+ className={cn(
68
+ buttonVariants({ variant, size }),
69
+ fullWidth && 'w-full',
70
+ className,
71
+ )}
72
+ {...props}
73
+ >
74
+ {loading && (
75
+ <svg
76
+ className="size-4 animate-spin shrink-0"
77
+ viewBox="0 0 24 24"
78
+ fill="none"
79
+ aria-hidden="true"
80
+ >
81
+ <circle
82
+ className="opacity-25"
83
+ cx="12"
84
+ cy="12"
85
+ r="10"
86
+ stroke="currentColor"
87
+ strokeWidth="4"
88
+ />
89
+ <path
90
+ className="opacity-75"
91
+ fill="currentColor"
92
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
93
+ />
94
+ </svg>
95
+ )}
96
+ {children}
97
+ </Comp>
98
+ );
99
+ }
100
+
101
+ export { Button, buttonVariants };
@@ -0,0 +1,29 @@
1
+ import type { InputHTMLAttributes, ReactNode } from 'react';
2
+
3
+ interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
4
+ label?: ReactNode;
5
+ error?: string;
6
+ }
7
+
8
+ export function Checkbox({ label, error, className = '', id, ...props }: Props) {
9
+ return (
10
+ <div>
11
+ <label
12
+ htmlFor={id}
13
+ className="flex items-center gap-2.5 cursor-pointer select-none"
14
+ >
15
+ <input
16
+ id={id}
17
+ type="checkbox"
18
+ className={`h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
19
+ aria-invalid={!!error}
20
+ {...props}
21
+ />
22
+ {label && (
23
+ <span className="text-sm text-foreground">{label}</span>
24
+ )}
25
+ </label>
26
+ {error && <p className="mt-1 text-xs text-destructive">{error}</p>}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,37 @@
1
+ import { DateField as DateFieldPrimitive, DateInput, DateSegment } from '../components/ui/datefield';
2
+ import { Label } from './Label';
3
+ import { cn } from '../lib/utils';
4
+ import type { DateValue } from 'react-aria-components';
5
+ import type { DateFieldProps } from 'react-aria-components';
6
+
7
+ interface DateFieldControlProps<T extends DateValue> extends DateFieldProps<T> {
8
+ label?: string;
9
+ error?: string;
10
+ className?: string;
11
+ inputClassName?: string;
12
+ }
13
+
14
+ export function DateField<T extends DateValue>({
15
+ label,
16
+ error,
17
+ className,
18
+ inputClassName,
19
+ id,
20
+ ...props
21
+ }: DateFieldControlProps<T>) {
22
+ return (
23
+ <div className={cn('flex flex-col gap-2', className)}>
24
+ {label && <Label htmlFor={id}>{label}</Label>}
25
+ <DateFieldPrimitive {...props}>
26
+ <DateInput className={cn(
27
+ 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors',
28
+ 'placeholder:text-muted-foreground focus-within:outline-none focus-within:ring-1 focus-within:ring-ring',
29
+ 'disabled:cursor-not-allowed disabled:opacity-50',
30
+ error && 'border-destructive focus-within:ring-destructive',
31
+ inputClassName,
32
+ )} />
33
+ </DateFieldPrimitive>
34
+ {error && <p className="text-xs text-destructive">{error}</p>}
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Label } from './Label';
3
+
4
+ interface Props {
5
+ label: string;
6
+ htmlFor?: string;
7
+ hint?: string;
8
+ required?: boolean;
9
+ children: ReactNode;
10
+ }
11
+
12
+ export function FormField({ label, htmlFor, hint, required, children }: Props) {
13
+ return (
14
+ <div>
15
+ <Label htmlFor={htmlFor} required={required}>{label}</Label>
16
+ {children}
17
+ {hint && <p className="mt-1 text-xs text-muted-foreground">{hint}</p>}
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,28 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ type Level = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
4
+
5
+ const sizes: Record<Level, string> = {
6
+ h1: 'text-4xl font-bold tracking-tight',
7
+ h2: 'text-3xl font-bold tracking-tight',
8
+ h3: 'text-2xl font-semibold tracking-tight',
9
+ h4: 'text-xl font-semibold',
10
+ h5: 'text-lg font-semibold',
11
+ h6: 'text-base font-semibold',
12
+ };
13
+
14
+ interface Props extends HTMLAttributes<HTMLHeadingElement> {
15
+ as?: Level;
16
+ children: ReactNode;
17
+ }
18
+
19
+ export function Heading({ as: Tag = 'h2', className = '', children, ...props }: Props) {
20
+ return (
21
+ <Tag
22
+ className={`text-foreground ${sizes[Tag]} ${className}`}
23
+ {...props}
24
+ >
25
+ {children}
26
+ </Tag>
27
+ );
28
+ }
@@ -0,0 +1,21 @@
1
+ import type { InputHTMLAttributes } from 'react';
2
+
3
+ interface Props extends InputHTMLAttributes<HTMLInputElement> {
4
+ error?: string;
5
+ }
6
+
7
+ const base =
8
+ 'w-full rounded-lg border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 transition-colors';
9
+
10
+ export function Input({ error, className = '', ...props }: Props) {
11
+ return (
12
+ <>
13
+ <input
14
+ className={`${base} ${error ? 'border-destructive' : 'border-border'} ${className}`}
15
+ aria-invalid={!!error}
16
+ {...props}
17
+ />
18
+ {error && <p className="mt-1 text-xs text-destructive">{error}</p>}
19
+ </>
20
+ );
21
+ }
@@ -0,0 +1,18 @@
1
+ import type { LabelHTMLAttributes, ReactNode } from 'react';
2
+
3
+ interface Props extends LabelHTMLAttributes<HTMLLabelElement> {
4
+ required?: boolean;
5
+ children?: ReactNode;
6
+ }
7
+
8
+ export function Label({ required, children, className = '', ...props }: Props) {
9
+ return (
10
+ <label
11
+ className={`block text-sm font-medium text-foreground mb-1 ${className}`}
12
+ {...props}
13
+ >
14
+ {children}
15
+ {required && <span className="ml-0.5 text-destructive" aria-hidden="true">*</span>}
16
+ </label>
17
+ );
18
+ }
@@ -0,0 +1,39 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ const sizes = {
4
+ xs: 'text-xs',
5
+ sm: 'text-sm',
6
+ md: 'text-base',
7
+ lg: 'text-lg',
8
+ } as const;
9
+
10
+ const variants = {
11
+ default: 'text-foreground',
12
+ muted: 'text-muted-foreground',
13
+ lead: 'text-muted-foreground text-xl',
14
+ error: 'text-destructive',
15
+ success: 'text-green-600',
16
+ } as const;
17
+
18
+ interface Props extends HTMLAttributes<HTMLParagraphElement> {
19
+ size?: keyof typeof sizes;
20
+ variant?: keyof typeof variants;
21
+ children: ReactNode;
22
+ }
23
+
24
+ export function Paragraph({
25
+ size = 'md',
26
+ variant = 'default',
27
+ className = '',
28
+ children,
29
+ ...props
30
+ }: Props) {
31
+ return (
32
+ <p
33
+ className={`leading-relaxed ${sizes[size]} ${variants[variant]} ${className}`}
34
+ {...props}
35
+ >
36
+ {children}
37
+ </p>
38
+ );
39
+ }
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { Eye, EyeOff } from 'lucide-react';
4
+ import { useState } from 'react';
5
+ import type { InputHTMLAttributes } from 'react';
6
+
7
+ interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
8
+ error?: string;
9
+ }
10
+
11
+ const base =
12
+ 'w-full rounded-lg border bg-background px-3 py-2 pr-10 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 transition-colors';
13
+
14
+ export function PasswordInput({ error, className = '', ...props }: Props) {
15
+ const [visible, setVisible] = useState(false);
16
+ const Icon = visible ? EyeOff : Eye;
17
+
18
+ return (
19
+ <>
20
+ <div className="relative">
21
+ <input
22
+ type={visible ? 'text' : 'password'}
23
+ className={`${base} ${error ? 'border-destructive' : 'border-border'} ${className}`}
24
+ aria-invalid={!!error}
25
+ {...props}
26
+ />
27
+ <button
28
+ type="button"
29
+ onClick={() => setVisible(v => !v)}
30
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
31
+ aria-label={visible ? 'Hide password' : 'Show password'}
32
+ tabIndex={-1}
33
+ >
34
+ <Icon className="h-4 w-4" />
35
+ </button>
36
+ </div>
37
+ {error && <p className="mt-1 text-xs text-destructive">{error}</p>}
38
+ </>
39
+ );
40
+ }
@@ -0,0 +1,70 @@
1
+ import type { ReactNode } from 'react';
2
+ import { RadioGroup as RadioGroupPrimitive, RadioGroupItem } from '../components/ui/radio-group';
3
+ import { Label } from './Label';
4
+ import { cn } from '../lib/utils';
5
+
6
+ interface RadioOption {
7
+ value: string;
8
+ label: string;
9
+ description?: string;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ interface RadioGroupProps {
14
+ name?: string;
15
+ value?: string;
16
+ defaultValue?: string;
17
+ onChange?: (value: string) => void;
18
+ options: RadioOption[];
19
+ error?: string;
20
+ className?: string;
21
+ orientation?: 'horizontal' | 'vertical';
22
+ children?: ReactNode;
23
+ }
24
+
25
+ export function RadioGroup({
26
+ name,
27
+ value,
28
+ defaultValue,
29
+ onChange,
30
+ options,
31
+ error,
32
+ className,
33
+ orientation = 'vertical',
34
+ children,
35
+ }: RadioGroupProps) {
36
+ return (
37
+ <div className={cn('space-y-1', className)}>
38
+ <RadioGroupPrimitive
39
+ name={name}
40
+ value={value}
41
+ defaultValue={defaultValue}
42
+ onValueChange={onChange}
43
+ className={cn(
44
+ 'flex gap-3',
45
+ orientation === 'vertical' ? 'flex-col' : 'flex-row flex-wrap',
46
+ )}
47
+ >
48
+ {children ?? options.map((opt) => (
49
+ <div key={opt.value} className="flex items-start gap-2">
50
+ <RadioGroupItem
51
+ id={`${name}-${opt.value}`}
52
+ value={opt.value}
53
+ disabled={opt.disabled}
54
+ className="mt-0.5"
55
+ />
56
+ <div className="flex flex-col gap-0.5">
57
+ <Label htmlFor={`${name}-${opt.value}`} className="font-normal cursor-pointer">
58
+ {opt.label}
59
+ </Label>
60
+ {opt.description && (
61
+ <span className="text-xs text-muted-foreground">{opt.description}</span>
62
+ )}
63
+ </div>
64
+ </div>
65
+ ))}
66
+ </RadioGroupPrimitive>
67
+ {error && <p className="text-xs text-destructive mt-1">{error}</p>}
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode, SelectHTMLAttributes } from 'react';
2
+
3
+ interface Props extends SelectHTMLAttributes<HTMLSelectElement> {
4
+ error?: string;
5
+ children: ReactNode;
6
+ }
7
+
8
+ const base =
9
+ 'w-full rounded-lg border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 transition-colors appearance-none bg-[url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'16\' height=\'16\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%236b7280\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3E%3Cpath d=\'m6 9 6 6 6-6\'/%3E%3C/svg%3E")] bg-no-repeat bg-[right_0.5rem_center] pr-9';
10
+
11
+ export function Select({ error, className = '', children, ...props }: Props) {
12
+ return (
13
+ <>
14
+ <select
15
+ className={`${base} ${error ? 'border-destructive' : 'border-border'} ${className}`}
16
+ aria-invalid={!!error}
17
+ {...props}
18
+ >
19
+ {children}
20
+ </select>
21
+ {error && <p className="mt-1 text-xs text-destructive">{error}</p>}
22
+ </>
23
+ );
24
+ }
@@ -0,0 +1,33 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { Slider as SliderPrimitive, SliderThumb } from '../components/ui/slider';
3
+ import { Label } from './Label';
4
+ import { cn } from '../lib/utils';
5
+
6
+ interface SliderProps extends ComponentProps<typeof SliderPrimitive> {
7
+ label?: string;
8
+ showValue?: boolean;
9
+ error?: string;
10
+ className?: string;
11
+ }
12
+
13
+ export function Slider({ label, showValue, error, className, ...props }: SliderProps) {
14
+ const currentValue = Array.isArray(props.value) ? props.value : props.defaultValue;
15
+ const displayValue = Array.isArray(currentValue) ? currentValue[0] : currentValue;
16
+
17
+ return (
18
+ <div className={cn('space-y-2', className)}>
19
+ {(label || showValue) && (
20
+ <div className="flex items-center justify-between">
21
+ {label && <Label className="font-normal">{label}</Label>}
22
+ {showValue && displayValue !== undefined && (
23
+ <span className="text-sm text-muted-foreground">{displayValue}</span>
24
+ )}
25
+ </div>
26
+ )}
27
+ <SliderPrimitive {...props}>
28
+ <SliderThumb />
29
+ </SliderPrimitive>
30
+ {error && <p className="text-xs text-destructive">{error}</p>}
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,31 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { Switch as SwitchPrimitive, SwitchWrapper } from '../components/ui/switch';
3
+ import { Label } from './Label';
4
+ import { cn } from '../lib/utils';
5
+
6
+ interface SwitchProps extends ComponentProps<typeof SwitchPrimitive> {
7
+ label?: string;
8
+ description?: string;
9
+ error?: string;
10
+ labelClassName?: string;
11
+ }
12
+
13
+ export function Switch({ label, description, error, labelClassName, className, id, ...props }: SwitchProps) {
14
+ const switchId = id ?? `switch-${Math.random().toString(36).slice(2, 7)}`;
15
+ return (
16
+ <SwitchWrapper className={cn('flex items-center gap-3', className)}>
17
+ <SwitchPrimitive id={switchId} {...props} />
18
+ {(label || description) && (
19
+ <div className="flex flex-col gap-0.5">
20
+ {label && (
21
+ <Label htmlFor={switchId} className={cn('font-normal cursor-pointer', labelClassName)}>
22
+ {label}
23
+ </Label>
24
+ )}
25
+ {description && <span className="text-xs text-muted-foreground">{description}</span>}
26
+ </div>
27
+ )}
28
+ {error && <p className="text-xs text-destructive">{error}</p>}
29
+ </SwitchWrapper>
30
+ );
31
+ }
@@ -0,0 +1,22 @@
1
+ import type { TextareaHTMLAttributes } from 'react';
2
+
3
+ interface Props extends TextareaHTMLAttributes<HTMLTextAreaElement> {
4
+ error?: string;
5
+ }
6
+
7
+ const base =
8
+ 'w-full rounded-lg border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-y transition-colors';
9
+
10
+ export function Textarea({ error, className = '', rows = 4, ...props }: Props) {
11
+ return (
12
+ <>
13
+ <textarea
14
+ rows={rows}
15
+ className={`${base} ${error ? 'border-destructive' : 'border-border'} ${className}`}
16
+ aria-invalid={!!error}
17
+ {...props}
18
+ />
19
+ {error && <p className="mt-1 text-xs text-destructive">{error}</p>}
20
+ </>
21
+ );
22
+ }
@@ -0,0 +1,43 @@
1
+ import type { FormEvent } from 'react';
2
+ import { Button } from '../controls/Button';
3
+ import { FormField } from '../controls/FormField';
4
+ import { PasswordInput } from '../controls/PasswordInput';
5
+
6
+ export interface ConfirmPasswordFormErrors {
7
+ password?: string;
8
+ }
9
+
10
+ interface Props {
11
+ password: string;
12
+ errors: ConfirmPasswordFormErrors;
13
+ processing?: boolean;
14
+ onChange: (value: string) => void;
15
+ onSubmit: (e: FormEvent) => void;
16
+ }
17
+
18
+ export function ConfirmPasswordForm({ password, errors, processing = false, onChange, onSubmit }: Props) {
19
+ return (
20
+ <form onSubmit={onSubmit} className="space-y-4">
21
+ <p className="text-sm text-muted-foreground">
22
+ This is a secure area of the application. Please confirm your password before continuing.
23
+ </p>
24
+
25
+ <FormField label="Password" htmlFor="confirm-password">
26
+ <PasswordInput
27
+ id="confirm-password"
28
+ value={password}
29
+ onChange={e => onChange(e.target.value)}
30
+ placeholder="••••••••"
31
+ error={errors.password}
32
+ required
33
+ autoFocus
34
+ autoComplete="current-password"
35
+ />
36
+ </FormField>
37
+
38
+ <Button type="submit" loading={processing} fullWidth>
39
+ Confirm password
40
+ </Button>
41
+ </form>
42
+ );
43
+ }
@@ -0,0 +1,38 @@
1
+ import { cn } from '../lib/utils';
2
+
3
+ export type DeviceStatus = 'online' | 'offline' | 'idle' | 'moving' | 'error' | 'unknown';
4
+
5
+ const STATUS_CONFIG: Record<DeviceStatus, { label: string; dotClass: string; textClass: string; bgClass: string }> = {
6
+ online: { label: 'Online', dotClass: 'bg-success', textClass: 'text-success', bgClass: 'bg-success/10 border-success/20' },
7
+ moving: { label: 'Moving', dotClass: 'bg-primary', textClass: 'text-primary', bgClass: 'bg-primary/10 border-primary/20' },
8
+ idle: { label: 'Idle', dotClass: 'bg-yellow-500', textClass: 'text-yellow-600', bgClass: 'bg-yellow-50 border-yellow-200' },
9
+ offline: { label: 'Offline', dotClass: 'bg-muted-foreground', textClass: 'text-muted-foreground', bgClass: 'bg-muted border-border' },
10
+ error: { label: 'Error', dotClass: 'bg-destructive', textClass: 'text-destructive', bgClass: 'bg-destructive/10 border-destructive/20' },
11
+ unknown: { label: 'Unknown', dotClass: 'bg-muted-foreground', textClass: 'text-muted-foreground', bgClass: 'bg-muted border-border' },
12
+ };
13
+
14
+ interface DeviceStatusBadgeProps {
15
+ status: DeviceStatus;
16
+ showDot?: boolean;
17
+ size?: 'sm' | 'md';
18
+ className?: string;
19
+ }
20
+
21
+ export function DeviceStatusBadge({ status, showDot = true, size = 'md', className }: DeviceStatusBadgeProps) {
22
+ const conf = STATUS_CONFIG[status] ?? STATUS_CONFIG.unknown;
23
+ return (
24
+ <span className={cn(
25
+ 'inline-flex items-center gap-1.5 rounded-full border font-medium',
26
+ size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-xs',
27
+ conf.bgClass, conf.textClass,
28
+ className,
29
+ )}>
30
+ {showDot && (
31
+ <span className={cn('size-1.5 rounded-full shrink-0', conf.dotClass,
32
+ (status === 'online' || status === 'moving') && 'animate-pulse',
33
+ )} />
34
+ )}
35
+ {conf.label}
36
+ </span>
37
+ );
38
+ }