@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,154 @@
1
+ 'use client';
2
+
3
+ import { PlatformLink } from '../../platform/context';
4
+
5
+ import { CheckCircle2, Mail, MapPin, Phone } from 'lucide-react';
6
+ import { useState } from 'react';
7
+ import { Button } from '../ui/button';
8
+ import { Input } from '../ui/input';
9
+
10
+ type NavLink = { id: number; label: string; href: string; target: string };
11
+
12
+ export type FooterLinks = {
13
+ quick?: NavLink[];
14
+ support?: NavLink[];
15
+ legal?: NavLink[];
16
+ };
17
+
18
+ const CONTACT = {
19
+ phone: '+1 (800) 123-4567',
20
+ email: 'info@trackanydevice.com',
21
+ address: '123 Innovation Drive, Smart City, CA 94043, USA',
22
+ };
23
+
24
+ function DefaultAppLogo() {
25
+ const appName = (typeof window !== 'undefined' && (window as any).AppConfig?.appName) || 'Fleet Tracking';
26
+ return <span className="font-display text-sm font-semibold text-foreground">{appName}</span>;
27
+ }
28
+
29
+ interface SiteFooterProps {
30
+ AppLogo?: React.ComponentType;
31
+ footerLinks?: FooterLinks;
32
+ subscribeEndpoint?: string;
33
+ }
34
+
35
+ export default function SiteFooter({ AppLogo = DefaultAppLogo, footerLinks, subscribeEndpoint = '/subscribe' }: SiteFooterProps) {
36
+ const groups = footerLinks ?? { quick: [], support: [], legal: [] };
37
+ const appName = (typeof window !== 'undefined' && (window as any).AppConfig?.appName) || 'Fleet Tracking';
38
+
39
+ return (
40
+ <footer id="contact" className="border-t border-border bg-card">
41
+ <div className="mx-auto max-w-7xl px-5 py-14 sm:px-8 lg:px-16">
42
+ <div className="grid gap-10 border-b border-border pb-10 lg:grid-cols-2">
43
+ <div>
44
+ <PlatformLink href="/" className="mb-4 inline-flex items-center gap-2.5">
45
+ <div className="flex h-9 items-center"><AppLogo /></div>
46
+ <span className="font-display text-sm font-semibold text-foreground">{appName}</span>
47
+ </PlatformLink>
48
+ <p className="max-w-md text-sm leading-relaxed text-muted-foreground">
49
+ Smart tracking solutions for a connected world. Track any device, anywhere, anytime.
50
+ </p>
51
+ </div>
52
+
53
+ <div className="grid gap-3 sm:grid-cols-3">
54
+ <ContactRow icon={Phone} label="Phone" value={CONTACT.phone} />
55
+ <ContactRow icon={Mail} label="Email" value={CONTACT.email} />
56
+ <ContactRow icon={MapPin} label="Address" value={CONTACT.address} />
57
+ </div>
58
+ </div>
59
+
60
+ <div className="grid gap-10 py-10 sm:grid-cols-3">
61
+ <FooterColumn title="Quick Links" links={groups.quick ?? []} />
62
+ <FooterColumn title="Policies" links={groups.legal ?? []} />
63
+ <SubscriptionForm endpoint={subscribeEndpoint} />
64
+ </div>
65
+ </div>
66
+
67
+ <div className="border-t border-border bg-muted/30">
68
+ <div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-3 px-5 py-5 text-xs text-muted-foreground sm:flex-row sm:px-8 lg:px-16">
69
+ <span>© {new Date().getFullYear()} {appName}. All rights reserved.</span>
70
+ <span>Built for modern field operations · IoT · Fleet · Smart Infrastructure</span>
71
+ </div>
72
+ </div>
73
+ </footer>
74
+ );
75
+ }
76
+
77
+ function FooterColumn({ title, links }: { title: string; links: NavLink[] }) {
78
+ if (!links || links.length === 0) return null;
79
+ return (
80
+ <div>
81
+ <h3 className="mb-4 font-display text-xs font-semibold tracking-widest text-foreground uppercase">{title}</h3>
82
+ <ul className="space-y-2.5">
83
+ {links.map((link) => (
84
+ <li key={link.id}>
85
+ <PlatformLink href={link.href} target={link.target === '_blank' ? '_blank' : undefined} rel={link.target === '_blank' ? 'noopener noreferrer' : undefined} className="text-sm text-muted-foreground transition-colors hover:text-foreground">
86
+ {link.label}
87
+ </PlatformLink>
88
+ </li>
89
+ ))}
90
+ </ul>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ function ContactRow({ icon: IconCmp, label, value }: { icon: React.ComponentType<{ className?: string }>; label: string; value: string }) {
96
+ return (
97
+ <div className="flex items-start gap-2.5">
98
+ <IconCmp className="mt-0.5 size-4 shrink-0 text-primary" />
99
+ <div>
100
+ <p className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">{label}</p>
101
+ <p className="text-sm text-foreground">{value}</p>
102
+ </div>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ function SubscriptionForm({ endpoint }: { endpoint: string }) {
108
+ const [email, setEmail] = useState('');
109
+ const [submitting, setSubmitting] = useState(false);
110
+ const [done, setDone] = useState(false);
111
+ const [error, setError] = useState('');
112
+
113
+ async function submit(e: React.FormEvent) {
114
+ e.preventDefault();
115
+ setSubmitting(true);
116
+ setError('');
117
+ try {
118
+ const res = await fetch(endpoint, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
121
+ body: JSON.stringify({ email, source: 'footer' }),
122
+ });
123
+ if (!res.ok) {
124
+ const data = await res.json().catch(() => ({}));
125
+ setError(data?.errors?.email ?? 'Something went wrong.');
126
+ } else {
127
+ setDone(true);
128
+ setEmail('');
129
+ setTimeout(() => setDone(false), 4000);
130
+ }
131
+ } catch {
132
+ setError('Network error. Please try again.');
133
+ } finally {
134
+ setSubmitting(false);
135
+ }
136
+ }
137
+
138
+ return (
139
+ <div>
140
+ <h3 className="mb-4 font-display text-xs font-semibold tracking-widest text-foreground uppercase">Subscribe</h3>
141
+ <p className="mb-3 text-sm text-muted-foreground">Product updates, new device launches, and tips for getting more out of your fleet.</p>
142
+ <form className="flex flex-col gap-2 sm:flex-row" onSubmit={submit} noValidate>
143
+ <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="your@email.com" aria-label="Email address" required className="flex-1" />
144
+ <Button type="submit" disabled={submitting} className="shrink-0">{submitting ? '…' : 'Subscribe'}</Button>
145
+ </form>
146
+ {error && <p className="mt-2 text-xs text-destructive">{error}</p>}
147
+ {done && (
148
+ <p className="mt-2 inline-flex items-center gap-1 text-xs text-primary">
149
+ <CheckCircle2 className="size-3" /> Thanks! You're subscribed.
150
+ </p>
151
+ )}
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,159 @@
1
+ 'use client';
2
+
3
+ import { LayoutDashboard, Menu, Moon, ShoppingCart, Sun } from 'lucide-react';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ import { Button } from '../ui/button';
7
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../ui/sheet';
8
+ import { useDarkMode } from '../../hooks/use-dark-mode';
9
+ import { cn } from '../../lib/utils';
10
+ import { PlatformLink } from '../../platform/context';
11
+
12
+ type NavLink = { id: number; label: string; href: string; target: string };
13
+
14
+ export type SiteHeaderAuth = {
15
+ user: { name: string; role: string } | null;
16
+ };
17
+
18
+ export type SiteHeaderHosts = {
19
+ central?: string | null;
20
+ admin?: string | null;
21
+ my?: string | null;
22
+ };
23
+
24
+ const FALLBACK_LINKS: NavLink[] = [
25
+ { id: 1, label: 'Products', href: '/products', target: '_self' },
26
+ { id: 2, label: 'Solutions', href: '/solutions', target: '_self' },
27
+ { id: 3, label: 'TAD101 Docs',href: '/docs/tad101', target: '_self' },
28
+ { id: 4, label: 'Industries', href: '/industries', target: '_self' },
29
+ { id: 5, label: 'Blog', href: '/blog', target: '_self' },
30
+ { id: 6, label: 'About', href: '/about', target: '_self' },
31
+ { id: 7, label: 'Contact', href: '/contact', target: '_self' },
32
+ ];
33
+
34
+ function DefaultAppLogo() {
35
+ const appName = (typeof window !== 'undefined' && (window as any).AppConfig?.appName) || 'Fleet Tracking';
36
+ return <span className="font-display text-sm font-semibold text-foreground">{appName}</span>;
37
+ }
38
+
39
+ interface SiteHeaderProps {
40
+ AppLogo?: React.ComponentType;
41
+ navLinks?: NavLink[];
42
+ auth?: SiteHeaderAuth;
43
+ hosts?: SiteHeaderHosts;
44
+ }
45
+
46
+ export default function SiteHeader({ AppLogo = DefaultAppLogo, navLinks, auth, hosts }: SiteHeaderProps) {
47
+ const links = navLinks?.length ? navLinks : FALLBACK_LINKS;
48
+ const user = auth?.user ?? null;
49
+
50
+ const dashboardHref = (() => {
51
+ if (user?.role === 'admin') return hosts?.admin ?? '/admin';
52
+ return hosts?.my ?? '/dashboard';
53
+ })();
54
+ const dashboardIsExternal = /^https?:\/\//.test(dashboardHref);
55
+
56
+ const [scrolled, setScrolled] = useState(false);
57
+ const [mobileOpen, setMobileOpen] = useState(false);
58
+
59
+ useEffect(() => {
60
+ const handler = () => setScrolled(window.scrollY > 24);
61
+ handler();
62
+ window.addEventListener('scroll', handler, { passive: true });
63
+ return () => window.removeEventListener('scroll', handler);
64
+ }, []);
65
+
66
+ return (
67
+ <header className={cn(
68
+ 'fixed inset-x-0 top-0 z-50 border-b transition-colors duration-200',
69
+ scrolled ? 'border-border bg-background/85 backdrop-blur-md' : 'border-transparent bg-background/0',
70
+ )}>
71
+ <nav className="mx-auto flex h-16 max-w-7xl items-center gap-4 px-5">
72
+ <PlatformLink href="/" className="flex shrink-0 items-center gap-2" aria-label="Home">
73
+ <AppLogo />
74
+ </PlatformLink>
75
+
76
+ <ul className="hidden flex-1 items-center justify-center gap-1 lg:flex">
77
+ {links.map((link) => (
78
+ <li key={link.id}><NavItem link={link} /></li>
79
+ ))}
80
+ </ul>
81
+
82
+ <div className="ml-auto flex items-center gap-1.5">
83
+ <DarkModeToggle />
84
+
85
+ <Button asChild variant="ghost" size="icon" aria-label="Cart">
86
+ <PlatformLink href="/cart"><ShoppingCart className="size-4" /></PlatformLink>
87
+ </Button>
88
+
89
+ <Button asChild size="sm" className="hidden sm:inline-flex">
90
+ <PlatformLink href={dashboardHref} {...(dashboardIsExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}>
91
+ <LayoutDashboard className="size-4" />
92
+ {user?.role === 'admin' ? 'Admin' : 'Dashboard'}
93
+ </PlatformLink>
94
+ </Button>
95
+
96
+ <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
97
+ <SheetTrigger asChild>
98
+ <Button variant="ghost" size="icon" className="lg:hidden" aria-label="Open menu">
99
+ <Menu className="size-5" />
100
+ </Button>
101
+ </SheetTrigger>
102
+ <SheetContent side="right" className="w-full sm:max-w-sm">
103
+ <SheetHeader><SheetTitle>Menu</SheetTitle></SheetHeader>
104
+ <ul className="mt-2 flex flex-col gap-1 px-2">
105
+ {links.map((link) => (
106
+ <li key={link.id}>
107
+ <NavItem link={link} mobile onNavigate={() => setMobileOpen(false)} />
108
+ </li>
109
+ ))}
110
+ </ul>
111
+ <div className="mt-6 flex flex-col gap-2 px-2">
112
+ <Button asChild>
113
+ <PlatformLink href={dashboardHref} onClick={() => setMobileOpen(false)}>
114
+ <LayoutDashboard className="size-4" />
115
+ {user?.role === 'admin' ? 'Admin' : 'Dashboard'}
116
+ </PlatformLink>
117
+ </Button>
118
+ <Button asChild variant="outline">
119
+ <PlatformLink href="/cart" onClick={() => setMobileOpen(false)}>
120
+ <ShoppingCart className="size-4" /> Cart
121
+ </PlatformLink>
122
+ </Button>
123
+ </div>
124
+ </SheetContent>
125
+ </Sheet>
126
+ </div>
127
+ </nav>
128
+ </header>
129
+ );
130
+ }
131
+
132
+ function NavItem({ link, mobile, onNavigate }: { link: NavLink; mobile?: boolean; onNavigate?: () => void }) {
133
+ const baseClass = mobile
134
+ ? 'block rounded-md px-3 py-2.5 text-sm font-medium text-foreground hover:bg-accent hover:text-accent-foreground'
135
+ : 'rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground';
136
+
137
+ if (link.href.startsWith('#')) {
138
+ return (
139
+ <PlatformLink href={link.href} onClick={(e) => { e?.preventDefault(); document.querySelector(link.href)?.scrollIntoView({ behavior: 'smooth' }); onNavigate?.(); }} className={baseClass}>
140
+ {link.label}
141
+ </PlatformLink>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <PlatformLink href={link.href} target={link.target === '_blank' ? '_blank' : undefined} rel={link.target === '_blank' ? 'noopener noreferrer' : undefined} className={baseClass} onClick={onNavigate}>
147
+ {link.label}
148
+ </PlatformLink>
149
+ );
150
+ }
151
+
152
+ function DarkModeToggle() {
153
+ const { isDark, toggle } = useDarkMode();
154
+ return (
155
+ <Button variant="ghost" size="icon" onClick={toggle} aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}>
156
+ {isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
157
+ </Button>
158
+ );
159
+ }
@@ -0,0 +1,321 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * WorkflowCanvas — React Flow visual editor for workflow graphs.
5
+ *
6
+ * The underlying data shape (nodes + edges with action_type / config in
7
+ * node.data) was already React-Flow-compatible — this component drops in
8
+ * over the same structure the executor walks server-side.
9
+ *
10
+ * The toolbar lives in the parent edit page; this component renders only
11
+ * the canvas + action palette and emits change events upward.
12
+ */
13
+ import {
14
+ addEdge,
15
+ Background,
16
+ Controls,
17
+ Handle,
18
+ Position,
19
+ ReactFlow,
20
+ ReactFlowProvider,
21
+ useEdgesState,
22
+ useNodesState,
23
+ } from '@xyflow/react';
24
+ import type { Connection, Edge, Node, NodeProps } from '@xyflow/react';
25
+ import '@xyflow/react/dist/style.css';
26
+
27
+ import {
28
+ AlertCircle,
29
+ Bell,
30
+ Hourglass,
31
+ Plus,
32
+ Send,
33
+ Trash2,
34
+ Webhook,
35
+ Zap,
36
+ } from 'lucide-react';
37
+ import { useCallback, useEffect, useRef } from 'react';
38
+ import type { ComponentType } from 'react';
39
+
40
+ import { cn } from '../../lib/utils';
41
+
42
+ export type WorkflowNodeData = {
43
+ action_type: string;
44
+ label: string;
45
+ config: Record<string, unknown>;
46
+ };
47
+
48
+ export type WorkflowNode = Node<WorkflowNodeData>;
49
+ export type WorkflowEdge = Edge;
50
+
51
+ export type WorkflowGraph = {
52
+ nodes: WorkflowNode[];
53
+ edges: WorkflowEdge[];
54
+ };
55
+
56
+ type ActionOption = {
57
+ value: string;
58
+ label: string;
59
+ icon: string;
60
+ color: string;
61
+ };
62
+
63
+ type Props = {
64
+ graph: WorkflowGraph;
65
+ actions: ActionOption[];
66
+ onChange: (graph: WorkflowGraph) => void;
67
+ onNodeSelect: (nodeId: string | null) => void;
68
+ selectedNodeId: string | null;
69
+ };
70
+
71
+ const ACTION_ICON: Record<string, ComponentType<{ className?: string }>> = {
72
+ trigger: Zap,
73
+ wait: Hourglass,
74
+ notify: Bell,
75
+ send_command: Send,
76
+ escalate_incident: AlertCircle,
77
+ webhook: Webhook,
78
+ };
79
+
80
+ const ACTION_TONE: Record<string, string> = {
81
+ trigger: 'border-primary/50 bg-primary/10 text-primary',
82
+ wait: 'border-warning/40 bg-warning-subtle text-warning-fg',
83
+ notify: 'border-info/40 bg-info-subtle text-info-fg',
84
+ send_command: 'border-primary/50 bg-primary/10 text-primary',
85
+ escalate_incident: 'border-danger/40 bg-danger-subtle text-danger',
86
+ webhook: 'border-border bg-accent text-accent-foreground',
87
+ };
88
+
89
+ function WorkflowNodeRenderer({ data, selected, id }: NodeProps<WorkflowNode>) {
90
+ const Icon = ACTION_ICON[data.action_type] ?? Plus;
91
+ const tone = ACTION_TONE[data.action_type] ?? 'border-border bg-card';
92
+ const isTrigger = data.action_type === 'trigger';
93
+
94
+ return (
95
+ <div
96
+ className={cn(
97
+ 'min-w-[180px] rounded-xl border-2 bg-background px-4 py-3 shadow-sm transition-all',
98
+ tone,
99
+ selected &&
100
+ 'ring-2 ring-primary ring-offset-2 ring-offset-background',
101
+ )}
102
+ data-node-id={id}
103
+ >
104
+ {!isTrigger && (
105
+ <Handle
106
+ type="target"
107
+ position={Position.Left}
108
+ className="!h-3 !w-3 !border-2 !bg-background"
109
+ />
110
+ )}
111
+ <div className="flex items-center gap-2">
112
+ <div
113
+ className={cn(
114
+ 'grid size-8 shrink-0 place-items-center rounded-full',
115
+ tone,
116
+ )}
117
+ >
118
+ <Icon className="size-4" />
119
+ </div>
120
+ <div className="min-w-0">
121
+ <p className="truncate text-sm font-semibold">
122
+ {data.label}
123
+ </p>
124
+ <p className="truncate text-[10px] tracking-wide text-muted-foreground uppercase">
125
+ {data.action_type.replace('_', ' ')}
126
+ </p>
127
+ </div>
128
+ </div>
129
+ <Handle
130
+ type="source"
131
+ position={Position.Right}
132
+ className="!h-3 !w-3 !border-2 !bg-background"
133
+ />
134
+ </div>
135
+ );
136
+ }
137
+
138
+ const nodeTypes = { workflowNode: WorkflowNodeRenderer };
139
+
140
+ function Canvas({
141
+ graph,
142
+ actions,
143
+ onChange,
144
+ onNodeSelect,
145
+ selectedNodeId,
146
+ }: Props) {
147
+ const [nodes, setNodes, onNodesChange] = useNodesState<WorkflowNode>(
148
+ graph.nodes,
149
+ );
150
+ const [edges, setEdges, onEdgesChange] = useEdgesState<WorkflowEdge>(
151
+ graph.edges,
152
+ );
153
+ // Track which graph (by reference equality) is currently mirrored into
154
+ // React Flow state. When the parent replaces the graph (e.g. on save),
155
+ // we reset internal state to match.
156
+ const lastSyncedGraph = useRef(graph);
157
+
158
+ useEffect(() => {
159
+ if (graph !== lastSyncedGraph.current) {
160
+ setNodes(graph.nodes);
161
+ setEdges(graph.edges);
162
+ lastSyncedGraph.current = graph;
163
+ }
164
+ }, [graph, setNodes, setEdges]);
165
+
166
+ // Push canvas changes back to the parent so save sees the latest graph.
167
+ useEffect(() => {
168
+ const next = { nodes, edges };
169
+ onChange(next);
170
+ lastSyncedGraph.current = next;
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ }, [nodes, edges]);
173
+
174
+ const onConnect = useCallback(
175
+ (connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
176
+ [setEdges],
177
+ );
178
+
179
+ const addNode = (actionType: string) => {
180
+ // Date.now() is flagged by the react-hooks/purity rule because
181
+ // it's impure; addNode is an event handler (not called during
182
+ // render), so reading the clock here is safe — eslint-disable
183
+ // the one line rather than memoising a counter.
184
+ // eslint-disable-next-line react-hooks/purity
185
+ const id = `${actionType}-${Date.now()}`;
186
+ const lastNode = nodes[nodes.length - 1];
187
+ const x = lastNode ? lastNode.position.x + 240 : 100;
188
+ const y = lastNode ? lastNode.position.y : 100;
189
+ const newNode: WorkflowNode = {
190
+ id,
191
+ type: 'workflowNode',
192
+ position: { x, y },
193
+ data: {
194
+ action_type: actionType,
195
+ label:
196
+ actions.find((a) => a.value === actionType)?.label ??
197
+ actionType,
198
+ config: defaultConfigFor(actionType),
199
+ },
200
+ };
201
+
202
+ const newEdges = lastNode
203
+ ? [
204
+ {
205
+ id: `e-${lastNode.id}-${id}`,
206
+ source: lastNode.id,
207
+ target: id,
208
+ },
209
+ ]
210
+ : [];
211
+
212
+ setNodes((ns) => [...ns, newNode]);
213
+ setEdges((es) => [...es, ...newEdges]);
214
+ onNodeSelect(id);
215
+ };
216
+
217
+ const removeSelectedNode = () => {
218
+ if (!selectedNodeId || selectedNodeId === 'trigger') {
219
+ return;
220
+ }
221
+
222
+ setNodes((ns) => ns.filter((n) => n.id !== selectedNodeId));
223
+ setEdges((es) =>
224
+ es.filter(
225
+ (e) =>
226
+ e.source !== selectedNodeId && e.target !== selectedNodeId,
227
+ ),
228
+ );
229
+ onNodeSelect(null);
230
+ };
231
+
232
+ return (
233
+ <div className="flex h-full">
234
+ {/* Left rail — action palette */}
235
+ <aside className="w-56 shrink-0 overflow-auto border-r border-border bg-card p-4">
236
+ <h2 className="mb-3 text-xs font-semibold tracking-wide text-muted-foreground uppercase">
237
+ Actions
238
+ </h2>
239
+ <div className="space-y-2">
240
+ {actions.map((a) => {
241
+ const Icon = ACTION_ICON[a.value] ?? Plus;
242
+
243
+ return (
244
+ <button
245
+ key={a.value}
246
+ onClick={() => addNode(a.value)}
247
+ className={cn(
248
+ 'flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-left text-sm transition-colors hover:bg-accent',
249
+ ACTION_TONE[a.value] ??
250
+ 'border-border bg-background',
251
+ )}
252
+ >
253
+ <Icon className="size-4 shrink-0" />
254
+ {a.label}
255
+ </button>
256
+ );
257
+ })}
258
+ </div>
259
+
260
+ {selectedNodeId && selectedNodeId !== 'trigger' && (
261
+ <button
262
+ onClick={removeSelectedNode}
263
+ className="mt-6 flex w-full items-center gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-left text-sm text-destructive hover:bg-destructive/10"
264
+ >
265
+ <Trash2 className="size-4 shrink-0" />
266
+ Delete selected node
267
+ </button>
268
+ )}
269
+ </aside>
270
+
271
+ {/* Canvas */}
272
+ <div className="flex-1 bg-muted/30">
273
+ <ReactFlow
274
+ nodes={nodes}
275
+ edges={edges}
276
+ onNodesChange={onNodesChange}
277
+ onEdgesChange={onEdgesChange}
278
+ onConnect={onConnect}
279
+ onNodeClick={(_, node) => onNodeSelect(node.id)}
280
+ onPaneClick={() => onNodeSelect(null)}
281
+ nodeTypes={nodeTypes}
282
+ fitView
283
+ fitViewOptions={{ padding: 0.2, maxZoom: 1.5 }}
284
+ proOptions={{ hideAttribution: true }}
285
+ >
286
+ <Background gap={20} size={1} />
287
+ <Controls position="bottom-right" />
288
+ </ReactFlow>
289
+ </div>
290
+ </div>
291
+ );
292
+ }
293
+
294
+ /**
295
+ * Wrap the canvas in ReactFlowProvider so multiple instances can coexist
296
+ * (e.g. a future split view of workflow + run logs side by side).
297
+ */
298
+ export function WorkflowCanvas(props: Props) {
299
+ return (
300
+ <ReactFlowProvider>
301
+ <Canvas {...props} />
302
+ </ReactFlowProvider>
303
+ );
304
+ }
305
+
306
+ function defaultConfigFor(actionType: string): Record<string, unknown> {
307
+ switch (actionType) {
308
+ case 'wait':
309
+ return { seconds: 10 };
310
+ case 'notify':
311
+ return { channels: ['in_app'], message: '' };
312
+ case 'send_command':
313
+ return { command: '', parameters: {} };
314
+ case 'escalate_incident':
315
+ return { priority: 'critical' };
316
+ case 'webhook':
317
+ return { url: '', method: 'POST' };
318
+ default:
319
+ return {};
320
+ }
321
+ }
@@ -0,0 +1,25 @@
1
+ import type { BlockquoteHTMLAttributes, ReactNode } from 'react';
2
+
3
+ interface Props extends BlockquoteHTMLAttributes<HTMLQuoteElement> {
4
+ children: ReactNode;
5
+ cite?: string;
6
+ }
7
+
8
+ export function Blockquote({ children, cite, className = '', ...props }: Props) {
9
+ return (
10
+ <figure className={className}>
11
+ <blockquote
12
+ cite={cite}
13
+ className="border-l-4 border-primary pl-4 italic text-muted-foreground"
14
+ {...props}
15
+ >
16
+ {children}
17
+ </blockquote>
18
+ {cite && (
19
+ <figcaption className="mt-2 pl-4 text-xs text-muted-foreground/70">
20
+ — <cite>{cite}</cite>
21
+ </figcaption>
22
+ )}
23
+ </figure>
24
+ );
25
+ }