@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.
- package/package.json +185 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/map/arrows/map-arrow-blue.png +0 -0
- package/src/assets/map/arrows/map-arrow-green.png +0 -0
- package/src/assets/map/arrows/map-arrow-purple.png +0 -0
- package/src/assets/map/arrows/map-arrow-red.png +0 -0
- package/src/assets/map/flags/flag-blue.png +0 -0
- package/src/assets/map/flags/flag-green.png +0 -0
- package/src/assets/map/flags/flag-red.png +0 -0
- package/src/assets/map/flags/flag-yellow.png +0 -0
- package/src/assets/map/pins/map-pin-blue.png +0 -0
- package/src/assets/map/pins/map-pin-green.png +0 -0
- package/src/assets/map/pins/map-pin-purple.png +0 -0
- package/src/assets/map/pins/map-pin-red.png +0 -0
- package/src/components/Card.tsx +9 -0
- package/src/components/alert-error.tsx +24 -0
- package/src/components/app-content.tsx +22 -0
- package/src/components/app-header.tsx +153 -0
- package/src/components/app-logo-icon.tsx +13 -0
- package/src/components/app-logo.tsx +21 -0
- package/src/components/app-shell.tsx +19 -0
- package/src/components/app-sidebar-header.tsx +68 -0
- package/src/components/app-sidebar.tsx +106 -0
- package/src/components/appearance-tabs.tsx +46 -0
- package/src/components/breadcrumbs.tsx +50 -0
- package/src/components/cms/blurred-image.tsx +111 -0
- package/src/components/cms/section-bg.tsx +473 -0
- package/src/components/cms/section-button.tsx +127 -0
- package/src/components/cms/sections/banner-5050-section.tsx +135 -0
- package/src/components/cms/sections/blogs-listing-section.tsx +270 -0
- package/src/components/cms/sections/cards-grid-section.tsx +185 -0
- package/src/components/cms/sections/contact-form-section.tsx +157 -0
- package/src/components/cms/sections/cta-section.tsx +101 -0
- package/src/components/cms/sections/featured-blog-slider-section.tsx +256 -0
- package/src/components/cms/sections/featured-products-grid-section.tsx +173 -0
- package/src/components/cms/sections/featured-solutions-grid-section.tsx +183 -0
- package/src/components/cms/sections/hero-section.tsx +180 -0
- package/src/components/cms/sections/solutions-with-filter-section.tsx +234 -0
- package/src/components/cms/sections/text-section.tsx +77 -0
- package/src/components/cutout-image.tsx +228 -0
- package/src/components/devices/devices-mini-map.tsx +275 -0
- package/src/components/docs/docs-shell.tsx +280 -0
- package/src/components/fleet-hero-animated.tsx +383 -0
- package/src/components/input-error.tsx +17 -0
- package/src/components/keenicons/assets/duotone/Read Me.txt +7 -0
- package/src/components/keenicons/assets/duotone/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/duotone/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/duotone/demo.html +12424 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.svg +1109 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.ttf +0 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.woff +0 -0
- package/src/components/keenicons/assets/duotone/selection.json +17313 -0
- package/src/components/keenicons/assets/duotone/style.css +4931 -0
- package/src/components/keenicons/assets/filled/Read Me.txt +7 -0
- package/src/components/keenicons/assets/filled/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/filled/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/filled/demo.html +12370 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.svg +1082 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.ttf +0 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.woff +0 -0
- package/src/components/keenicons/assets/filled/selection.json +17096 -0
- package/src/components/keenicons/assets/filled/style.css +4769 -0
- package/src/components/keenicons/assets/outline/Read Me.txt +7 -0
- package/src/components/keenicons/assets/outline/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/outline/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/outline/demo.html +11356 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.svg +575 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.ttf +0 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.woff +0 -0
- package/src/components/keenicons/assets/outline/selection.json +13054 -0
- package/src/components/keenicons/assets/outline/style.css +1721 -0
- package/src/components/keenicons/assets/solid/Read Me.txt +7 -0
- package/src/components/keenicons/assets/solid/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/solid/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/solid/demo.html +11356 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.svg +575 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.ttf +0 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.woff +0 -0
- package/src/components/keenicons/assets/solid/selection.json +13048 -0
- package/src/components/keenicons/assets/solid/style.css +1721 -0
- package/src/components/keenicons/assets/styles.css +4 -0
- package/src/components/keenicons/index.ts +2 -0
- package/src/components/keenicons/keenicons.tsx +16 -0
- package/src/components/keenicons/types.ts +7 -0
- package/src/components/nav-footer.tsx +49 -0
- package/src/components/nav-main.tsx +53 -0
- package/src/components/nav-user.tsx +59 -0
- package/src/components/notification-bell.tsx +190 -0
- package/src/components/products/product-card.tsx +159 -0
- package/src/components/text-link.tsx +23 -0
- package/src/components/ui/accordion-menu.tsx +322 -0
- package/src/components/ui/accordion.tsx +133 -0
- package/src/components/ui/alert-dialog.tsx +82 -0
- package/src/components/ui/alert.tsx +63 -0
- package/src/components/ui/avatar-group.tsx +129 -0
- package/src/components/ui/avatar.tsx +67 -0
- package/src/components/ui/badge.tsx +230 -0
- package/src/components/ui/breadcrumb.tsx +88 -0
- package/src/components/ui/button.tsx +412 -0
- package/src/components/ui/calendar.tsx +56 -0
- package/src/components/ui/card.tsx +147 -0
- package/src/components/ui/chart.tsx +290 -0
- package/src/components/ui/checkbox.tsx +47 -0
- package/src/components/ui/code.tsx +45 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/command-palette.tsx +189 -0
- package/src/components/ui/command.tsx +138 -0
- package/src/components/ui/cookie-banner.tsx +220 -0
- package/src/components/ui/copy-button.tsx +60 -0
- package/src/components/ui/data-grid-column-filter.tsx +124 -0
- package/src/components/ui/data-grid-column-header.tsx +284 -0
- package/src/components/ui/data-grid-column-visibility.tsx +38 -0
- package/src/components/ui/data-grid-pagination.tsx +206 -0
- package/src/components/ui/data-grid-table-dnd-rows.tsx +147 -0
- package/src/components/ui/data-grid-table-dnd.tsx +175 -0
- package/src/components/ui/data-grid-table.tsx +500 -0
- package/src/components/ui/data-grid.tsx +193 -0
- package/src/components/ui/data-list.tsx +76 -0
- package/src/components/ui/datefield.tsx +91 -0
- package/src/components/ui/dialog.tsx +139 -0
- package/src/components/ui/divider.tsx +41 -0
- package/src/components/ui/drawer.tsx +59 -0
- package/src/components/ui/dropdown-menu.tsx +224 -0
- package/src/components/ui/empty-state.tsx +54 -0
- package/src/components/ui/file-upload.tsx +152 -0
- package/src/components/ui/form.tsx +88 -0
- package/src/components/ui/icon.tsx +14 -0
- package/src/components/ui/input-otp.tsx +71 -0
- package/src/components/ui/input.tsx +155 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +31 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +37 -0
- package/src/components/ui/placeholder-pattern.tsx +21 -0
- package/src/components/ui/popover.tsx +50 -0
- package/src/components/ui/progress.tsx +65 -0
- package/src/components/ui/radio-group.tsx +73 -0
- package/src/components/ui/resizable.tsx +39 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +234 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +721 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/slider.tsx +35 -0
- package/src/components/ui/sonner.tsx +28 -0
- package/src/components/ui/sortable.tsx +724 -0
- package/src/components/ui/spinner.tsx +17 -0
- package/src/components/ui/stat-card.tsx +82 -0
- package/src/components/ui/stepper.tsx +410 -0
- package/src/components/ui/switch.tsx +68 -0
- package/src/components/ui/table.tsx +42 -0
- package/src/components/ui/tabs.tsx +196 -0
- package/src/components/ui/timeline.tsx +90 -0
- package/src/components/ui/toggle-group.tsx +73 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/user-info.tsx +33 -0
- package/src/components/user-menu-content.tsx +53 -0
- package/src/components/web/SiteFooter.tsx +154 -0
- package/src/components/web/SiteHeader.tsx +159 -0
- package/src/components/workflows/workflow-canvas.tsx +321 -0
- package/src/controls/Blockquote.tsx +25 -0
- package/src/controls/Button.tsx +101 -0
- package/src/controls/Checkbox.tsx +29 -0
- package/src/controls/DateField.tsx +37 -0
- package/src/controls/FormField.tsx +20 -0
- package/src/controls/Heading.tsx +28 -0
- package/src/controls/Input.tsx +21 -0
- package/src/controls/Label.tsx +18 -0
- package/src/controls/Paragraph.tsx +39 -0
- package/src/controls/PasswordInput.tsx +40 -0
- package/src/controls/RadioGroup.tsx +70 -0
- package/src/controls/Select.tsx +24 -0
- package/src/controls/Slider.tsx +33 -0
- package/src/controls/Switch.tsx +31 -0
- package/src/controls/Textarea.tsx +22 -0
- package/src/elements/ConfirmPasswordForm.tsx +43 -0
- package/src/elements/DeviceStatusBadge.tsx +38 -0
- package/src/elements/DriverCard.tsx +67 -0
- package/src/elements/ForgotPasswordForm.tsx +64 -0
- package/src/elements/IncidentCard.tsx +67 -0
- package/src/elements/LoginForm.tsx +100 -0
- package/src/elements/OtpForm.tsx +71 -0
- package/src/elements/RegisterForm.tsx +150 -0
- package/src/elements/ResetPasswordForm.tsx +72 -0
- package/src/elements/SmsChallengeForm.tsx +104 -0
- package/src/elements/VehicleCard.tsx +73 -0
- package/src/elements/VerifyEmailForm.tsx +39 -0
- package/src/hooks/use-appearance.tsx +117 -0
- package/src/hooks/use-applied-theme.ts +98 -0
- package/src/hooks/use-clipboard.ts +34 -0
- package/src/hooks/use-current-url.ts +83 -0
- package/src/hooks/use-dark-mode.ts +48 -0
- package/src/hooks/use-flash-toast.ts +29 -0
- package/src/hooks/use-initials.tsx +24 -0
- package/src/hooks/use-mobile-navigation.ts +12 -0
- package/src/hooks/use-mobile.tsx +38 -0
- package/src/index.ts +408 -0
- package/src/layouts/AppLayout.tsx +60 -0
- package/src/layouts/AuthLayout.tsx +32 -0
- package/src/layouts/SettingsLayout.tsx +21 -0
- package/src/layouts/app/AIChatLayout.tsx +73 -0
- package/src/layouts/app/AsideSidebarLayout.tsx +3 -0
- package/src/layouts/app/CalendarSidebarLayout.tsx +69 -0
- package/src/layouts/app/CommunitiesNavbarLayout.tsx +3 -0
- package/src/layouts/app/DualNavbarSidebarLayout.tsx +3 -0
- package/src/layouts/app/FocusSidebarLayout.tsx +75 -0
- package/src/layouts/app/MailLayout.tsx +69 -0
- package/src/layouts/app/MegaMenuHeaderLayout.tsx +3 -0
- package/src/layouts/app/MegaMenuLayout.tsx +81 -0
- package/src/layouts/app/MegaMenuNavbarLayout.tsx +88 -0
- package/src/layouts/app/MegaMenuSearchNavbarLayout.tsx +3 -0
- package/src/layouts/app/NavbarCollapsibleLayout.tsx +88 -0
- package/src/layouts/app/NavbarCollapsibleLinksLayout.tsx +3 -0
- package/src/layouts/app/NavbarMinimalLayout.tsx +3 -0
- package/src/layouts/app/NavbarMinimalSidebarLayout.tsx +3 -0
- package/src/layouts/app/NavbarSidebarDashboardLayout.tsx +3 -0
- package/src/layouts/app/NavbarSidebarLayout.tsx +92 -0
- package/src/layouts/app/NavbarSimpleSidebarLayout.tsx +3 -0
- package/src/layouts/app/NavbarTitledSidebarLayout.tsx +3 -0
- package/src/layouts/app/PanelSidebarLayout.tsx +3 -0
- package/src/layouts/app/SearchNavbarSidebarLayout.tsx +3 -0
- package/src/layouts/app/SidebarBreadcrumbLayout.tsx +3 -0
- package/src/layouts/app/SidebarCleanLayout.tsx +3 -0
- package/src/layouts/app/SidebarCommunitiesLayout.tsx +3 -0
- package/src/layouts/app/SidebarContentLayout.tsx +3 -0
- package/src/layouts/app/SidebarDualMenuLayout.tsx +104 -0
- package/src/layouts/app/SidebarFixedLayout.tsx +166 -0
- package/src/layouts/app/SidebarFooterNavbarLayout.tsx +3 -0
- package/src/layouts/app/SidebarHeaderMenuLayout.tsx +3 -0
- package/src/layouts/app/SidebarMegaMenuLayout.tsx +4 -0
- package/src/layouts/app/SidebarMinimalLayout.tsx +70 -0
- package/src/layouts/app/SidebarMobileSearchLayout.tsx +3 -0
- package/src/layouts/app/SidebarMultiPanelLayout.tsx +3 -0
- package/src/layouts/app/SidebarPrimarySecondaryLayout.tsx +3 -0
- package/src/layouts/app/SidebarSearchHeaderLayout.tsx +103 -0
- package/src/layouts/app/SidebarSearchToolbarLayout.tsx +3 -0
- package/src/layouts/app/SidebarTabsDualLayout.tsx +3 -0
- package/src/layouts/app/SidebarTabsLayout.tsx +98 -0
- package/src/layouts/app/SidebarTreeLayout.tsx +3 -0
- package/src/layouts/app/SplitNavbarLayout.tsx +3 -0
- package/src/layouts/app/SplitSidebarDashboardLayout.tsx +3 -0
- package/src/layouts/app/SplitSidebarLayout.tsx +99 -0
- package/src/layouts/app/TopNavLayout.tsx +105 -0
- package/src/layouts/app/TopNavLinksLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceBreadcrumbLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceCommunitiesLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceNavbarLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceSidebarLayout.tsx +98 -0
- package/src/layouts/app/WorkspaceSidebarTitleLayout.tsx +3 -0
- package/src/layouts/app/app-header-layout.tsx +45 -0
- package/src/layouts/app/app-sidebar-layout.tsx +56 -0
- package/src/layouts/app/layout-context.tsx +44 -0
- package/src/layouts/app/layout-types.ts +47 -0
- package/src/layouts/app/partials/Footer.tsx +35 -0
- package/src/layouts/app/partials/HeaderTopbar.tsx +96 -0
- package/src/layouts/app/partials/Navbar.tsx +85 -0
- package/src/layouts/app/partials/Toolbar.tsx +47 -0
- package/src/layouts/app-layout.tsx +29 -0
- package/src/layouts/auth/AuthBrandedLayout.tsx +58 -0
- package/src/layouts/auth/AuthCardLayout.tsx +31 -0
- package/src/layouts/auth/AuthCenteredLayout.tsx +41 -0
- package/src/layouts/auth/AuthClassicLayout.tsx +41 -0
- package/src/layouts/auth/AuthSimpleLayout.tsx +33 -0
- package/src/layouts/auth/AuthSplitLayout.tsx +89 -0
- package/src/layouts/web-app-layout.tsx +162 -0
- package/src/layouts/web-layout.tsx +23 -0
- package/src/lib/datetime.ts +188 -0
- package/src/lib/google-maps-loader.ts +99 -0
- package/src/lib/location.ts +127 -0
- package/src/lib/lucide-icon-map.ts +132 -0
- package/src/lib/map-markers.ts +124 -0
- package/src/lib/map-styles.ts +351 -0
- package/src/lib/utils.ts +11 -0
- package/src/platform/adapters/default.tsx +156 -0
- package/src/platform/adapters/inertia.tsx +88 -0
- package/src/platform/adapters/nextjs.ts +86 -0
- package/src/platform/context.tsx +106 -0
- package/src/platform/index.ts +27 -0
- package/src/platform/types.ts +105 -0
- package/src/styles/layouts/sidebar-fixed.css +161 -0
- package/src/styles/themes.css +583 -0
- package/src/types/assets.d.ts +5 -0
- package/src/types/auth.ts +25 -0
- package/src/types/global.d.ts +13 -0
- package/src/types/index.ts +9 -0
- package/src/types/navigation.ts +15 -0
- 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
|
+
}
|