@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,127 @@
1
+ import { PlatformLink } from '../../platform/context';
2
+
3
+ import { Button } from '../ui/button';
4
+ import { Icon } from '../ui/icon';
5
+ import { lucideIcon } from '../../lib/lucide-icon-map';
6
+ import { cn } from '../../lib/utils';
7
+
8
+ export type CmsButton = {
9
+ label: string;
10
+ link: string;
11
+ icon?: string | null;
12
+ variant?:
13
+ | 'primary'
14
+ | 'secondary'
15
+ | 'outline'
16
+ | 'ghost'
17
+ | 'destructive'
18
+ | null;
19
+ };
20
+
21
+ /**
22
+ * Maps the CMS `buttons[]` variant strings to shadcn Button variants.
23
+ * Our schema uses "primary" (matches user intent); shadcn calls it "default".
24
+ */
25
+ function mapVariant(v?: string | null) {
26
+ switch (v) {
27
+ case 'secondary':
28
+ return 'secondary' as const;
29
+ case 'outline':
30
+ return 'outline' as const;
31
+ case 'ghost':
32
+ return 'ghost' as const;
33
+ case 'destructive':
34
+ return 'destructive' as const;
35
+ case 'primary':
36
+ default:
37
+ return 'primary' as const;
38
+ }
39
+ }
40
+
41
+ function isExternal(href: string): boolean {
42
+ return /^(https?:)?\/\//i.test(href) || /^mailto:|^tel:/i.test(href);
43
+ }
44
+
45
+ function isAnchor(href: string): boolean {
46
+ return href.startsWith('#');
47
+ }
48
+
49
+ export function SectionButton({
50
+ button,
51
+ size = 'lg',
52
+ className,
53
+ }: {
54
+ button: CmsButton;
55
+ size?: 'md' | 'sm' | 'lg';
56
+ className?: string;
57
+ }) {
58
+ const Icon$ = lucideIcon(button.icon);
59
+ const variant = mapVariant(button.variant);
60
+
61
+ const content = (
62
+ <>
63
+ {Icon$ ? <Icon iconNode={Icon$} className="size-4" /> : null}
64
+ {button.label}
65
+ </>
66
+ );
67
+
68
+ if (isExternal(button.link) || isAnchor(button.link)) {
69
+ return (
70
+ <Button
71
+ asChild
72
+ variant={variant}
73
+ size={size}
74
+ className={cn(className)}
75
+ >
76
+ <a
77
+ href={button.link}
78
+ target={isExternal(button.link) ? '_blank' : undefined}
79
+ rel={
80
+ isExternal(button.link)
81
+ ? 'noopener noreferrer'
82
+ : undefined
83
+ }
84
+ >
85
+ {content}
86
+ </a>
87
+ </Button>
88
+ );
89
+ }
90
+
91
+ return (
92
+ <Button asChild variant={variant} size={size} className={cn(className)}>
93
+ <PlatformLink href={button.link}>{content}</PlatformLink>
94
+ </Button>
95
+ );
96
+ }
97
+
98
+ export function SectionButtons({
99
+ buttons,
100
+ align = 'center',
101
+ size = 'lg',
102
+ className,
103
+ }: {
104
+ buttons?: CmsButton[] | null;
105
+ align?: 'left' | 'center' | 'right';
106
+ size?: 'md' | 'sm' | 'lg';
107
+ className?: string;
108
+ }) {
109
+ if (!buttons || buttons.length === 0) {
110
+ return null;
111
+ }
112
+
113
+ return (
114
+ <div
115
+ className={cn(
116
+ 'flex flex-wrap items-center gap-3',
117
+ align === 'center' && 'justify-center',
118
+ align === 'right' && 'justify-end',
119
+ className,
120
+ )}
121
+ >
122
+ {buttons.map((b, i) => (
123
+ <SectionButton key={`${b.label}-${i}`} button={b} size={size} />
124
+ ))}
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,135 @@
1
+ import { Check, ImageOff } from 'lucide-react';
2
+
3
+ import { SectionButtons } from '../section-button';
4
+ import type { CmsButton } from '../section-button';
5
+ import { Icon } from '../../ui/icon';
6
+ import { lucideIcon } from '../../../lib/lucide-icon-map';
7
+ import { cn } from '../../../lib/utils';
8
+
9
+ export type Banner5050Content = {
10
+ image?: string | null;
11
+ image_position?: 'left' | 'right' | null;
12
+ eyebrow?: string | null;
13
+ title?: string | null;
14
+ body?: string | null;
15
+ bullets?: { icon?: string | null; text: string }[] | null;
16
+ buttons?: CmsButton[] | null;
17
+ };
18
+
19
+ /**
20
+ * 50/50 banner — image on one side, text/bullets/CTAs on the other.
21
+ * Used for "Built for engineers", team intros, product highlights —
22
+ * anywhere we want a visual + supporting copy with equal weight.
23
+ *
24
+ * Image renders as a full-bleed cover photo inside a rounded card —
25
+ * no blurred halo / surface wash, so the image keeps a hard edge.
26
+ */
27
+ export function Banner5050Section({
28
+ content,
29
+ identifier,
30
+ }: {
31
+ content?: Banner5050Content;
32
+ identifier?: string | null;
33
+ }) {
34
+ const imagePos = content?.image_position ?? 'left';
35
+
36
+ return (
37
+ <section
38
+ id={identifier ?? undefined}
39
+ className="bg-background py-20 sm:py-24"
40
+ >
41
+ <div className="mx-auto max-w-6xl px-6">
42
+ <div
43
+ className={cn(
44
+ 'grid items-center gap-10 lg:grid-cols-2 lg:gap-16',
45
+ )}
46
+ >
47
+ <div
48
+ className={cn(
49
+ imagePos === 'right' ? 'lg:order-2' : 'lg:order-1',
50
+ )}
51
+ >
52
+ <div className="aspect-[4/3] overflow-hidden rounded-2xl bg-muted/40">
53
+ {content?.image ? (
54
+ <img
55
+ src={content.image}
56
+ alt={content.title ?? ''}
57
+ loading="lazy"
58
+ className="h-full w-full object-cover"
59
+ />
60
+ ) : (
61
+ <div
62
+ aria-label={content?.title ?? ''}
63
+ className="flex h-full w-full items-center justify-center text-muted-foreground"
64
+ >
65
+ <ImageOff className="size-12 opacity-40" />
66
+ </div>
67
+ )}
68
+ </div>
69
+ </div>
70
+
71
+ <div
72
+ className={cn(
73
+ imagePos === 'right' ? 'lg:order-1' : 'lg:order-2',
74
+ )}
75
+ >
76
+ {content?.eyebrow && (
77
+ <p className="mb-3 text-xs font-medium tracking-widest text-muted-foreground uppercase">
78
+ {content.eyebrow}
79
+ </p>
80
+ )}
81
+ {content?.title && (
82
+ <h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
83
+ {content.title}
84
+ </h2>
85
+ )}
86
+ {content?.body && (
87
+ <p className="mt-4 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
88
+ {content.body}
89
+ </p>
90
+ )}
91
+
92
+ {content?.bullets && content.bullets.length > 0 && (
93
+ <ul className="mt-6 space-y-3">
94
+ {content.bullets.map((b, i) => {
95
+ const Icon$ = lucideIcon(b.icon);
96
+
97
+ return (
98
+ <li
99
+ key={i}
100
+ className="flex items-start gap-3 text-sm leading-relaxed"
101
+ >
102
+ <span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
103
+ {Icon$ ? (
104
+ <Icon
105
+ iconNode={Icon$}
106
+ className="size-3"
107
+ />
108
+ ) : (
109
+ <Check className="size-3" />
110
+ )}
111
+ </span>
112
+ <span className="text-foreground">
113
+ {b.text}
114
+ </span>
115
+ </li>
116
+ );
117
+ })}
118
+ </ul>
119
+ )}
120
+
121
+ {content?.buttons && content.buttons.length > 0 && (
122
+ <div className="mt-8">
123
+ <SectionButtons
124
+ buttons={content.buttons}
125
+ align="left"
126
+ size="lg"
127
+ />
128
+ </div>
129
+ )}
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </section>
134
+ );
135
+ }
@@ -0,0 +1,270 @@
1
+ 'use client';
2
+ import { PlatformLink } from '../../../platform/context';
3
+
4
+ import { ArrowRight, Calendar, Search } from 'lucide-react';
5
+ import { useMemo, useState } from 'react';
6
+
7
+ import { BlurredImage } from '../blurred-image';
8
+ import { Badge } from '../../ui/badge';
9
+ import { Card } from '../../ui/card';
10
+ import { Input } from '../../ui/input';
11
+ import { cn } from '../../../lib/utils';
12
+
13
+ import type { BlogPostCard } from './featured-blog-slider-section';
14
+
15
+ export type BlogsListingContent = {
16
+ title?: string | null;
17
+ subtitle?: string | null;
18
+ items_per_page?: number | null;
19
+ show_tag_filter?: boolean | null;
20
+ show_author_filter?: boolean | null;
21
+ };
22
+
23
+ /**
24
+ * Paginated blog listing with client-side search + tag filter.
25
+ * Reads `posts` shared prop (an array of BlogPostCard).
26
+ */
27
+ export function BlogsListingSection({
28
+ content,
29
+ posts,
30
+ identifier,
31
+ }: {
32
+ content?: BlogsListingContent;
33
+ posts?: BlogPostCard[];
34
+ identifier?: string | null;
35
+ }) {
36
+ const perPage = content?.items_per_page ?? 12;
37
+ const showTagFilter = content?.show_tag_filter ?? true;
38
+
39
+ const [query, setQuery] = useState('');
40
+ const [activeTag, setActiveTag] = useState<string | null>(null);
41
+ const [visible, setVisible] = useState(perPage);
42
+
43
+ const allTags = useMemo(() => {
44
+ const seen = new Map<string, { name: string; color: string | null }>();
45
+
46
+ for (const p of posts ?? []) {
47
+ for (const t of p.tags) {
48
+ if (!seen.has(t.slug)) {
49
+ seen.set(t.slug, { name: t.name, color: t.color });
50
+ }
51
+ }
52
+ }
53
+
54
+ return Array.from(seen, ([slug, v]) => ({ slug, ...v }));
55
+ }, [posts]);
56
+
57
+ const filtered = useMemo(() => {
58
+ const q = query.trim().toLowerCase();
59
+
60
+ return (posts ?? []).filter((p) => {
61
+ if (
62
+ q &&
63
+ !`${p.title} ${p.excerpt ?? ''}`.toLowerCase().includes(q)
64
+ ) {
65
+ return false;
66
+ }
67
+
68
+ if (activeTag && !p.tags.some((t) => t.slug === activeTag)) {
69
+ return false;
70
+ }
71
+
72
+ return true;
73
+ });
74
+ }, [posts, query, activeTag]);
75
+
76
+ const shown = filtered.slice(0, visible);
77
+
78
+ return (
79
+ <section
80
+ id={identifier ?? undefined}
81
+ className="bg-background py-20 sm:py-24"
82
+ >
83
+ <div className="mx-auto max-w-6xl px-6">
84
+ {(content?.title || content?.subtitle) && (
85
+ <header className="mb-10 max-w-2xl">
86
+ {content?.title && (
87
+ <h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
88
+ {content.title}
89
+ </h2>
90
+ )}
91
+ {content?.subtitle && (
92
+ <p className="mt-3 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
93
+ {content.subtitle}
94
+ </p>
95
+ )}
96
+ </header>
97
+ )}
98
+
99
+ <div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
100
+ <div className="relative w-full sm:max-w-xs">
101
+ <Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
102
+ <Input
103
+ value={query}
104
+ onChange={(e) => {
105
+ setQuery(e.target.value);
106
+ setVisible(perPage);
107
+ }}
108
+ placeholder="Search articles"
109
+ className="pl-9"
110
+ />
111
+ </div>
112
+
113
+ {showTagFilter && allTags.length > 0 && (
114
+ <div className="flex flex-wrap gap-1.5">
115
+ <TagPill
116
+ label="All"
117
+ isActive={activeTag === null}
118
+ onClick={() => {
119
+ setActiveTag(null);
120
+ setVisible(perPage);
121
+ }}
122
+ />
123
+ {allTags.map((t) => (
124
+ <TagPill
125
+ key={t.slug}
126
+ label={t.name}
127
+ color={t.color}
128
+ isActive={activeTag === t.slug}
129
+ onClick={() => {
130
+ setActiveTag(t.slug);
131
+ setVisible(perPage);
132
+ }}
133
+ />
134
+ ))}
135
+ </div>
136
+ )}
137
+ </div>
138
+
139
+ {shown.length > 0 ? (
140
+ <div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
141
+ {shown.map((p) => (
142
+ <PostCardCell key={p.id} post={p} />
143
+ ))}
144
+ </div>
145
+ ) : (
146
+ <p className="rounded-xl border border-dashed border-border bg-muted/30 py-16 text-center text-sm text-muted-foreground">
147
+ No articles match your filters.
148
+ </p>
149
+ )}
150
+
151
+ {visible < filtered.length && (
152
+ <div className="mt-10 text-center">
153
+ <button
154
+ type="button"
155
+ onClick={() => setVisible((v) => v + perPage)}
156
+ className="text-sm font-medium text-primary hover:underline"
157
+ >
158
+ Load {Math.min(perPage, filtered.length - visible)}{' '}
159
+ more
160
+ </button>
161
+ </div>
162
+ )}
163
+ </div>
164
+ </section>
165
+ );
166
+ }
167
+
168
+ function TagPill({
169
+ label,
170
+ color,
171
+ isActive,
172
+ onClick,
173
+ }: {
174
+ label: string;
175
+ color?: string | null;
176
+ isActive: boolean;
177
+ onClick: () => void;
178
+ }) {
179
+ return (
180
+ <button
181
+ type="button"
182
+ onClick={onClick}
183
+ className={cn(
184
+ 'rounded-full border px-3 py-1 text-xs font-medium transition-colors',
185
+ isActive
186
+ ? 'border-primary bg-primary text-primary-foreground'
187
+ : 'border-border bg-card text-muted-foreground hover:border-primary/40 hover:text-foreground',
188
+ )}
189
+ style={
190
+ isActive && color
191
+ ? {
192
+ backgroundColor: color,
193
+ borderColor: color,
194
+ color: 'white',
195
+ }
196
+ : undefined
197
+ }
198
+ >
199
+ {label}
200
+ </button>
201
+ );
202
+ }
203
+
204
+ function PostCardCell({ post }: { post: BlogPostCard }) {
205
+ return (
206
+ <PlatformLink href={`/blog/${post.slug}`} className="group">
207
+ <Card className="flex h-full flex-col overflow-hidden border-border/60 bg-card p-0 transition-colors hover:border-primary/40">
208
+ <div className="aspect-[16/10] overflow-hidden bg-muted">
209
+ <BlurredImage
210
+ src={post.cover_image}
211
+ alt={post.title}
212
+ padding="p-0"
213
+ rounded="md"
214
+ fit="cover"
215
+ whiteWash={0}
216
+ className="h-full w-full transition-transform duration-500 group-hover:scale-[1.03]"
217
+ />
218
+ </div>
219
+ <div className="flex flex-1 flex-col p-5">
220
+ {post.tags.length > 0 && (
221
+ <div className="flex flex-wrap gap-1.5">
222
+ {post.tags.slice(0, 2).map((t) => (
223
+ <Badge
224
+ key={t.slug}
225
+ variant="secondary"
226
+ className="text-[10px]"
227
+ style={
228
+ t.color
229
+ ? {
230
+ backgroundColor: `${t.color}1a`,
231
+ color: t.color,
232
+ }
233
+ : undefined
234
+ }
235
+ >
236
+ {t.name}
237
+ </Badge>
238
+ ))}
239
+ </div>
240
+ )}
241
+ <h3 className="mt-2 line-clamp-2 text-base font-semibold text-card-foreground group-hover:text-primary">
242
+ {post.title}
243
+ </h3>
244
+ {post.excerpt && (
245
+ <p className="mt-2 line-clamp-3 flex-1 text-sm text-muted-foreground">
246
+ {post.excerpt}
247
+ </p>
248
+ )}
249
+ <div className="mt-4 flex items-center justify-between text-xs text-muted-foreground">
250
+ {post.published_at && (
251
+ <span className="inline-flex items-center gap-1">
252
+ <Calendar className="size-3" />
253
+ <time dateTime={post.published_at}>
254
+ {new Date(
255
+ post.published_at,
256
+ ).toLocaleDateString(undefined, {
257
+ month: 'short',
258
+ day: 'numeric',
259
+ year: 'numeric',
260
+ })}
261
+ </time>
262
+ </span>
263
+ )}
264
+ <ArrowRight className="size-3.5 text-primary transition-transform group-hover:translate-x-0.5" />
265
+ </div>
266
+ </div>
267
+ </Card>
268
+ </PlatformLink>
269
+ );
270
+ }