@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,180 @@
1
+ import { SectionBackground } from '../section-bg';
2
+ import type { CmsBackground } from '../section-bg';
3
+ import { SectionButtons } from '../section-button';
4
+ import type { CmsButton } from '../section-button';
5
+ import { cn } from '../../../lib/utils';
6
+
7
+ export type HeroContent = {
8
+ size?: 'full' | 'half' | 'third' | null;
9
+ alignment?: 'left' | 'center' | null;
10
+ eyebrow?: string | null;
11
+ title?: string | null;
12
+ title_highlight?: string | null;
13
+ subtitle?: string | null;
14
+ body?: string | null;
15
+ bg?: CmsBackground | null;
16
+ buttons?: CmsButton[] | null;
17
+ };
18
+
19
+ const SIZE_CLASSES: Record<string, string> = {
20
+ full: 'min-h-[88vh]',
21
+ half: 'min-h-[60vh]',
22
+ third: 'min-h-[40vh]',
23
+ };
24
+
25
+ /**
26
+ * Hero section — the universal page header. Three size variants
27
+ * (full / half / third), five background modes, optional eyebrow,
28
+ * highlighted title fragment, subtitle, body, and a buttons[] CTA row.
29
+ *
30
+ * Light-on-dark vs dark-on-light is decided automatically by the bg
31
+ * mode: image, video, map, and gradient default to light text; color
32
+ * uses the matching foreground token.
33
+ */
34
+ export function HeroSection({
35
+ content,
36
+ identifier,
37
+ }: {
38
+ content?: HeroContent;
39
+ identifier?: string | null;
40
+ }) {
41
+ const size = content?.size ?? 'half';
42
+ const alignment = content?.alignment ?? 'center';
43
+ const bg = content?.bg ?? { kind: 'color', color_token: 'muted' };
44
+ const isDarkSurface = ['image', 'video', 'map', 'gradient'].includes(
45
+ bg.kind ?? '',
46
+ );
47
+ const isMapSurface = bg.kind === 'map';
48
+ const isPrimaryColor =
49
+ bg.kind === 'color' && (bg.color_token ?? 'primary') === 'primary';
50
+ const textColor =
51
+ isDarkSurface || isPrimaryColor ? 'text-white' : 'text-foreground';
52
+ const supportColor =
53
+ isDarkSurface || isPrimaryColor
54
+ ? 'text-white/85'
55
+ : 'text-muted-foreground';
56
+ const eyebrowColor =
57
+ isDarkSurface || isPrimaryColor
58
+ ? 'text-white/70'
59
+ : 'text-muted-foreground';
60
+
61
+ const title = content?.title ?? '';
62
+ const highlight = content?.title_highlight?.trim();
63
+ const titleNode =
64
+ highlight && title.includes(highlight)
65
+ ? renderHighlightedTitle(
66
+ title,
67
+ highlight,
68
+ isDarkSurface || isPrimaryColor,
69
+ )
70
+ : title;
71
+
72
+ return (
73
+ <section
74
+ id={identifier ?? undefined}
75
+ className={cn(
76
+ 'relative isolate flex w-full items-center overflow-hidden',
77
+ SIZE_CLASSES[size] ?? SIZE_CLASSES.half,
78
+ )}
79
+ >
80
+ <SectionBackground bg={bg} />
81
+
82
+ <div
83
+ className={cn(
84
+ 'relative mx-auto w-full max-w-5xl px-6 py-16 sm:py-20 lg:py-24',
85
+ alignment === 'center' ? 'text-center' : 'text-left',
86
+ // On a live map background the moving tiles can otherwise
87
+ // wash out the text. A subtle frosted card behind the
88
+ // copy keeps the title and CTAs readable on any region.
89
+ isMapSurface &&
90
+ 'rounded-3xl bg-black/35 ring-1 ring-white/10 backdrop-blur-sm',
91
+ )}
92
+ >
93
+ {content?.eyebrow ? (
94
+ <p
95
+ className={cn(
96
+ 'mb-4 text-xs font-medium tracking-widest uppercase',
97
+ eyebrowColor,
98
+ )}
99
+ >
100
+ {content.eyebrow}
101
+ </p>
102
+ ) : null}
103
+
104
+ {title ? (
105
+ <h1
106
+ className={cn(
107
+ 'text-4xl font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl',
108
+ textColor,
109
+ isDarkSurface &&
110
+ '[text-shadow:0_2px_24px_rgba(0,0,0,0.4)]',
111
+ )}
112
+ >
113
+ {titleNode}
114
+ </h1>
115
+ ) : null}
116
+
117
+ {content?.subtitle ? (
118
+ <p
119
+ className={cn(
120
+ 'mx-auto mt-6 max-w-2xl text-lg leading-relaxed text-pretty sm:text-xl',
121
+ supportColor,
122
+ alignment === 'left' && 'mx-0',
123
+ isDarkSurface &&
124
+ '[text-shadow:0_1px_12px_rgba(0,0,0,0.5)]',
125
+ )}
126
+ >
127
+ {content.subtitle}
128
+ </p>
129
+ ) : null}
130
+
131
+ {content?.body ? (
132
+ <p
133
+ className={cn(
134
+ 'mx-auto mt-4 max-w-2xl text-base leading-relaxed text-pretty',
135
+ supportColor,
136
+ alignment === 'left' && 'mx-0',
137
+ )}
138
+ >
139
+ {content.body}
140
+ </p>
141
+ ) : null}
142
+
143
+ {content?.buttons && content.buttons.length > 0 ? (
144
+ <div className="mt-10">
145
+ <SectionButtons
146
+ buttons={content.buttons}
147
+ align={alignment}
148
+ size="lg"
149
+ />
150
+ </div>
151
+ ) : null}
152
+ </div>
153
+ </section>
154
+ );
155
+ }
156
+
157
+ function renderHighlightedTitle(
158
+ title: string,
159
+ highlight: string,
160
+ onDark: boolean,
161
+ ) {
162
+ const idx = title.indexOf(highlight);
163
+
164
+ if (idx === -1) {
165
+ return title;
166
+ }
167
+
168
+ const before = title.slice(0, idx);
169
+ const after = title.slice(idx + highlight.length);
170
+
171
+ return (
172
+ <>
173
+ {before}
174
+ <span className={cn(onDark ? 'text-primary' : 'text-primary')}>
175
+ {highlight}
176
+ </span>
177
+ {after}
178
+ </>
179
+ );
180
+ }
@@ -0,0 +1,234 @@
1
+ 'use client';
2
+ import { PlatformLink } from '../../../platform/context';
3
+
4
+ import { ArrowRight, Search } from 'lucide-react';
5
+ import { useMemo, useState } from 'react';
6
+
7
+ import { Card } from '../../ui/card';
8
+ import { Icon } from '../../ui/icon';
9
+ import { Input } from '../../ui/input';
10
+ import { lucideIcon } from '../../../lib/lucide-icon-map';
11
+ import { cn } from '../../../lib/utils';
12
+
13
+ import type { SolutionCard } from './featured-solutions-grid-section';
14
+
15
+ export type SolutionsWithFilterContent = {
16
+ title?: string | null;
17
+ subtitle?: string | null;
18
+ items_per_page?: number | null;
19
+ filters?: {
20
+ industries?: { label: string; value: string }[];
21
+ categories?: { label: string; value: string }[];
22
+ } | null;
23
+ };
24
+
25
+ /**
26
+ * Solutions list with client-side filters. Reads `solutions` shared
27
+ * prop. Provides a search input and optional pill-style facet filters
28
+ * (industries / categories). Pagination is "Load more" on top of the
29
+ * filtered list.
30
+ */
31
+ export function SolutionsWithFilterSection({
32
+ content,
33
+ solutions,
34
+ identifier,
35
+ }: {
36
+ content?: SolutionsWithFilterContent;
37
+ solutions?: SolutionCard[];
38
+ identifier?: string | null;
39
+ }) {
40
+ const perPage = content?.items_per_page ?? 12;
41
+
42
+ const [query, setQuery] = useState('');
43
+ const [activeFilter, setActiveFilter] = useState<string | null>(null);
44
+ const [visible, setVisible] = useState(perPage);
45
+
46
+ const filtered = useMemo(() => {
47
+ const q = query.trim().toLowerCase();
48
+
49
+ return (solutions ?? []).filter((s) => {
50
+ if (
51
+ q &&
52
+ !`${s.title} ${s.description ?? ''}`.toLowerCase().includes(q)
53
+ ) {
54
+ return false;
55
+ }
56
+
57
+ // Filter facet stored on solution's metadata (best-effort match).
58
+ // For now, simple title-contains match against the facet value
59
+ // so seeded content "just works" without a schema change.
60
+ if (
61
+ activeFilter &&
62
+ !s.title.toLowerCase().includes(activeFilter.toLowerCase())
63
+ ) {
64
+ return false;
65
+ }
66
+
67
+ return true;
68
+ });
69
+ }, [solutions, query, activeFilter]);
70
+
71
+ const shown = filtered.slice(0, visible);
72
+
73
+ return (
74
+ <section
75
+ id={identifier ?? undefined}
76
+ className="bg-background py-20 sm:py-24"
77
+ >
78
+ <div className="mx-auto max-w-6xl px-6">
79
+ {(content?.title || content?.subtitle) && (
80
+ <header className="mb-10 max-w-2xl">
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?.subtitle && (
87
+ <p className="mt-3 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
88
+ {content.subtitle}
89
+ </p>
90
+ )}
91
+ </header>
92
+ )}
93
+
94
+ <div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
95
+ <div className="relative w-full sm:max-w-xs">
96
+ <Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
97
+ <Input
98
+ value={query}
99
+ onChange={(e) => {
100
+ setQuery(e.target.value);
101
+ setVisible(perPage);
102
+ }}
103
+ placeholder="Search solutions"
104
+ className="pl-9"
105
+ />
106
+ </div>
107
+
108
+ {content?.filters?.industries &&
109
+ content.filters.industries.length > 0 && (
110
+ <FilterPills
111
+ options={content.filters.industries}
112
+ active={activeFilter}
113
+ onChange={(v) => {
114
+ setActiveFilter(v);
115
+ setVisible(perPage);
116
+ }}
117
+ />
118
+ )}
119
+ </div>
120
+
121
+ {shown.length > 0 ? (
122
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
123
+ {shown.map((s) => (
124
+ <SolutionCardCell key={s.id} solution={s} />
125
+ ))}
126
+ </div>
127
+ ) : (
128
+ <p className="rounded-xl border border-dashed border-border bg-muted/30 py-16 text-center text-sm text-muted-foreground">
129
+ No solutions match your filters.
130
+ </p>
131
+ )}
132
+
133
+ {visible < filtered.length && (
134
+ <div className="mt-8 text-center">
135
+ <button
136
+ type="button"
137
+ onClick={() => setVisible((v) => v + perPage)}
138
+ className="text-sm font-medium text-primary hover:underline"
139
+ >
140
+ Load {Math.min(perPage, filtered.length - visible)}{' '}
141
+ more
142
+ </button>
143
+ </div>
144
+ )}
145
+ </div>
146
+ </section>
147
+ );
148
+ }
149
+
150
+ function FilterPills({
151
+ options,
152
+ active,
153
+ onChange,
154
+ }: {
155
+ options: { label: string; value: string }[];
156
+ active: string | null;
157
+ onChange: (v: string | null) => void;
158
+ }) {
159
+ return (
160
+ <div className="flex flex-wrap gap-1.5">
161
+ <FilterPillButton
162
+ label="All"
163
+ isActive={active === null}
164
+ onClick={() => onChange(null)}
165
+ />
166
+ {options.map((o) => (
167
+ <FilterPillButton
168
+ key={o.value}
169
+ label={o.label}
170
+ isActive={active === o.value}
171
+ onClick={() => onChange(o.value)}
172
+ />
173
+ ))}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ function FilterPillButton({
179
+ label,
180
+ isActive,
181
+ onClick,
182
+ }: {
183
+ label: string;
184
+ isActive: boolean;
185
+ onClick: () => void;
186
+ }) {
187
+ return (
188
+ <button
189
+ type="button"
190
+ onClick={onClick}
191
+ className={cn(
192
+ 'rounded-full border px-3 py-1 text-xs font-medium transition-colors',
193
+ isActive
194
+ ? 'border-primary bg-primary text-primary-foreground'
195
+ : 'border-border bg-card text-muted-foreground hover:border-primary/40 hover:text-foreground',
196
+ )}
197
+ >
198
+ {label}
199
+ </button>
200
+ );
201
+ }
202
+
203
+ function SolutionCardCell({ solution }: { solution: SolutionCard }) {
204
+ const Icon$ = lucideIcon(solution.icon_name);
205
+ const href =
206
+ solution.cta_href ??
207
+ (solution.slug ? `/solutions/${solution.slug}` : null);
208
+
209
+ const card = (
210
+ <Card className="group flex h-full flex-col border-border/60 bg-card p-6 transition-colors hover:border-primary/40">
211
+ {Icon$ && (
212
+ <div className="mb-4 inline-flex size-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
213
+ <Icon iconNode={Icon$} className="size-5" />
214
+ </div>
215
+ )}
216
+ <h3 className="text-base font-semibold text-card-foreground">
217
+ {solution.title}
218
+ </h3>
219
+ {solution.description && (
220
+ <p className="mt-2 line-clamp-3 flex-1 text-sm leading-relaxed text-muted-foreground">
221
+ {solution.description}
222
+ </p>
223
+ )}
224
+ {href && (
225
+ <div className="mt-4 inline-flex items-center gap-1 text-sm font-medium text-primary group-hover:gap-2">
226
+ Explore
227
+ <ArrowRight className="size-3.5 transition-transform group-hover:translate-x-0.5" />
228
+ </div>
229
+ )}
230
+ </Card>
231
+ );
232
+
233
+ return href ? <PlatformLink href={href}>{card}</PlatformLink> : card;
234
+ }
@@ -0,0 +1,77 @@
1
+ import { cn } from '../../../lib/utils';
2
+
3
+ export type TextSectionContent = {
4
+ eyebrow?: string | null;
5
+ title?: string | null;
6
+ alignment?: 'left' | 'center' | null;
7
+ max_width?: 'narrow' | 'medium' | 'wide' | 'full' | null;
8
+ body_html?: string | null;
9
+ };
10
+
11
+ const MAX_WIDTH: Record<string, string> = {
12
+ narrow: 'max-w-2xl',
13
+ medium: 'max-w-3xl',
14
+ wide: 'max-w-5xl',
15
+ full: 'max-w-none',
16
+ };
17
+
18
+ /**
19
+ * Long-form prose block. Renders TipTap (Filament RichEditor) output as
20
+ * sanitised HTML. Use `prose` typography classes for readable paragraphs,
21
+ * headings, lists, blockquotes, code blocks — without per-element styling.
22
+ *
23
+ * SECURITY: body_html comes from authenticated /admin authors. Filament's
24
+ * RichEditor sanitises on save (its allowed tags/attributes are conservative
25
+ * by default). If we ever expose richtext to untrusted submitters, swap to
26
+ * a server-side sanitiser before render.
27
+ */
28
+ export function TextSection({
29
+ content,
30
+ identifier,
31
+ }: {
32
+ content?: TextSectionContent;
33
+ identifier?: string | null;
34
+ }) {
35
+ const alignment = content?.alignment ?? 'left';
36
+ const maxWidth = MAX_WIDTH[content?.max_width ?? 'narrow'];
37
+
38
+ return (
39
+ <section
40
+ id={identifier ?? undefined}
41
+ className="bg-background py-16 sm:py-20"
42
+ >
43
+ <div
44
+ className={cn(
45
+ 'mx-auto px-6',
46
+ maxWidth,
47
+ alignment === 'center' && 'text-center',
48
+ )}
49
+ >
50
+ {content?.eyebrow && (
51
+ <p className="mb-3 text-xs font-medium tracking-widest text-muted-foreground uppercase">
52
+ {content.eyebrow}
53
+ </p>
54
+ )}
55
+
56
+ {content?.title && (
57
+ <h2 className="mb-6 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
58
+ {content.title}
59
+ </h2>
60
+ )}
61
+
62
+ {content?.body_html && (
63
+ <div
64
+ className={cn(
65
+ 'prose max-w-none prose-neutral dark:prose-invert',
66
+ 'prose-headings:font-semibold prose-headings:tracking-tight',
67
+ 'prose-a:text-primary prose-a:no-underline hover:prose-a:underline',
68
+ 'prose-strong:text-foreground',
69
+ alignment === 'center' && 'mx-auto',
70
+ )}
71
+ dangerouslySetInnerHTML={{ __html: content.body_html }}
72
+ />
73
+ )}
74
+ </div>
75
+ </section>
76
+ );
77
+ }