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