From 19de2ac41155e6b6c233f650fe987e212dfb16d7 Mon Sep 17 00:00:00 2001 From: albertgwo Date: Mon, 9 Mar 2026 22:47:03 -0400 Subject: [PATCH 01/16] feat(homepage): add marketing site for flowprint.so Standalone Next.js 16 marketing homepage at packages/homepage/, designed for separate deployment to Cloudflare Pages (flowprint.so). Three-tab layout (Overview, Capabilities, Try It) showcasing the Flowprint Engine as the bridge between business flow design and developer integration. Key components: - Overview tab: 3D parallax bridge with business/developer state cycle - Capabilities tab: 3 bridge cards (Decision Logic, Simulation, Version Control) with sticky Business/Developer perspective toggle - Try It tab: template picker with live SVG topology previews for E-Commerce, Patient Intake, and Insurance Claims blueprints Tech stack: Next.js 16, React 19, Tailwind v4, Radix Tabs, CSS custom properties, vanilla JS animations via useEffect/useRef. --- packages/homepage/.gitignore | 27 + packages/homepage/app/globals.css | 235 ++ packages/homepage/app/layout.tsx | 62 + packages/homepage/app/page.tsx | 15 + packages/homepage/components.json | 21 + .../components/bridges/bridge-decision.css | 548 +++ .../components/bridges/bridge-decision.tsx | 538 +++ .../components/bridges/bridge-simulation.css | 799 +++++ .../components/bridges/bridge-simulation.tsx | 1331 +++++++ .../components/bridges/bridge-versioning.css | 380 ++ .../components/bridges/bridge-versioning.tsx | 406 +++ .../components/bridges/overview-parallax.css | 684 ++++ .../components/bridges/overview-parallax.tsx | 541 +++ .../homepage/components/flow/flow-defs.tsx | 15 + .../homepage/components/flow/flow-node.tsx | 37 + packages/homepage/components/flow/flow.css | 78 + .../components/flow/topology-preview.tsx | 270 ++ .../homepage/components/hero-background.tsx | 173 + .../sections/capabilities-section.css | 88 + .../sections/capabilities-section.tsx | 51 + .../homepage/components/sections/footer.tsx | 138 + .../components/sections/getting-started.tsx | 131 + .../homepage/components/sections/header.tsx | 110 + .../homepage/components/sections/hero.tsx | 100 + .../components/sections/homepage-tabs.tsx | 36 + .../components/sections/how-it-works.tsx | 282 ++ .../components/sections/integrations.tsx | 63 + .../components/sections/overview-section.tsx | 9 + .../components/sections/templates.tsx | 177 + .../components/sections/tryit-section.css | 365 ++ .../components/sections/tryit-section.tsx | 261 ++ .../sections/value-propositions.tsx | 189 + .../homepage/components/theme-provider.tsx | 11 + packages/homepage/components/ui/accordion.tsx | 66 + .../homepage/components/ui/alert-dialog.tsx | 157 + packages/homepage/components/ui/alert.tsx | 66 + .../homepage/components/ui/aspect-ratio.tsx | 11 + packages/homepage/components/ui/avatar.tsx | 53 + packages/homepage/components/ui/badge.tsx | 46 + .../homepage/components/ui/breadcrumb.tsx | 109 + .../homepage/components/ui/button-group.tsx | 83 + packages/homepage/components/ui/button.tsx | 60 + packages/homepage/components/ui/calendar.tsx | 213 ++ packages/homepage/components/ui/card.tsx | 92 + packages/homepage/components/ui/carousel.tsx | 241 ++ packages/homepage/components/ui/chart.tsx | 353 ++ packages/homepage/components/ui/checkbox.tsx | 32 + .../homepage/components/ui/collapsible.tsx | 33 + packages/homepage/components/ui/command.tsx | 184 + .../homepage/components/ui/context-menu.tsx | 252 ++ packages/homepage/components/ui/dialog.tsx | 143 + packages/homepage/components/ui/drawer.tsx | 135 + .../homepage/components/ui/dropdown-menu.tsx | 257 ++ packages/homepage/components/ui/empty.tsx | 104 + packages/homepage/components/ui/field.tsx | 244 ++ packages/homepage/components/ui/form.tsx | 167 + .../homepage/components/ui/hover-card.tsx | 44 + .../homepage/components/ui/input-group.tsx | 169 + packages/homepage/components/ui/input-otp.tsx | 77 + packages/homepage/components/ui/input.tsx | 21 + packages/homepage/components/ui/item.tsx | 193 + packages/homepage/components/ui/kbd.tsx | 28 + packages/homepage/components/ui/label.tsx | 24 + packages/homepage/components/ui/menubar.tsx | 276 ++ .../components/ui/navigation-menu.tsx | 166 + .../homepage/components/ui/pagination.tsx | 127 + packages/homepage/components/ui/popover.tsx | 48 + packages/homepage/components/ui/progress.tsx | 31 + .../homepage/components/ui/radio-group.tsx | 45 + packages/homepage/components/ui/resizable.tsx | 56 + .../homepage/components/ui/scroll-area.tsx | 58 + packages/homepage/components/ui/select.tsx | 185 + packages/homepage/components/ui/separator.tsx | 28 + packages/homepage/components/ui/sheet.tsx | 139 + packages/homepage/components/ui/sidebar.tsx | 726 ++++ packages/homepage/components/ui/skeleton.tsx | 13 + packages/homepage/components/ui/slider.tsx | 63 + packages/homepage/components/ui/sonner.tsx | 25 + packages/homepage/components/ui/spinner.tsx | 16 + packages/homepage/components/ui/switch.tsx | 31 + packages/homepage/components/ui/table.tsx | 116 + packages/homepage/components/ui/tabs.tsx | 66 + packages/homepage/components/ui/textarea.tsx | 18 + packages/homepage/components/ui/toast.tsx | 129 + packages/homepage/components/ui/toaster.tsx | 35 + .../homepage/components/ui/toggle-group.tsx | 73 + packages/homepage/components/ui/toggle.tsx | 47 + packages/homepage/components/ui/tooltip.tsx | 61 + .../homepage/components/ui/use-mobile.tsx | 19 + packages/homepage/components/ui/use-toast.ts | 191 + packages/homepage/hooks/use-mobile.ts | 19 + packages/homepage/hooks/use-toast.ts | 191 + packages/homepage/lib/utils.ts | 6 + packages/homepage/next.config.mjs | 11 + packages/homepage/package.json | 72 + packages/homepage/pnpm-lock.yaml | 3180 +++++++++++++++++ packages/homepage/postcss.config.mjs | 8 + packages/homepage/styles/globals.css | 125 + packages/homepage/tsconfig.json | 41 + 99 files changed, 18269 insertions(+) create mode 100644 packages/homepage/.gitignore create mode 100644 packages/homepage/app/globals.css create mode 100644 packages/homepage/app/layout.tsx create mode 100644 packages/homepage/app/page.tsx create mode 100644 packages/homepage/components.json create mode 100644 packages/homepage/components/bridges/bridge-decision.css create mode 100644 packages/homepage/components/bridges/bridge-decision.tsx create mode 100644 packages/homepage/components/bridges/bridge-simulation.css create mode 100644 packages/homepage/components/bridges/bridge-simulation.tsx create mode 100644 packages/homepage/components/bridges/bridge-versioning.css create mode 100644 packages/homepage/components/bridges/bridge-versioning.tsx create mode 100644 packages/homepage/components/bridges/overview-parallax.css create mode 100644 packages/homepage/components/bridges/overview-parallax.tsx create mode 100644 packages/homepage/components/flow/flow-defs.tsx create mode 100644 packages/homepage/components/flow/flow-node.tsx create mode 100644 packages/homepage/components/flow/flow.css create mode 100644 packages/homepage/components/flow/topology-preview.tsx create mode 100644 packages/homepage/components/hero-background.tsx create mode 100644 packages/homepage/components/sections/capabilities-section.css create mode 100644 packages/homepage/components/sections/capabilities-section.tsx create mode 100644 packages/homepage/components/sections/footer.tsx create mode 100644 packages/homepage/components/sections/getting-started.tsx create mode 100644 packages/homepage/components/sections/header.tsx create mode 100644 packages/homepage/components/sections/hero.tsx create mode 100644 packages/homepage/components/sections/homepage-tabs.tsx create mode 100644 packages/homepage/components/sections/how-it-works.tsx create mode 100644 packages/homepage/components/sections/integrations.tsx create mode 100644 packages/homepage/components/sections/overview-section.tsx create mode 100644 packages/homepage/components/sections/templates.tsx create mode 100644 packages/homepage/components/sections/tryit-section.css create mode 100644 packages/homepage/components/sections/tryit-section.tsx create mode 100644 packages/homepage/components/sections/value-propositions.tsx create mode 100644 packages/homepage/components/theme-provider.tsx create mode 100644 packages/homepage/components/ui/accordion.tsx create mode 100644 packages/homepage/components/ui/alert-dialog.tsx create mode 100644 packages/homepage/components/ui/alert.tsx create mode 100644 packages/homepage/components/ui/aspect-ratio.tsx create mode 100644 packages/homepage/components/ui/avatar.tsx create mode 100644 packages/homepage/components/ui/badge.tsx create mode 100644 packages/homepage/components/ui/breadcrumb.tsx create mode 100644 packages/homepage/components/ui/button-group.tsx create mode 100644 packages/homepage/components/ui/button.tsx create mode 100644 packages/homepage/components/ui/calendar.tsx create mode 100644 packages/homepage/components/ui/card.tsx create mode 100644 packages/homepage/components/ui/carousel.tsx create mode 100644 packages/homepage/components/ui/chart.tsx create mode 100644 packages/homepage/components/ui/checkbox.tsx create mode 100644 packages/homepage/components/ui/collapsible.tsx create mode 100644 packages/homepage/components/ui/command.tsx create mode 100644 packages/homepage/components/ui/context-menu.tsx create mode 100644 packages/homepage/components/ui/dialog.tsx create mode 100644 packages/homepage/components/ui/drawer.tsx create mode 100644 packages/homepage/components/ui/dropdown-menu.tsx create mode 100644 packages/homepage/components/ui/empty.tsx create mode 100644 packages/homepage/components/ui/field.tsx create mode 100644 packages/homepage/components/ui/form.tsx create mode 100644 packages/homepage/components/ui/hover-card.tsx create mode 100644 packages/homepage/components/ui/input-group.tsx create mode 100644 packages/homepage/components/ui/input-otp.tsx create mode 100644 packages/homepage/components/ui/input.tsx create mode 100644 packages/homepage/components/ui/item.tsx create mode 100644 packages/homepage/components/ui/kbd.tsx create mode 100644 packages/homepage/components/ui/label.tsx create mode 100644 packages/homepage/components/ui/menubar.tsx create mode 100644 packages/homepage/components/ui/navigation-menu.tsx create mode 100644 packages/homepage/components/ui/pagination.tsx create mode 100644 packages/homepage/components/ui/popover.tsx create mode 100644 packages/homepage/components/ui/progress.tsx create mode 100644 packages/homepage/components/ui/radio-group.tsx create mode 100644 packages/homepage/components/ui/resizable.tsx create mode 100644 packages/homepage/components/ui/scroll-area.tsx create mode 100644 packages/homepage/components/ui/select.tsx create mode 100644 packages/homepage/components/ui/separator.tsx create mode 100644 packages/homepage/components/ui/sheet.tsx create mode 100644 packages/homepage/components/ui/sidebar.tsx create mode 100644 packages/homepage/components/ui/skeleton.tsx create mode 100644 packages/homepage/components/ui/slider.tsx create mode 100644 packages/homepage/components/ui/sonner.tsx create mode 100644 packages/homepage/components/ui/spinner.tsx create mode 100644 packages/homepage/components/ui/switch.tsx create mode 100644 packages/homepage/components/ui/table.tsx create mode 100644 packages/homepage/components/ui/tabs.tsx create mode 100644 packages/homepage/components/ui/textarea.tsx create mode 100644 packages/homepage/components/ui/toast.tsx create mode 100644 packages/homepage/components/ui/toaster.tsx create mode 100644 packages/homepage/components/ui/toggle-group.tsx create mode 100644 packages/homepage/components/ui/toggle.tsx create mode 100644 packages/homepage/components/ui/tooltip.tsx create mode 100644 packages/homepage/components/ui/use-mobile.tsx create mode 100644 packages/homepage/components/ui/use-toast.ts create mode 100644 packages/homepage/hooks/use-mobile.ts create mode 100644 packages/homepage/hooks/use-toast.ts create mode 100644 packages/homepage/lib/utils.ts create mode 100644 packages/homepage/next.config.mjs create mode 100644 packages/homepage/package.json create mode 100644 packages/homepage/pnpm-lock.yaml create mode 100644 packages/homepage/postcss.config.mjs create mode 100644 packages/homepage/styles/globals.css create mode 100644 packages/homepage/tsconfig.json diff --git a/packages/homepage/.gitignore b/packages/homepage/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/packages/homepage/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/packages/homepage/app/globals.css b/packages/homepage/app/globals.css new file mode 100644 index 0000000..3dcd3b1 --- /dev/null +++ b/packages/homepage/app/globals.css @@ -0,0 +1,235 @@ +@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap'); +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +/* Flowprint Theme - Premium Enterprise with Technical-Creative Hybrid Energy */ +/* Primary: #110904 (Earthly Mocha) | Accent: #E446FF (Electric Magenta) */ + +:root { + /* Dark theme by default - matching product canvas aesthetic */ + --background: #110904; + --foreground: #fafafa; + --card: #1a0f0a; + --card-foreground: #fafafa; + --popover: #1a0f0a; + --popover-foreground: #fafafa; + --primary: #E446FF; + --primary-foreground: #110904; + --secondary: #2a1a14; + --secondary-foreground: #fafafa; + --muted: #2a1a14; + --muted-foreground: #a8a29e; + --accent: #E446FF; + --accent-foreground: #110904; + --destructive: #ef4444; + --destructive-foreground: #fafafa; + --border: #3d2a22; + --input: #3d2a22; + --ring: #E446FF; + --chart-1: #E446FF; + --chart-2: #8b5cf6; + --chart-3: #06b6d4; + --chart-4: #f59e0b; + --chart-5: #10b981; + --radius: 0.5rem; + --sidebar: #1a0f0a; + --sidebar-foreground: #fafafa; + --sidebar-primary: #E446FF; + --sidebar-primary-foreground: #110904; + --sidebar-accent: #2a1a14; + --sidebar-accent-foreground: #fafafa; + --sidebar-border: #3d2a22; + --sidebar-ring: #E446FF; + --accent-dim: rgba(228, 70, 255, 0.35); + --accent-glow: rgba(228, 70, 255, 0.15); + --surface-border: rgba(228, 70, 255, 0.12); + --text-muted: #6b5a4d; + --code-bg: #0d0805; + --table-header: #2a1a12; + --table-row-odd: rgba(26, 15, 10, 0.8); + --table-row-even: rgba(34, 21, 16, 0.6); + --true-green: #5ae07a; + --false-red: #e05a5a; + --keyword-blue: #7aafff; + --string-amber: #ffb86c; + --type-teal: #5ae0c2; + --comment-muted: #5a4a3d; + --node-fill: #2a1a14; + --node-stroke: #3d2a22; + --lane-customer: #FF6B6B; + --lane-routing: #E446FF; + --lane-fulfillment: #06B6D4; + --node-type-action: #FF9243; + --node-type-switch: #A374FF; + --node-type-terminal: #3FDC77; + --error-red: #FF362B; + --visited-color: rgba(180, 190, 254, 0.2); + --green-glow: rgba(63, 220, 119, 0.5); + --amber-glow: rgba(255, 146, 67, 0.5); + --red-glow: rgba(255, 107, 107, 0.5); + --edge-color: #6b4d40; + --surface: #1a0f0a; + --surface-elevated: #221510; + --text-primary: #f0e6df; + --text-secondary: #a89585; +} + +.dark { + --background: #110904; + --foreground: #fafafa; + --card: #1a0f0a; + --card-foreground: #fafafa; + --popover: #1a0f0a; + --popover-foreground: #fafafa; + --primary: #E446FF; + --primary-foreground: #110904; + --secondary: #2a1a14; + --secondary-foreground: #fafafa; + --muted: #2a1a14; + --muted-foreground: #a8a29e; + --accent: #E446FF; + --accent-foreground: #110904; + --destructive: #ef4444; + --destructive-foreground: #fafafa; + --border: #3d2a22; + --input: #3d2a22; + --ring: #E446FF; + --chart-1: #E446FF; + --chart-2: #8b5cf6; + --chart-3: #06b6d4; + --chart-4: #f59e0b; + --chart-5: #10b981; + --sidebar: #1a0f0a; + --sidebar-foreground: #fafafa; + --sidebar-primary: #E446FF; + --sidebar-primary-foreground: #110904; + --sidebar-accent: #2a1a14; + --sidebar-accent-foreground: #fafafa; + --sidebar-border: #3d2a22; + --sidebar-ring: #E446FF; +} + +@theme inline { + --font-sans: 'DM Sans', system-ui, sans-serif; + --font-serif: 'Instrument Serif', Georgia, serif; + --font-mono: 'JetBrains Mono', monospace; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +/* Custom animations for Flowprint */ +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 0.8; } +} + +@keyframes flow-line { + 0% { stroke-dashoffset: 100; } + 100% { stroke-dashoffset: 0; } +} + +@keyframes fade-in-up { + 0% { opacity: 0; transform: translateY(20px); } + 100% { opacity: 1; transform: translateY(0); } +} + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +.animate-pulse-glow { + animation: pulse-glow 3s ease-in-out infinite; +} + +.animate-flow-line { + animation: flow-line 2s ease-out forwards; + stroke-dasharray: 100; + stroke-dashoffset: 100; +} + +.animate-fade-in-up { + animation: fade-in-up 0.6s ease-out forwards; +} + +/* Gradient text utility */ +.gradient-text { + background: linear-gradient(135deg, #E446FF 0%, #ff6b9d 50%, #E446FF 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Code block styling */ +.code-block { + background: #1a0f0a; + border: 1px solid #3d2a22; + border-radius: 0.5rem; + font-family: 'JetBrains Mono', monospace; +} + +/* Homepage tabs */ +.homepage-tabs { width: 100%; max-width: 1100px; margin: 0 auto; } +.tab-list { display: flex; justify-content: center; gap: 4px; padding: 4px; background: rgba(26, 15, 10, 0.6); border: 1px solid rgba(228, 70, 255, 0.12); border-radius: 12px; width: fit-content; margin: 0 auto 48px; } +.tab-trigger { padding: 10px 28px; font-family: var(--font-sans); font-size: 14px; font-weight: 500; color: #6b5a4d; background: transparent; border: none; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; } +.tab-trigger[data-state="active"] { color: #f0e6df; background: rgba(228, 70, 255, 0.12); box-shadow: 0 0 12px rgba(228, 70, 255, 0.15); } +.tab-trigger:hover:not([data-state="active"]) { color: #a89585; } +.tab-content { outline: none; } + +/* Overview section — centers the parallax card within the tab panel */ +.overview-section { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + min-height: 720px; +} diff --git a/packages/homepage/app/layout.tsx b/packages/homepage/app/layout.tsx new file mode 100644 index 0000000..3dadf0e --- /dev/null +++ b/packages/homepage/app/layout.tsx @@ -0,0 +1,62 @@ +import type { Metadata, Viewport } from 'next' +import { DM_Sans, JetBrains_Mono } from 'next/font/google' +import { Analytics } from '@vercel/analytics/next' +import './globals.css' + +const dmSans = DM_Sans({ + subsets: ['latin'], + weight: ['400', '500', '600'], + variable: '--font-dm-sans', +}) + +const jetbrainsMono = JetBrains_Mono({ + subsets: ['latin'], + weight: ['400', '500'], + variable: '--font-jetbrains', +}) + +export const metadata: Metadata = { + title: 'Flowprint - Executable Service Blueprints', + description: 'Visual blueprint editor that lets business operatives create executable service blueprints. Design flows, define rules, simulate outcomes, and generate production-ready code.', + generator: 'Flowprint', + keywords: ['service blueprints', 'workflow automation', 'code generation', 'Temporal', 'business process', 'visual editor'], + authors: [{ name: 'Flowprint' }], + icons: { + icon: [ + { + url: '/icon-light-32x32.png', + media: '(prefers-color-scheme: light)', + }, + { + url: '/icon-dark-32x32.png', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/icon.svg', + type: 'image/svg+xml', + }, + ], + apple: '/apple-icon.png', + }, +} + +export const viewport: Viewport = { + themeColor: '#110904', + width: 'device-width', + initialScale: 1, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/packages/homepage/app/page.tsx b/packages/homepage/app/page.tsx new file mode 100644 index 0000000..e4fa48f --- /dev/null +++ b/packages/homepage/app/page.tsx @@ -0,0 +1,15 @@ +import { Header } from "@/components/sections/header" +import { HeroSection } from "@/components/sections/hero" +import { HomepageTabs } from "@/components/sections/homepage-tabs" +import { FooterSection } from "@/components/sections/footer" + +export default function HomePage() { + return ( +
+
+ + + +
+ ) +} diff --git a/packages/homepage/components.json b/packages/homepage/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/packages/homepage/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/packages/homepage/components/bridges/bridge-decision.css b/packages/homepage/components/bridges/bridge-decision.css new file mode 100644 index 0000000..0109197 --- /dev/null +++ b/packages/homepage/components/bridges/bridge-decision.css @@ -0,0 +1,548 @@ +/* ══════════════════════════════════════════════════════ + Bridge Decision — ported 1:1 from prototype-bridge-decision.html + ══════════════════════════════════════════════════════ */ + +/* ══════════════════════════════════════════════════════ + CARD + ══════════════════════════════════════════════════════ */ +.bridge-decision .card { + position: relative; + width: 780px; + border-radius: 20px; + background: var(--surface); + border: 1px solid var(--surface-border); + box-shadow: + 0 4px 24px rgba(0, 0, 0, 0.5), + 0 0 80px var(--accent-glow), + inset 0 1px 0 rgba(255, 255, 255, 0.04); + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* ══════════════════════════════════════════════════════ + HEADER + ══════════════════════════════════════════════════════ */ +.bridge-decision .header { + padding: 28px 32px 0; +} + +.bridge-decision .badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 100px; + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + margin-bottom: 12px; + transition: all 0.4s ease; +} + +.bridge-decision .badge--business { + background: rgba(163, 116, 255, 0.12); + color: var(--node-type-switch); + border: 1px solid rgba(163, 116, 255, 0.2); +} + +.bridge-decision .badge--developer { + background: rgba(228, 70, 255, 0.12); + color: var(--accent); + border: 1px solid rgba(228, 70, 255, 0.2); +} + +.bridge-decision .badge-dot { + width: 6px; + height: 6px; + border-radius: 50%; + animation: bridge-decision-pulse-dot 2s ease-in-out infinite; +} + +.bridge-decision .badge--business .badge-dot { + background: var(--node-type-switch); +} + +.bridge-decision .badge--developer .badge-dot { + background: var(--accent); +} + +@keyframes bridge-decision-pulse-dot { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.bridge-decision .title { + font-family: 'Instrument Serif', serif; + font-size: 32px; + font-weight: 400; + color: var(--text-primary); + margin-bottom: 6px; + line-height: 1.15; +} + +.bridge-decision .description { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.5; + max-width: 520px; +} + +/* ══════════════════════════════════════════════════════ + VIEWS CONTAINER + ══════════════════════════════════════════════════════ */ +.bridge-decision .views { + position: relative; + overflow: hidden; + margin-top: 20px; + transition: height 0.5s cubic-bezier(0.23, 1, 0.32, 1); +} + +.bridge-decision .view { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 0 32px 28px; + transition: opacity 0.5s ease, transform 0.5s ease; +} + +.bridge-decision .view--business { + opacity: 1; + transform: translateY(0); +} + +.bridge-decision .view--business.hidden { + opacity: 0; + transform: translateY(8px); + pointer-events: none; +} + +.bridge-decision .view--developer { + opacity: 0; + transform: translateY(-8px); + pointer-events: none; +} + +.bridge-decision .view--developer.visible { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +/* ══════════════════════════════════════════════════════ + BUSINESS VIEW — Decision Table Editor + ══════════════════════════════════════════════════════ */ +.bridge-decision .table-toolbar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.bridge-decision .hit-policy { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: var(--surface-elevated); + border: 1px solid rgba(163, 116, 255, 0.15); + border-radius: 8px; +} + +.bridge-decision .hit-policy-label { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.bridge-decision .hit-policy-select { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + font-weight: 500; + color: var(--node-type-switch); + background: transparent; + border: none; + cursor: default; +} + +.bridge-decision .hit-policy-options { + display: flex; + gap: 2px; +} + +.bridge-decision .hit-option { + padding: 3px 8px; + border-radius: 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s; + border: 1px solid transparent; +} + +.bridge-decision .hit-option:hover { + color: var(--text-secondary); + background: rgba(163, 116, 255, 0.06); +} + +.bridge-decision .hit-option.active { + color: var(--node-type-switch); + background: rgba(163, 116, 255, 0.1); + border-color: rgba(163, 116, 255, 0.2); +} + +.bridge-decision .table-editor { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 12px; + overflow: hidden; + border: 1px solid rgba(163, 116, 255, 0.1); +} + +.bridge-decision .table-editor th { + padding: 10px 16px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--node-type-switch); + background: rgba(163, 116, 255, 0.08); + text-align: left; + border-bottom: 1px solid rgba(163, 116, 255, 0.12); +} + +.bridge-decision .table-editor th.col-input { + color: var(--node-type-action); + background: rgba(255, 146, 67, 0.06); +} + +.bridge-decision .table-editor th.col-output { + color: var(--node-type-terminal); + background: rgba(63, 220, 119, 0.06); +} + +.bridge-decision .table-editor th .col-type { + display: block; + font-size: 9px; + color: var(--text-muted); + font-weight: 400; + margin-top: 2px; + letter-spacing: 0.06em; +} + +.bridge-decision .table-editor td { + padding: 10px 16px; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + color: var(--text-primary); + border-bottom: 1px solid rgba(255, 255, 255, 0.04); + border-right: 1px solid rgba(255, 255, 255, 0.03); +} + +.bridge-decision .table-editor td:last-child { + border-right: none; +} + +.bridge-decision .table-editor tr:last-child td { + border-bottom: none; +} + +.bridge-decision .table-editor tbody tr { + background: var(--surface); + transition: background 0.2s; +} + +.bridge-decision .table-editor tbody tr:nth-child(odd) { + background: rgba(26, 15, 10, 0.8); +} + +.bridge-decision .table-editor tbody tr:nth-child(even) { + background: rgba(34, 21, 16, 0.6); +} + +.bridge-decision .table-editor tbody tr:hover { + background: rgba(163, 116, 255, 0.06); +} + +.bridge-decision .table-editor .row-num { + width: 36px; + text-align: center; + font-size: 10px; + color: var(--text-muted); +} + +.bridge-decision .cell-value { + padding: 2px 6px; + border-radius: 4px; +} + +.bridge-decision .cell-value--input { + color: var(--node-type-action); +} + +.bridge-decision .cell-value--output { + color: var(--node-type-terminal); +} + +.bridge-decision .cell-value--any { + color: var(--text-muted); + font-style: italic; +} + +.bridge-decision .table-note { + display: flex; + align-items: center; + gap: 8px; + margin-top: 14px; + font-size: 12px; + color: var(--text-muted); +} + +.bridge-decision .table-note-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 4px; + background: rgba(163, 116, 255, 0.08); + color: var(--node-type-switch); + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 500; +} + +/* ══════════════════════════════════════════════════════ + DEVELOPER VIEW — Horizontal Data Flow + ══════════════════════════════════════════════════════ */ +.bridge-decision .flow-container { + display: flex; + align-items: center; + justify-content: center; + gap: 0; + min-height: 200px; + position: relative; +} + +.bridge-decision .flow-json { + flex: 0 0 180px; + background: var(--code-bg); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 10px; + padding: 14px 16px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + line-height: 1.6; + position: relative; + z-index: 2; + transition: border-color 0.4s, box-shadow 0.4s; +} + +.bridge-decision .flow-json--input { + opacity: 0; + transform: translateX(-20px); +} + +.bridge-decision .flow-json--input.animate-in { + opacity: 1; + transform: translateX(0); + transition: opacity 0.5s ease, transform 0.5s ease; +} + +.bridge-decision .flow-json--output { + opacity: 0; + transform: translateX(20px); +} + +.bridge-decision .flow-json--output.animate-in { + opacity: 1; + transform: translateX(0); + transition: opacity 0.5s ease, transform 0.5s ease; +} + +.bridge-decision .flow-json--active { + border-color: rgba(228, 70, 255, 0.3); + box-shadow: 0 0 20px rgba(228, 70, 255, 0.1); +} + +.bridge-decision .json-brace { + color: var(--text-muted); +} + +.bridge-decision .json-key { + color: var(--node-type-action); +} + +.bridge-decision .json-string { + color: #ffb86c; +} + +.bridge-decision .json-number { + color: #5ae0c2; +} + +/* Flow arrows */ +.bridge-decision .flow-arrow { + flex: 0 0 40px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + z-index: 1; +} + +.bridge-decision .flow-arrow svg { + overflow: visible; +} + +.bridge-decision .arrow-line { + stroke: var(--text-muted); + stroke-width: 1.5; + opacity: 0.3; + transition: opacity 0.4s, stroke 0.4s; +} + +.bridge-decision .arrow-head { + fill: var(--text-muted); + opacity: 0.3; + transition: opacity 0.4s, fill 0.4s; +} + +.bridge-decision .arrow-line.active { + stroke: var(--accent); + opacity: 0.8; + filter: drop-shadow(0 0 4px rgba(228, 70, 255, 0.4)); +} + +.bridge-decision .arrow-head.active { + fill: var(--accent); + opacity: 0.8; +} + +/* Particle on arrow */ +.bridge-decision .flow-particle { + position: absolute; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(228, 70, 255, 0.4); + opacity: 0; + pointer-events: none; +} + +/* Flow table (compact, in developer view) */ +.bridge-decision .flow-table-wrap { + flex: 0 0 260px; + position: relative; + z-index: 2; +} + +.bridge-decision .flow-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 10px; + overflow: hidden; + border: 1px solid rgba(163, 116, 255, 0.1); + font-family: 'JetBrains Mono', monospace; + font-size: 11px; +} + +.bridge-decision .flow-table th { + padding: 8px 12px; + font-size: 9px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--node-type-switch); + background: rgba(163, 116, 255, 0.08); + text-align: left; + border-bottom: 1px solid rgba(163, 116, 255, 0.12); +} + +.bridge-decision .flow-table td { + padding: 8px 12px; + color: var(--text-secondary); + border-bottom: 1px solid rgba(255, 255, 255, 0.03); + transition: all 0.4s ease; +} + +.bridge-decision .flow-table tr:last-child td { + border-bottom: none; +} + +.bridge-decision .flow-table tbody tr { + background: var(--surface); + transition: all 0.4s ease; +} + +.bridge-decision .flow-table tbody tr:nth-child(odd) { + background: rgba(26, 15, 10, 0.8); +} + +.bridge-decision .flow-table tbody tr:nth-child(even) { + background: rgba(34, 21, 16, 0.6); +} + +.bridge-decision .flow-table tbody tr.row-match { + background: rgba(228, 70, 255, 0.1) !important; + box-shadow: inset 0 0 0 1px rgba(228, 70, 255, 0.25); +} + +.bridge-decision .flow-table tbody tr.row-match td { + color: var(--text-primary); +} + +.bridge-decision .flow-table tbody tr.row-dim td { + opacity: 0.35; +} + +/* GoRules badge */ +.bridge-decision .gorules-badge { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 14px; + padding: 5px 12px; + border-radius: 6px; + background: rgba(228, 70, 255, 0.06); + border: 1px solid rgba(228, 70, 255, 0.12); + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: var(--text-muted); + letter-spacing: 0.04em; +} + +.bridge-decision .gorules-badge span { + color: var(--accent); + font-weight: 500; +} + +/* ══════════════════════════════════════════════════════ + ANIMATION KEYFRAMES + ══════════════════════════════════════════════════════ */ +@keyframes bridge-decision-particle-flow { + 0% { + left: 0; + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + left: calc(100% - 6px); + opacity: 0; + } +} diff --git a/packages/homepage/components/bridges/bridge-decision.tsx b/packages/homepage/components/bridges/bridge-decision.tsx new file mode 100644 index 0000000..0c7399d --- /dev/null +++ b/packages/homepage/components/bridges/bridge-decision.tsx @@ -0,0 +1,538 @@ +'use client' + +import { useState, useRef, useEffect, useCallback } from 'react' +import './bridge-decision.css' + +interface BridgeDecisionProps { + perspective: 'business' | 'developer' +} + +const testCases = [ + { tier: 'enterprise', value: 50000, matchRow: 1, route: 'express' }, + { tier: 'business', value: 25000, matchRow: 2, route: 'review' }, + { tier: 'business', value: 5000, matchRow: 3, route: 'standard' }, + { tier: 'starter', value: 800, matchRow: 4, route: 'standard' }, +] + +const policyDescriptions: Record = { + first: { + icon: 'F', + text: 'First-hit policy \u2014 returns the first matching row. No ambiguity.', + }, + collect: { + icon: 'C', + text: 'Collect policy \u2014 gathers all matching rows into a list.', + }, + all: { + icon: 'A', + text: 'All policy \u2014 all rows must match; returns a single combined result.', + }, + priority: { + icon: 'P', + text: 'Priority policy \u2014 returns the highest-priority matching row.', + }, +} + +export function BridgeDecision({ perspective }: BridgeDecisionProps) { + const isDev = perspective === 'developer' + const [hitPolicy, setHitPolicy] = useState('first') + + // Animation state + const [inputTier, setInputTier] = useState("'enterprise'") + const [inputValue, setInputValue] = useState('50,000') + const [outputRoute, setOutputRoute] = useState("'express'") + const [inputAnimateIn, setInputAnimateIn] = useState(false) + const [inputActive, setInputActive] = useState(false) + const [outputAnimateIn, setOutputAnimateIn] = useState(false) + const [outputActive, setOutputActive] = useState(false) + const [arrowInActive, setArrowInActive] = useState(false) + const [arrowOutActive, setArrowOutActive] = useState(false) + const [rowStates, setRowStates] = useState< + Array<'normal' | 'match' | 'dim'> + >(['normal', 'normal', 'normal', 'normal']) + + const animationTimerRef = useRef | null>(null) + const currentCaseRef = useRef(0) + const particleInRef = useRef(null) + const particleOutRef = useRef(null) + const arrowInRef = useRef(null) + const arrowOutRef = useRef(null) + const animationActiveRef = useRef(false) + + // Dynamic height refs + const viewsRef = useRef(null) + const bizViewRef = useRef(null) + const devViewRef = useRef(null) + const initialRenderRef = useRef(true) + + // Dynamic height: measure active view and set container height + useEffect(() => { + const activeView = isDev ? devViewRef.current : bizViewRef.current + if (!activeView || !viewsRef.current) return + const h = activeView.scrollHeight + if (initialRenderRef.current) { + viewsRef.current.style.transition = 'none' + viewsRef.current.style.height = `${h}px` + requestAnimationFrame(() => { + if (viewsRef.current) viewsRef.current.style.transition = '' + }) + initialRenderRef.current = false + } else { + viewsRef.current.style.height = `${h}px` + } + }, [isDev]) + + const resetFlowState = useCallback(() => { + setInputAnimateIn(false) + setInputActive(false) + setOutputAnimateIn(false) + setOutputActive(false) + setArrowInActive(false) + setArrowOutActive(false) + setRowStates(['normal', 'normal', 'normal', 'normal']) + if (particleInRef.current) particleInRef.current.style.opacity = '0' + if (particleOutRef.current) particleOutRef.current.style.opacity = '0' + }, []) + + const animateParticle = useCallback((particle: HTMLDivElement | null) => { + if (!particle) return + const parent = particle.parentElement + if (!parent) return + const width = parent.offsetWidth + + particle.style.opacity = '0' + particle.style.left = '0px' + particle.style.top = '50%' + particle.style.transform = 'translateY(-50%)' + + let start: number | null = null + const duration = 400 + + function step(ts: number) { + if (!start) start = ts + const progress = Math.min((ts - start) / duration, 1) + const eased = 1 - Math.pow(1 - progress, 3) // ease-out cubic + + particle!.style.left = eased * (width - 6) + 'px' + particle!.style.opacity = + progress < 0.1 + ? String(progress / 0.1) + : progress > 0.9 + ? String((1 - progress) / 0.1) + : '1' + + if (progress < 1) { + requestAnimationFrame(step) + } else { + particle!.style.opacity = '0' + } + } + + requestAnimationFrame(step) + }, []) + + const runFlowAnimation = useCallback(() => { + if (!animationActiveRef.current) return + + const tc = testCases[currentCaseRef.current] + currentCaseRef.current = + (currentCaseRef.current + 1) % testCases.length + + // Set input values + setInputTier(`'${tc.tier}'`) + setInputValue(tc.value.toLocaleString()) + setOutputRoute(`'${tc.route}'`) + + // Reset + setInputAnimateIn(false) + setInputActive(false) + setOutputAnimateIn(false) + setOutputActive(false) + setArrowInActive(false) + setArrowOutActive(false) + setRowStates(['normal', 'normal', 'normal', 'normal']) + if (particleInRef.current) particleInRef.current.style.opacity = '0' + if (particleOutRef.current) particleOutRef.current.style.opacity = '0' + + // Phase 1: Input slides in (0ms) + requestAnimationFrame(() => { + if (!animationActiveRef.current) return + setInputAnimateIn(true) + setInputActive(true) + }) + + // Phase 2: Arrow in activates + particle (600ms) + setTimeout(() => { + if (!animationActiveRef.current) return + setArrowInActive(true) + animateParticle(particleInRef.current) + }, 600) + + // Phase 3: Table rows scan (1200ms) + const scanDelay = 200 + const scanStart = 1200 + ;[0, 1, 2, 3].forEach((i) => { + setTimeout(() => { + if (!animationActiveRef.current) return + setRowStates((prev) => + prev.map((_, j) => (j === i ? 'match' : 'normal')) + ) + }, scanStart + i * scanDelay) + }) + + // Phase 4: Settle on the matched row (2200ms) + setTimeout(() => { + if (!animationActiveRef.current) return + setRowStates( + [0, 1, 2, 3].map((i) => + i === tc.matchRow - 1 ? 'match' : 'dim' + ) + ) + }, scanStart + 4 * scanDelay + 200) + + // Phase 5: Arrow out + particle (2800ms) + setTimeout(() => { + if (!animationActiveRef.current) return + setArrowOutActive(true) + animateParticle(particleOutRef.current) + }, 2800) + + // Phase 6: Output appears (3200ms) + setTimeout(() => { + if (!animationActiveRef.current) return + setOutputAnimateIn(true) + setOutputActive(true) + }, 3200) + + // Phase 7: Hold and reset (4800ms) + animationTimerRef.current = setTimeout(() => { + if (!animationActiveRef.current) return + resetFlowState() + // Brief pause, then next case + animationTimerRef.current = setTimeout(() => { + if (!animationActiveRef.current) return + runFlowAnimation() + }, 400) + }, 4800) + }, [animateParticle, resetFlowState]) + + const stopAnimation = useCallback(() => { + animationActiveRef.current = false + if (animationTimerRef.current) { + clearTimeout(animationTimerRef.current) + animationTimerRef.current = null + } + resetFlowState() + }, [resetFlowState]) + + const startAnimation = useCallback(() => { + stopAnimation() + currentCaseRef.current = 0 + animationActiveRef.current = true + setTimeout(() => { + if (animationActiveRef.current) { + runFlowAnimation() + } + }, 300) + }, [stopAnimation, runFlowAnimation]) + + // Start/stop animation based on perspective + useEffect(() => { + if (isDev) { + startAnimation() + } else { + stopAnimation() + } + return () => { + stopAnimation() + } + }, [isDev, startAnimation, stopAnimation]) + + const policyInfo = policyDescriptions[hitPolicy] + + return ( +
+
+ {/* Header */} +
+
+ + + {isDev ? 'Developer Perspective' : 'Business Perspective'} + +
+

Decision Tables

+

+ {isDev + ? 'Zero developer code for decision logic. GoRules ZEN evaluates tables natively at runtime.' + : 'Define routing rules in a spreadsheet. No code, no developer needed.'} +

+
+ + {/* Views */} +
+ {/* BUSINESS VIEW */} +
+
+
+ Hit Policy +
+
+ {['first', 'collect', 'all', 'priority'].map( + (policy) => ( + setHitPolicy(policy)} + > + {policy.charAt(0).toUpperCase() + policy.slice(1)} + + ) + )} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
# + Tierinput + + Valueinput + + Routeoutput +
1 + + Enterprise + + + + Any + + + + express + +
2 + + Business + + + + > $10k + + + + review + +
3 + + Business + + + + ≤ $10k + + + + standard + +
4 + + Starter + + + + Any + + + + standard + +
+ +
+ {policyInfo.icon} + {policyInfo.text} +
+
+ + {/* DEVELOPER VIEW */} +
+
+ {/* Input JSON */} +
+ {'{'} +
+    + tier + :{' '} + {inputTier} + , +
+    + value + :{' '} + {inputValue} +
+ {'}'} +
+ + {/* Arrow in */} +
+ + + + +
+
+ + {/* Decision Table */} +
+ + + + + + + + + + {[ + { + tier: 'Enterprise', + value: 'Any', + route: 'express', + }, + { + tier: 'Business', + value: '> $10k', + route: 'review', + }, + { + tier: 'Business', + value: '\u2264 $10k', + route: 'standard', + }, + { + tier: 'Starter', + value: 'Any', + route: 'standard', + }, + ].map((row, i) => ( + + + + + + ))} + +
TierValueRoute
{row.tier}{row.value}{row.route}
+
+ + {/* Arrow out */} +
+ + + + +
+
+ + {/* Output JSON */} +
+ {'{'} +
+    + route + :{' '} + {outputRoute} +
+ {'}'} +
+
+ +
+ Powered by GoRules ZEN — native decision table + evaluation at runtime +
+
+
+
+
+ ) +} diff --git a/packages/homepage/components/bridges/bridge-simulation.css b/packages/homepage/components/bridges/bridge-simulation.css new file mode 100644 index 0000000..c92a0d2 --- /dev/null +++ b/packages/homepage/components/bridges/bridge-simulation.css @@ -0,0 +1,799 @@ +/* ══════════════════════════════════════════════════════ + BRIDGE SIMULATION — Scoped CSS + All selectors prefixed with .bridge-simulation + ══════════════════════════════════════════════════════ */ + +/* Local custom properties (matching prototype :root values + that aren't in globals.css) */ +.bridge-simulation { + --surface: #1a0f0a; + --text-primary: #f0e6df; + --text-secondary: #a89585; +} + +/* ══════════════════════════════════════════════════════ + VIEW LAYERS + ══════════════════════════════════════════════════════ */ +.bridge-simulation .view { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 32px 32px 28px; + transition: opacity 0.5s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.5s cubic-bezier(0.23, 1, 0.32, 1); +} + +.bridge-simulation .view--business { + opacity: 1; + transform: translateY(0); + z-index: 2; +} + +.bridge-simulation .view--developer { + opacity: 0; + transform: translateY(12px); + pointer-events: none; + z-index: 1; +} + +.bridge-simulation.show-dev .view--business { + opacity: 0; + transform: translateY(-12px); + pointer-events: none; +} + +.bridge-simulation.show-dev .view--developer { + opacity: 1; + transform: translateY(0); + pointer-events: auto; + z-index: 2; +} + +/* ══════════════════════════════════════════════════════ + LAYER BADGE + ══════════════════════════════════════════════════════ */ +.bridge-simulation .layer-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + border-radius: 100px; + margin-bottom: 16px; +} + +.bridge-simulation .layer-badge--business { + color: var(--accent); + background: rgba(228, 70, 255, 0.1); + border: 1px solid rgba(228, 70, 255, 0.2); +} + +.bridge-simulation .layer-badge--developer { + color: var(--type-teal); + background: rgba(90, 224, 194, 0.1); + border: 1px solid rgba(90, 224, 194, 0.2); +} + +.bridge-simulation .layer-badge .dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.bridge-simulation .layer-badge--business .dot { background: var(--accent); } +.bridge-simulation .layer-badge--developer .dot { background: var(--type-teal); } + +/* ══════════════════════════════════════════════════════ + TYPOGRAPHY + ══════════════════════════════════════════════════════ */ +.bridge-simulation .card-title { + font-family: 'Instrument Serif', Georgia, serif; + font-size: 32px; + font-weight: 400; + font-style: normal; + letter-spacing: -0.01em; + line-height: 1.15; + margin-bottom: 10px; +} + +.bridge-simulation .card-description { + font-size: 13.5px; + font-weight: 400; + line-height: 1.6; + color: var(--text-secondary); + margin-bottom: 16px; +} + +/* ══════════════════════════════════════════════════════ + PILL TABS (simulation modes) + ══════════════════════════════════════════════════════ */ +.bridge-simulation .pill-tabs { + display: inline-flex; + gap: 2px; + padding: 3px; + background: rgba(228, 70, 255, 0.06); + border: 1px solid rgba(228, 70, 255, 0.1); + border-radius: 100px; + margin-bottom: 14px; +} + +.bridge-simulation .pill-tab { + padding: 6px 16px; + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + font-weight: 500; + letter-spacing: 0.04em; + color: var(--text-muted); + background: transparent; + border: none; + border-radius: 100px; + cursor: pointer; + transition: all 0.3s ease; + white-space: nowrap; +} + +.bridge-simulation .pill-tab:hover { + color: var(--text-secondary); +} + +.bridge-simulation .pill-tab.active { + color: var(--text-primary); + background: rgba(228, 70, 255, 0.15); + box-shadow: 0 0 12px rgba(228, 70, 255, 0.15); +} + +/* ══════════════════════════════════════════════════════ + FLOW CONTAINER + ══════════════════════════════════════════════════════ */ +.bridge-simulation .flow-container { + border: 1px solid rgba(228, 70, 255, 0.08); + border-radius: 12px; + overflow: hidden; + background: var(--code-bg); +} + +.bridge-simulation .flow-panel-header { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: var(--table-header); + border-bottom: 1px solid rgba(228, 70, 255, 0.08); + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + color: var(--text-muted); + letter-spacing: 0.02em; +} + +.bridge-simulation .flow-panel-header svg { + width: 12px; + height: 12px; + opacity: 0.6; +} + +.bridge-simulation .flow-panel-header .header-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + opacity: 0.4; + margin-left: auto; + box-shadow: 0 0 6px rgba(228, 70, 255, 0.3); +} + +.bridge-simulation .flow-panel-body { + padding: 8px 4px 4px; + position: relative; +} + +/* ══════════════════════════════════════════════════════ + SVG FLOW DIAGRAM STYLES + ══════════════════════════════════════════════════════ */ +.bridge-simulation .flow-svg .lane-bg-rect { + fill: rgba(228, 70, 255, 0.03); + stroke: rgba(228, 70, 255, 0.08); + stroke-width: 0.5; +} + +.bridge-simulation .flow-svg .lane-label { + font-family: 'JetBrains Mono', monospace; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + fill: rgba(160, 139, 122, 0.5); +} + +.bridge-simulation .flow-svg .node-rect { + rx: 6; + ry: 6; + fill: var(--node-fill); + stroke: var(--node-stroke); + stroke-width: 1.2; + transition: stroke 0.5s, filter 0.5s, fill 0.5s; +} + +.bridge-simulation .flow-svg .node-circle { + fill: var(--node-fill); + stroke: var(--node-type-terminal); + stroke-width: 1.2; + transition: stroke 0.5s, filter 0.5s; +} + +.bridge-simulation .flow-svg .node-label { + font-family: 'DM Sans', sans-serif; + font-size: 9.5px; + font-weight: 500; + fill: var(--text-primary); + text-anchor: middle; + dominant-baseline: central; + pointer-events: none; + transition: fill 0.4s ease; +} + +.bridge-simulation .flow-svg .node-type-badge { + font-family: 'JetBrains Mono', monospace; + font-size: 5.5px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + text-anchor: middle; + dominant-baseline: central; + pointer-events: none; + opacity: 0.6; +} + +.bridge-simulation .flow-svg .edge { + fill: none; + stroke: #6b4d40; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; + transition: stroke 0.4s ease, stroke-width 0.4s ease, filter 0.4s ease; +} + +.bridge-simulation .flow-svg .edge-label { + font-family: 'JetBrains Mono', monospace; + font-size: 7.5px; + font-weight: 500; + fill: var(--text-muted); + opacity: 0.7; +} + +/* ── Ripple circle (animated via JS) ── */ +.bridge-simulation .ripple-circle { + fill: none; + stroke-width: 1.5; + opacity: 0; + pointer-events: none; +} + +/* ── Data tooltip (step-by-step) ── */ +.bridge-simulation .data-tooltip { + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; +} + +.bridge-simulation .data-tooltip rect { + fill: rgba(13, 8, 5, 0.95); + stroke: rgba(228, 70, 255, 0.25); + stroke-width: 0.7; + rx: 5; +} + +.bridge-simulation .data-tooltip text { + font-family: 'JetBrains Mono', monospace; + font-size: 6.5px; + fill: var(--text-secondary); +} + +.bridge-simulation .data-tooltip .tt-key { + fill: var(--keyword-blue); +} + +.bridge-simulation .data-tooltip .tt-val { + fill: var(--string-amber); +} + +.bridge-simulation .data-tooltip .tt-arrow-label { + fill: var(--text-muted); + font-size: 7px; +} + +.bridge-simulation .data-tooltip .tt-heading { + fill: var(--text-primary); + font-weight: 500; + font-size: 7px; +} + +.bridge-simulation .data-tooltip.visible { + opacity: 1; +} + +/* ══════════════════════════════════════════════════════ + NODE ACTIVATION STATES — GREEN (walk-through / scenario A) + ══════════════════════════════════════════════════════ */ +.bridge-simulation .node-group.active .node-rect, +.bridge-simulation .node-group.active .node-circle { + fill: rgba(63, 220, 119, 0.15); + stroke: var(--node-type-terminal); + filter: drop-shadow(0 0 8px rgba(63, 220, 119, 0.4)); +} + +.bridge-simulation .node-group.active .node-label { + fill: var(--node-type-terminal); +} + +/* ── ERROR activation (what-if scenario B error) ── */ +.bridge-simulation .node-group.active-error .node-rect, +.bridge-simulation .node-group.active-error .node-circle { + fill: rgba(255, 54, 43, 0.15); + stroke: var(--error-red); + filter: drop-shadow(0 0 12px rgba(255, 54, 43, 0.3)); +} + +.bridge-simulation .node-group.active-error .node-label { + fill: var(--error-red); +} + +/* ── Step-by-step: active node with debugger feel ── */ +.bridge-simulation .node-group.debug-active .node-rect, +.bridge-simulation .node-group.debug-active .node-circle { + fill: rgba(63, 220, 119, 0.22); + stroke: var(--node-type-terminal); + stroke-width: 2; + filter: drop-shadow(0 0 12px rgba(63, 220, 119, 0.5)) drop-shadow(0 0 4px rgba(63, 220, 119, 0.3)); +} + +.bridge-simulation .node-group.debug-active .node-label { + fill: var(--node-type-terminal); +} + +/* Visited — subtle blue-purple (#b4befe at 0.2 opacity) */ +.bridge-simulation .node-group.visited .node-rect, +.bridge-simulation .node-group.visited .node-circle { + fill: rgba(180, 190, 254, 0.06); + stroke: rgba(180, 190, 254, 0.2); + filter: none; +} + +.bridge-simulation .node-group.visited .node-label { + fill: rgba(180, 190, 254, 0.5); +} + +/* Edge glow — green */ +.bridge-simulation .edge.active-edge { + stroke: var(--node-type-terminal); + stroke-width: 1.8; + filter: drop-shadow(0 0 6px rgba(63, 220, 119, 0.5)); +} + +.bridge-simulation .edge.visited-edge { + stroke: rgba(180, 190, 254, 0.2); + stroke-width: 1.2; + filter: none; +} + +/* Edge glow — error red (what-if scenario B) */ +.bridge-simulation .edge.error-edge { + stroke: var(--error-red); + stroke-width: 1.8; + filter: drop-shadow(0 0 6px rgba(255, 54, 43, 0.5)); +} + +.bridge-simulation .edge.error-visited-edge { + stroke: rgba(255, 54, 43, 0.2); + stroke-width: 1.2; + filter: none; +} + +/* Edge glow — debug (step-by-step) */ +.bridge-simulation .edge.debug-edge { + stroke: var(--node-type-terminal); + stroke-width: 2.2; + filter: drop-shadow(0 0 8px rgba(63, 220, 119, 0.6)); +} + +.bridge-simulation .edge.debug-visited-edge { + stroke: rgba(180, 190, 254, 0.2); + stroke-width: 1.2; + filter: none; +} + +/* ══════════════════════════════════════════════════════ + STEP CONTROL BAR (step-by-step mode) + ══════════════════════════════════════════════════════ */ +.bridge-simulation .step-controls { + display: none; + align-items: center; + justify-content: center; + gap: 16px; + padding: 10px 16px; + border-top: 1px solid rgba(228, 70, 255, 0.08); + background: rgba(42, 26, 18, 0.6); +} + +.bridge-simulation .step-controls.visible { + display: flex; +} + +.bridge-simulation .step-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 500; + letter-spacing: 0.03em; + color: var(--text-secondary); + background: rgba(228, 70, 255, 0.08); + border: 1px solid rgba(228, 70, 255, 0.15); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.bridge-simulation .step-btn:hover:not(:disabled) { + color: var(--text-primary); + background: rgba(228, 70, 255, 0.15); + border-color: rgba(228, 70, 255, 0.3); + box-shadow: 0 0 12px rgba(228, 70, 255, 0.1); +} + +.bridge-simulation .step-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.bridge-simulation .step-indicator { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + font-weight: 500; + color: var(--text-primary); + min-width: 90px; + text-align: center; +} + +/* ══════════════════════════════════════════════════════ + SCENARIO LABEL (what-if mode) + ══════════════════════════════════════════════════════ */ +.bridge-simulation .scenario-label { + display: none; + position: absolute; + top: 12px; + right: 16px; + padding: 5px 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + border-radius: 6px; + z-index: 5; + transition: all 0.4s ease; +} + +.bridge-simulation .scenario-label.visible { + display: block; +} + +.bridge-simulation .scenario-label.scenario-a { + color: var(--node-type-terminal); + background: rgba(63, 220, 119, 0.1); + border: 1px solid rgba(63, 220, 119, 0.25); +} + +.bridge-simulation .scenario-label.scenario-b { + color: var(--error-red); + background: rgba(255, 54, 43, 0.1); + border: 1px solid rgba(255, 54, 43, 0.25); +} + +/* ══════════════════════════════════════════════════════ + ERROR TOOLTIP (what-if mode) + ══════════════════════════════════════════════════════ */ +.bridge-simulation .error-tooltip { + opacity: 0; + pointer-events: none; + transition: opacity 0.4s ease; +} + +.bridge-simulation .error-tooltip.visible { + opacity: 1; +} + +.bridge-simulation .error-tooltip rect { + fill: rgba(13, 8, 5, 0.95); + stroke: rgba(255, 54, 43, 0.4); + stroke-width: 0.8; + rx: 5; +} + +.bridge-simulation .error-tooltip text { + font-family: 'JetBrains Mono', monospace; + font-size: 7.5px; + fill: var(--error-red); + font-weight: 500; +} + +.bridge-simulation .error-tooltip .tt-icon { + fill: var(--error-red); + font-size: 9px; + font-weight: 700; +} + +/* ══════════════════════════════════════════════════════ + CTA / TOGGLE BUTTON + ══════════════════════════════════════════════════════ */ +.bridge-simulation .cta { + display: inline-flex; + align-items: center; + gap: 10px; + margin-top: 16px; + padding: 11px 22px; + font-family: 'DM Sans', system-ui, sans-serif; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--text-primary); + background: linear-gradient(135deg, rgba(228, 70, 255, 0.15), rgba(228, 70, 255, 0.05)); + border: 1px solid rgba(228, 70, 255, 0.25); + border-radius: 10px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.bridge-simulation .cta::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(228, 70, 255, 0.1), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + +.bridge-simulation .cta:hover::before { opacity: 1; } + +.bridge-simulation .cta:hover { + border-color: rgba(228, 70, 255, 0.45); + box-shadow: 0 0 30px rgba(228, 70, 255, 0.15); +} + +.bridge-simulation .cta-icon { + display: inline-flex; + transition: transform 0.4s cubic-bezier(0.23, 1, 0.32, 1); +} + +/* ══════════════════════════════════════════════════════ + DEVELOPER VIEW: TERMINAL OUTPUT + ══════════════════════════════════════════════════════ */ +.bridge-simulation .terminal-container { + border: 1px solid rgba(228, 70, 255, 0.08); + border-radius: 12px; + overflow: hidden; + background: var(--code-bg); + margin-top: 4px; +} + +.bridge-simulation .terminal-header { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: var(--table-header); + border-bottom: 1px solid rgba(228, 70, 255, 0.08); + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + color: var(--text-muted); + letter-spacing: 0.02em; +} + +.bridge-simulation .terminal-header .header-dots { + display: flex; + gap: 5px; + margin-right: 8px; +} + +.bridge-simulation .terminal-header .header-dots span { + width: 7px; + height: 7px; + border-radius: 50%; + opacity: 0.5; +} + +.bridge-simulation .terminal-header .header-dots span:nth-child(1) { background: var(--false-red); } +.bridge-simulation .terminal-header .header-dots span:nth-child(2) { background: var(--string-amber); } +.bridge-simulation .terminal-header .header-dots span:nth-child(3) { background: var(--true-green); } + +.bridge-simulation .terminal-body { + padding: 16px 20px; + font-family: 'JetBrains Mono', monospace; + font-size: 12.5px; + line-height: 1.9; + min-height: 220px; +} + +.bridge-simulation .term-line { + opacity: 0; + transform: translateY(4px); + transition: opacity 0.3s ease, transform 0.3s ease; + white-space: nowrap; +} + +.bridge-simulation .term-line.revealed { + opacity: 1; + transform: translateY(0); +} + +.bridge-simulation .term-cmd { color: var(--text-secondary); } +.bridge-simulation .term-prompt { color: var(--true-green); } +.bridge-simulation .term-file { color: var(--accent); } +.bridge-simulation .term-check { color: var(--true-green); } +.bridge-simulation .term-node-name { + color: var(--text-primary); + display: inline-block; + min-width: 130px; +} +.bridge-simulation .term-timing { color: var(--text-muted); } +.bridge-simulation .term-route { color: var(--node-type-switch); } +.bridge-simulation .term-blank { height: 1.9em; } +.bridge-simulation .term-summary { color: var(--true-green); font-weight: 500; } +.bridge-simulation .term-cursor { + display: inline-block; + width: 7px; + height: 14px; + background: var(--text-primary); + opacity: 0.7; + animation: bridge-sim-blink-cursor 1s step-end infinite; + vertical-align: middle; + margin-left: 2px; +} + +@keyframes bridge-sim-blink-cursor { + 50% { opacity: 0; } +} + +/* ══════════════════════════════════════════════════════ + STATUS BAR (bottom of flow container) + ══════════════════════════════════════════════════════ */ +.bridge-simulation .flow-status { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-top: 1px solid rgba(228, 70, 255, 0.06); + font-family: 'JetBrains Mono', monospace; + font-size: 9.5px; + color: var(--text-muted); +} + +.bridge-simulation .flow-status .status-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--true-green); + animation: bridge-sim-pulse-dot 2s ease-in-out infinite; +} + +.bridge-simulation .flow-status .status-dot.paused { + background: var(--string-amber); + animation: none; +} + +.bridge-simulation .flow-status .status-dot.error { + background: var(--error-red); + animation: bridge-sim-pulse-dot-red 1s ease-in-out infinite; +} + +@keyframes bridge-sim-pulse-dot { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; box-shadow: 0 0 6px rgba(90, 224, 122, 0.5); } +} + +@keyframes bridge-sim-pulse-dot-red { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; box-shadow: 0 0 6px rgba(255, 54, 43, 0.5); } +} + +.bridge-simulation .flow-status .status-mode { + color: var(--accent); + margin-left: auto; +} + +/* ══════════════════════════════════════════════════════ + KEYFRAME: RIPPLE + ══════════════════════════════════════════════════════ */ +@keyframes bridge-sim-ripple-expand { + 0% { + r: 4; + opacity: 0.7; + stroke-width: 2; + } + 60% { + opacity: 0.3; + } + 100% { + r: 38; + opacity: 0; + stroke-width: 0.3; + } +} + +/* ══════════════════════════════════════════════════════ + KEYFRAME: DEBUG BREAKPOINT PULSE + ══════════════════════════════════════════════════════ */ +@keyframes bridge-sim-debug-pulse { + 0%, 100% { + stroke-width: 2; + filter: drop-shadow(0 0 10px rgba(63, 220, 119, 0.5)); + } + 50% { + stroke-width: 2.5; + filter: drop-shadow(0 0 16px rgba(63, 220, 119, 0.7)); + } +} + +.bridge-simulation .node-group.debug-active .node-rect, +.bridge-simulation .node-group.debug-active .node-circle { + animation: bridge-sim-debug-pulse 1.5s ease-in-out infinite; +} + +/* ══════════════════════════════════════════════════════ + KEYFRAME: ERROR PULSE + ══════════════════════════════════════════════════════ */ +@keyframes bridge-sim-error-pulse { + 0%, 100% { + stroke-width: 2; + filter: drop-shadow(0 0 10px rgba(255, 54, 43, 0.3)); + } + 50% { + stroke-width: 2.5; + filter: drop-shadow(0 0 18px rgba(255, 54, 43, 0.6)); + } +} + +.bridge-simulation .node-group.active-error .node-rect, +.bridge-simulation .node-group.active-error .node-circle { + animation: bridge-sim-error-pulse 1s ease-in-out infinite; +} + +/* ══════════════════════════════════════════════════════ + KEYFRAME: NEW NODE APPEAR + ══════════════════════════════════════════════════════ */ +@keyframes bridge-sim-node-appear { + 0% { + transform: scale(0); + opacity: 0; + } + 60% { + transform: scale(1.08); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +@keyframes bridge-sim-new-node-glow { + 0%, 100% { + filter: drop-shadow(0 0 8px rgba(63, 220, 119, 0.4)); + } + 50% { + filter: drop-shadow(0 0 16px rgba(63, 220, 119, 0.7)); + } +} + +/* ══════════════════════════════════════════════════════ + SVG VISIBILITY: show/hide per mode + ══════════════════════════════════════════════════════ */ +.bridge-simulation .flow-svg { display: none; } +.bridge-simulation .flow-svg.active-svg { display: block; } diff --git a/packages/homepage/components/bridges/bridge-simulation.tsx b/packages/homepage/components/bridges/bridge-simulation.tsx new file mode 100644 index 0000000..526c931 --- /dev/null +++ b/packages/homepage/components/bridges/bridge-simulation.tsx @@ -0,0 +1,1331 @@ +'use client' + +import { useState, useEffect, useRef, useCallback } from 'react' +import './bridge-simulation.css' +import '../flow/flow.css' + +// ══════════════════════════════════════════════════════ +// TYPES +// ══════════════════════════════════════════════════════ + +interface BridgeSimulationProps { + perspective: 'business' | 'developer' +} + +interface RippleObj { + id: string + cx: number + cy: number + color: string + delay: number +} + +type SimMode = 'walkthrough' | 'stepbystep' | 'whatif' + +// ══════════════════════════════════════════════════════ +// NODE & EDGE DEFINITIONS +// ══════════════════════════════════════════════════════ + +// Walk-through: Patient Intake — urgent path +const walkthroughPath = { + nodes: ['wt-node-checkin', 'wt-node-verify', 'wt-node-assess', 'wt-node-priority', 'wt-node-emergprep', 'wt-node-review', 'wt-node-end'], + edges: ['wt-edge-checkin-verify', 'wt-edge-verify-assess', 'wt-edge-assess-priority', 'wt-edge-priority-urgent-v', 'wt-edge-emergprep-review', 'wt-edge-review-end'], + tooltips: [null, null, null, null, null, null, null] as (string | null)[], + labels: ['Check In', 'Verify Insurance', 'Initial Assessment', 'Priority Rating', 'Emergency Prep', 'Doctor Review', 'End'], +} + +// Step-by-step: Loan Application — approved path +const stepByStepPath = { + nodes: ['sb-node-submit', 'sb-node-upload', 'sb-node-credit', 'sb-node-risk', 'sb-node-offer', 'sb-node-disburse', 'sb-node-end'], + edges: ['sb-edge-submit-upload', 'sb-edge-upload-credit', 'sb-edge-credit-risk', 'sb-edge-risk-approved-v', 'sb-edge-offer-disburse', 'sb-edge-disburse-end'], + tooltips: ['tt-sb-submit', 'tt-sb-upload', 'tt-sb-credit', 'tt-sb-risk', 'tt-sb-offer', 'tt-sb-disburse', 'tt-sb-end'], + labels: ['Submit Application', 'Upload Documents', 'Credit Check', 'Risk Assessment', 'Generate Offer', 'Disburse Funds', 'End'], +} + +// What-if Scenario A: Insurance Claim — approve path (green, happy) +const whatIfPathA = { + nodes: ['wi-node-file', 'wi-node-evidence', 'wi-node-review', 'wi-node-assess', 'wi-node-payout', 'wi-node-issue', 'wi-node-end'], + edges: ['wi-edge-file-evidence', 'wi-edge-evidence-review', 'wi-edge-review-assess', 'wi-edge-assess-approve-v', 'wi-edge-payout-issue', 'wi-edge-issue-end'], + labels: ['File Claim', 'Submit Evidence', 'Review Claim', 'Assess Damage', 'Calculate Payout', 'Issue Payment', 'End'], +} + +// What-if Scenario B: Insurance Claim — investigate path with Fraud Check ERROR +const whatIfPathB = { + nodes: ['wi-node-file', 'wi-node-evidence', 'wi-node-review', 'wi-node-assess', 'wi-node-fraud'], + edges: ['wi-edge-file-evidence', 'wi-edge-evidence-review', 'wi-edge-review-assess', 'wi-edge-assess-investigate-v'], + labels: ['File Claim', 'Submit Evidence', 'Review Claim', 'Assess Damage', 'Fraud Check'], +} + +// All IDs per SVG for reset +const allWtNodeIds = ['wt-node-checkin', 'wt-node-verify', 'wt-node-assess', 'wt-node-priority', 'wt-node-emergprep', 'wt-node-review', 'wt-node-schedule', 'wt-node-end'] +const allWtEdgeIds = ['wt-edge-checkin-verify', 'wt-edge-verify-assess', 'wt-edge-assess-priority', 'wt-edge-priority-urgent-v', 'wt-edge-priority-routine-h', 'wt-edge-priority-routine-v', 'wt-edge-emergprep-review', 'wt-edge-review-end', 'wt-edge-sched-end-h'] + +const allSbNodeIds = ['sb-node-submit', 'sb-node-upload', 'sb-node-credit', 'sb-node-risk', 'sb-node-offer', 'sb-node-disburse', 'sb-node-reject', 'sb-node-manual', 'sb-node-end'] +const allSbEdgeIds = ['sb-edge-submit-upload', 'sb-edge-upload-credit', 'sb-edge-credit-risk', 'sb-edge-risk-approved-v', 'sb-edge-risk-denied-h', 'sb-edge-risk-denied-v', 'sb-edge-risk-review-h', 'sb-edge-risk-review-v', 'sb-edge-offer-disburse', 'sb-edge-disburse-end', 'sb-edge-reject-end-h', 'sb-edge-reject-end-v', 'sb-edge-manual-risk-v', 'sb-edge-manual-risk-h'] +const allSbTooltipIds = ['tt-sb-submit', 'tt-sb-upload', 'tt-sb-credit', 'tt-sb-risk', 'tt-sb-offer', 'tt-sb-disburse', 'tt-sb-reject', 'tt-sb-manual', 'tt-sb-end'] + +const allWiNodeIds = ['wi-node-file', 'wi-node-evidence', 'wi-node-review', 'wi-node-assess', 'wi-node-payout', 'wi-node-issue', 'wi-node-end', 'wi-node-fraud'] +const allWiEdgeIds = ['wi-edge-file-evidence', 'wi-edge-evidence-review', 'wi-edge-review-assess', 'wi-edge-assess-approve-v', 'wi-edge-assess-investigate-h', 'wi-edge-assess-investigate-v', 'wi-edge-payout-issue', 'wi-edge-issue-end'] + +const fileNames: Record = { + walkthrough: 'patient-intake.flowprint', + stepbystep: 'loan-application.flowprint', + whatif: 'insurance-claim.flowprint', +} + +// Node center positions (from data-cx, data-cy) +const nodeCenters: Record = { + 'wt-node-checkin': { x: 200, y: 63 }, + 'wt-node-verify': { x: 430, y: 63 }, + 'wt-node-assess': { x: 490, y: 187 }, + 'wt-node-priority': { x: 660, y: 187 }, + 'wt-node-emergprep': { x: 600, y: 325 }, + 'wt-node-review': { x: 310, y: 325 }, + 'wt-node-schedule': { x: 780, y: 375 }, + 'wt-node-end': { x: 130, y: 325 }, + 'sb-node-submit': { x: 200, y: 58 }, + 'sb-node-upload': { x: 430, y: 58 }, + 'sb-node-credit': { x: 430, y: 182 }, + 'sb-node-risk': { x: 640, y: 182 }, + 'sb-node-offer': { x: 640, y: 320 }, + 'sb-node-disburse': { x: 560, y: 380 }, + 'sb-node-reject': { x: 780, y: 320 }, + 'sb-node-manual': { x: 340, y: 320 }, + 'sb-node-end': { x: 160, y: 380 }, + 'wi-node-file': { x: 200, y: 58 }, + 'wi-node-evidence': { x: 430, y: 58 }, + 'wi-node-review': { x: 430, y: 182 }, + 'wi-node-assess': { x: 640, y: 182 }, + 'wi-node-payout': { x: 630, y: 325 }, + 'wi-node-issue': { x: 320, y: 325 }, + 'wi-node-end': { x: 130, y: 325 }, + 'wi-node-fraud': { x: 780, y: 325 }, +} + +// ══════════════════════════════════════════════════════ +// COMPONENT +// ══════════════════════════════════════════════════════ + +export function BridgeSimulation({ perspective }: BridgeSimulationProps) { + const [showDev, setShowDev] = useState(perspective === 'developer') + const [currentMode, setCurrentMode] = useState('walkthrough') + const [statusText, setStatusText] = useState('Simulating...') + const [statusDotClass, setStatusDotClass] = useState('') + const [activeSvg, setActiveSvg] = useState('walkthrough') + const [stepControlsVisible, setStepControlsVisible] = useState(false) + const [stepIndex, setStepIndex] = useState(-1) + const [scenarioLabelText, setScenarioLabelText] = useState('') + const [scenarioLabelClass, setScenarioLabelClass] = useState('') + const [scenarioLabelVisible, setScenarioLabelVisible] = useState(false) + const [errorTooltipVisible, setErrorTooltipVisible] = useState(false) + + // Node/edge class states + const [nodeClasses, setNodeClasses] = useState>({}) + const [edgeClasses, setEdgeClasses] = useState>({}) + const [tooltipVisible, setTooltipVisible] = useState>({}) + + // Ripple state + const [wtRipples, setWtRipples] = useState([]) + const [sbRipples, setSbRipples] = useState([]) + const [wiRipples, setWiRipples] = useState([]) + + // Fraud node visibility + const [fraudNodeStyle, setFraudNodeStyle] = useState({ + opacity: 0, + transformOrigin: '780px 325px', + transform: 'scale(0)', + }) + const [fraudNodeRectStyle, setFraudNodeRectStyle] = useState({ + stroke: 'rgba(255,146,67,0.3)', + }) + // Investigate edges/label visibility + const [investigateEdgesOpacity, setInvestigateEdgesOpacity] = useState(0) + const [investigateLabelOpacity, setInvestigateLabelOpacity] = useState(0) + + // Developer terminal + const [revealedLines, setRevealedLines] = useState>(new Set()) + + // Timer refs + const animationTimers = useRef([]) + const devAnimTimers = useRef([]) + const rippleIdCounter = useRef(0) + const currentViewRef = useRef<'business' | 'developer'>(perspective === 'developer' ? 'developer' : 'business') + const currentModeRef = useRef('walkthrough') + const stepIndexRef = useRef(-1) + + // Dynamic height refs + const containerRef = useRef(null) + const bizViewRef = useRef(null) + const devViewRef = useRef(null) + const initialRender = useRef(true) + + // Sync showDev with parent perspective prop and trigger animations + const hasInitialized = useRef(false) + useEffect(() => { + const isDev = perspective === 'developer' + setShowDev(isDev) + currentViewRef.current = isDev ? 'developer' : 'business' + + // Skip animation trigger on initial mount (handled by INIT effect) + if (!hasInitialized.current) { + hasInitialized.current = true + return + } + + if (isDev) { + stopAllAnimations() + setTimeout(() => startDevAnimation(), 0) + } else { + stopDevAnimation() + startSimulation(currentModeRef.current) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [perspective]) + + // Dynamic height: measure active view and set container height + useEffect(() => { + const activeView = showDev ? devViewRef.current : bizViewRef.current + if (!activeView || !containerRef.current) return + const h = activeView.scrollHeight + if (initialRender.current) { + containerRef.current.style.transition = 'none' + containerRef.current.style.height = `${h}px` + requestAnimationFrame(() => { + if (containerRef.current) { + containerRef.current.style.transition = '' + } + }) + initialRender.current = false + } else { + containerRef.current.style.height = `${h}px` + } + }, [showDev]) + + // Keep refs in sync + useEffect(() => { + currentViewRef.current = showDev ? 'developer' : 'business' + }, [showDev]) + + useEffect(() => { + currentModeRef.current = currentMode + }, [currentMode]) + + useEffect(() => { + stepIndexRef.current = stepIndex + }, [stepIndex]) + + // ══════════════════════════════════════════════════════ + // TIMER HELPERS + // ══════════════════════════════════════════════════════ + const addTimer = useCallback((fn: () => void, delay: number) => { + const t = window.setTimeout(fn, delay) + animationTimers.current.push(t) + return t + }, []) + + const clearTimers = useCallback(() => { + animationTimers.current.forEach(t => clearTimeout(t)) + animationTimers.current = [] + }, []) + + const addDevTimer = useCallback((fn: () => void, delay: number) => { + const t = window.setTimeout(fn, delay) + devAnimTimers.current.push(t) + return t + }, []) + + const clearDevTimers = useCallback(() => { + devAnimTimers.current.forEach(t => clearTimeout(t)) + devAnimTimers.current = [] + }, []) + + // ══════════════════════════════════════════════════════ + // RIPPLE EFFECT + // ══════════════════════════════════════════════════════ + const spawnRipple = useCallback((cx: number, cy: number, color: string, mode: SimMode) => { + const newRipples: RippleObj[] = [] + for (let i = 0; i < 3; i++) { + const id = `ripple-${rippleIdCounter.current++}` + newRipples.push({ id, cx, cy, color, delay: i * 0.2 }) + } + + const setter = + mode === 'walkthrough' ? setWtRipples : + mode === 'stepbystep' ? setSbRipples : + setWiRipples + + setter(prev => [...prev, ...newRipples]) + + // Remove ripples after animation completes + newRipples.forEach((r, i) => { + addTimer(() => { + setter(prev => prev.filter(p => p.id !== r.id)) + }, 1400 + i * 200) + }) + }, [addTimer]) + + // ══════════════════════════════════════════════════════ + // RESET + // ══════════════════════════════════════════════════════ + const hideFraudNode = useCallback(() => { + setFraudNodeStyle({ + opacity: 0, + transformOrigin: '780px 325px', + transform: 'scale(0)', + }) + setFraudNodeRectStyle({ stroke: 'rgba(255,146,67,0.3)' }) + }, []) + + const hideInvestigateEdges = useCallback(() => { + setInvestigateEdgesOpacity(0) + setInvestigateLabelOpacity(0) + }, []) + + const resetAllSvgStates = useCallback(() => { + setNodeClasses({}) + setEdgeClasses({}) + setTooltipVisible({}) + setWtRipples([]) + setSbRipples([]) + setWiRipples([]) + setScenarioLabelVisible(false) + setScenarioLabelClass('') + setStepControlsVisible(false) + setStatusDotClass('') + setErrorTooltipVisible(false) + hideFraudNode() + hideInvestigateEdges() + }, [hideFraudNode, hideInvestigateEdges]) + + const stopAllAnimations = useCallback(() => { + clearTimers() + resetAllSvgStates() + }, [clearTimers, resetAllSvgStates]) + + // ══════════════════════════════════════════════════════ + // MODE 1: WALK-THROUGH + // ══════════════════════════════════════════════════════ + const runWalkthrough = useCallback(() => { + const path = walkthroughPath + const delay = 500 + const totalDuration = path.nodes.length * delay + 1500 + + path.nodes.forEach((nodeId, i) => { + addTimer(() => { + setNodeClasses(prev => { + const next = { ...prev } + if (i > 0) { + next[path.nodes[i - 1]] = 'visited' + } + next[nodeId] = 'active' + return next + }) + + if (i > 0 && path.edges[i - 1]) { + setEdgeClasses(prev => ({ + ...prev, + [path.edges[i - 1]]: 'visited-edge', + })) + } + if (path.edges[i]) { + setEdgeClasses(prev => ({ + ...prev, + [path.edges[i]]: 'active-edge', + })) + } + + const center = nodeCenters[nodeId] + if (center) spawnRipple(center.x, center.y, '#3FDC77', 'walkthrough') + + setStatusText(`Walking: ${path.labels[i]} \u2022 ${i + 1}/${path.nodes.length} nodes`) + }, i * delay) + }) + + // Mark last node as visited + addTimer(() => { + const lastNode = path.nodes[path.nodes.length - 1] + setNodeClasses(prev => ({ ...prev, [lastNode]: 'visited' })) + const lastEdge = path.edges[path.edges.length - 1] + if (lastEdge) { + setEdgeClasses(prev => ({ ...prev, [lastEdge]: 'visited-edge' })) + } + setStatusText(`Walking: complete \u2022 ${path.nodes.length}/${path.nodes.length} nodes`) + }, path.nodes.length * delay) + + // Loop + addTimer(() => { + if (currentViewRef.current === 'business' && currentModeRef.current === 'walkthrough') { + resetAllSvgStates() + setActiveSvg('walkthrough') + // Need to re-run after reset + setTimeout(() => { + runWalkthrough() + }, 0) + } + }, totalDuration) + }, [addTimer, spawnRipple, resetAllSvgStates]) + + // ══════════════════════════════════════════════════════ + // MODE 2: STEP-BY-STEP + // ══════════════════════════════════════════════════════ + const stepForward = useCallback(() => { + const path = stepByStepPath + const idx = stepIndexRef.current + + if (idx >= path.nodes.length - 1) return + + // Transition current node from debug-active to visited + if (idx >= 0) { + setNodeClasses(prev => ({ ...prev, [path.nodes[idx]]: 'visited' })) + if (path.tooltips[idx]) { + setTooltipVisible(prev => ({ ...prev, [path.tooltips[idx]!]: false })) + } + if (path.edges[idx]) { + setEdgeClasses(prev => ({ ...prev, [path.edges[idx]]: 'debug-visited-edge' })) + } + } + + const newIdx = idx + 1 + stepIndexRef.current = newIdx + setStepIndex(newIdx) + + // Activate new node + setNodeClasses(prev => ({ ...prev, [path.nodes[newIdx]]: 'debug-active' })) + + // Spawn ripple + const center = nodeCenters[path.nodes[newIdx]] + if (center) spawnRipple(center.x, center.y, '#3FDC77', 'stepbystep') + + // Show tooltip + if (path.tooltips[newIdx]) { + setTooltipVisible(prev => ({ ...prev, [path.tooltips[newIdx]!]: true })) + } + + // Activate leading edge + if (path.edges[newIdx]) { + setEdgeClasses(prev => ({ ...prev, [path.edges[newIdx]]: 'debug-edge' })) + } + + // Update status + const nodeName = path.labels[newIdx] + setStatusText(`Step ${newIdx + 1} of ${path.nodes.length} \u2022 ${nodeName}`) + }, [spawnRipple]) + + const stepBackward = useCallback(() => { + const path = stepByStepPath + const idx = stepIndexRef.current + + if (idx <= 0) return + + // Remove current node activation + setNodeClasses(prev => { + const next = { ...prev } + delete next[path.nodes[idx]] + return next + }) + if (path.tooltips[idx]) { + setTooltipVisible(prev => ({ ...prev, [path.tooltips[idx]!]: false })) + } + if (path.edges[idx]) { + setEdgeClasses(prev => { + const next = { ...prev } + delete next[path.edges[idx]] + return next + }) + } + + const newIdx = idx - 1 + stepIndexRef.current = newIdx + setStepIndex(newIdx) + + // Re-activate previous node + setNodeClasses(prev => ({ ...prev, [path.nodes[newIdx]]: 'debug-active' })) + if (path.tooltips[newIdx]) { + setTooltipVisible(prev => ({ ...prev, [path.tooltips[newIdx]!]: true })) + } + if (path.edges[newIdx]) { + setEdgeClasses(prev => ({ ...prev, [path.edges[newIdx]]: 'debug-edge' })) + } + + const nodeName = path.labels[newIdx] + setStatusText(`Step ${newIdx + 1} of ${path.nodes.length} \u2022 ${nodeName}`) + }, []) + + const initStepByStep = useCallback(() => { + stepIndexRef.current = -1 + setStepIndex(-1) + setStepControlsVisible(true) + setStatusDotClass('paused') + setStatusText('Step-by-step \u2022 click Next to begin') + // Auto-advance to first step + setTimeout(() => stepForward(), 0) + }, [stepForward]) + + // ══════════════════════════════════════════════════════ + // MODE 3: WHAT-IF + // ══════════════════════════════════════════════════════ + const showFraudNodeFn = useCallback((callback?: () => void) => { + // Animate the node appearing + setFraudNodeStyle({ + opacity: 1, + transformOrigin: '780px 325px', + transform: 'scale(1)', + transition: 'opacity 0.6s ease, transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)', + }) + + // Green glow on new node + setFraudNodeRectStyle({ + stroke: '#3FDC77', + filter: 'drop-shadow(0 0 12px rgba(63, 220, 119, 0.5))', + transition: 'all 0.6s ease', + }) + + // Fade in edges and label + addTimer(() => { + setInvestigateEdgesOpacity(1) + setInvestigateLabelOpacity(1) + }, 300) + + // Pulse the green glow + let pulseCount = 0 + const pulseInterval = window.setInterval(() => { + if (pulseCount >= 3) { + clearInterval(pulseInterval) + setFraudNodeRectStyle({ stroke: 'rgba(255,146,67,0.3)' }) + if (callback) callback() + return + } + const bright = pulseCount % 2 === 0 + setFraudNodeRectStyle({ + stroke: '#3FDC77', + filter: bright + ? 'drop-shadow(0 0 16px rgba(63, 220, 119, 0.7))' + : 'drop-shadow(0 0 8px rgba(63, 220, 119, 0.3))', + transition: 'all 0.6s ease', + }) + pulseCount++ + }, 400) + + // Store interval for cleanup + animationTimers.current.push(pulseInterval as unknown as number) + }, [addTimer]) + + const runScenarioBPath = useCallback(( + path: typeof whatIfPathB, + delay: number, + onDone?: () => void, + ) => { + const startDelay = 800 + + path.nodes.forEach((nodeId, i) => { + addTimer(() => { + const isLast = i === path.nodes.length - 1 + const isError = isLast + + if (i > 0) { + setNodeClasses(prev => ({ ...prev, [path.nodes[i - 1]]: 'visited' })) + if (path.edges[i - 1]) { + setEdgeClasses(prev => ({ ...prev, [path.edges[i - 1]]: 'visited-edge' })) + } + } + + if (isError) { + // ERROR STATE + setNodeClasses(prev => ({ ...prev, [nodeId]: 'active-error' })) + + // Red ripple + const center = nodeCenters[nodeId] + if (center) { + spawnRipple(center.x, center.y, '#FF362B', 'whatif') + addTimer(() => spawnRipple(center.x, center.y, '#FF362B', 'whatif'), 300) + addTimer(() => spawnRipple(center.x, center.y, '#FF362B', 'whatif'), 600) + } + + // Show error tooltip + addTimer(() => { + setErrorTooltipVisible(true) + }, 200) + + // Update status to error + setStatusDotClass('error') + setStatusText('Scenario B \u2022 ERROR at Fraud Check') + setScenarioLabelText('Scenario B: + Fraud Detection \u2192 Alert detected') + } else { + setNodeClasses(prev => ({ ...prev, [nodeId]: 'active' })) + const center = nodeCenters[nodeId] + if (center) spawnRipple(center.x, center.y, '#3FDC77', 'whatif') + + if (path.edges[i]) { + setEdgeClasses(prev => ({ ...prev, [path.edges[i]]: 'active-edge' })) + } + + setStatusText(`Scenario B \u2022 ${path.labels[i]} \u2022 ${i + 1}/${path.nodes.length}`) + } + }, startDelay + i * delay) + }) + + // Hold on error state for 3 seconds + const totalTime = startDelay + path.nodes.length * delay + addTimer(() => { + if (onDone) onDone() + }, totalTime + 3000) + }, [addTimer, spawnRipple]) + + const runWhatIfScenarioB = useCallback((onDone?: () => void) => { + const path = whatIfPathB + const delay = 500 + + // Reset graph state + resetAllSvgStates() + setActiveSvg('whatif') + + setScenarioLabelText('Scenario B: + Fraud Detection') + setScenarioLabelClass('scenario-b') + setScenarioLabelVisible(true) + + // First: morph in the fraud check node + addTimer(() => { + setStatusText('Adding Fraud Check node...') + showFraudNodeFn(() => { + // Now run the path through to the error + runScenarioBPath(path, delay, onDone) + }) + }, 400) + }, [addTimer, resetAllSvgStates, showFraudNodeFn, runScenarioBPath]) + + const runWhatIfScenarioA = useCallback((onDone?: () => void) => { + const path = whatIfPathA + const delay = 450 + + setScenarioLabelText('Scenario A: Normal Claim') + setScenarioLabelClass('scenario-a') + setScenarioLabelVisible(true) + + path.nodes.forEach((nodeId, i) => { + addTimer(() => { + if (i > 0) { + setNodeClasses(prev => ({ ...prev, [path.nodes[i - 1]]: 'visited' })) + if (path.edges[i - 1]) { + setEdgeClasses(prev => ({ ...prev, [path.edges[i - 1]]: 'visited-edge' })) + } + } + + setNodeClasses(prev => ({ ...prev, [nodeId]: 'active' })) + + const center = nodeCenters[nodeId] + if (center) spawnRipple(center.x, center.y, '#3FDC77', 'whatif') + + if (path.edges[i]) { + setEdgeClasses(prev => ({ ...prev, [path.edges[i]]: 'active-edge' })) + } + + setStatusText(`Scenario A \u2022 ${path.labels[i]} \u2022 ${i + 1}/${path.nodes.length}`) + }, i * delay) + }) + + const totalTime = path.nodes.length * delay + + addTimer(() => { + const lastNode = path.nodes[path.nodes.length - 1] + setNodeClasses(prev => ({ ...prev, [lastNode]: 'visited' })) + const lastEdge = path.edges[path.edges.length - 1] + if (lastEdge) { + setEdgeClasses(prev => ({ ...prev, [lastEdge]: 'visited-edge' })) + } + setStatusText('Scenario A \u2022 All nodes passed') + }, totalTime) + + // Brief pause then callback + addTimer(() => { + if (onDone) onDone() + }, totalTime + 1500) + }, [addTimer, spawnRipple]) + + const runWhatIf = useCallback(() => { + runWhatIfScenarioA(() => { + if (currentViewRef.current !== 'business' || currentModeRef.current !== 'whatif') return + + runWhatIfScenarioB(() => { + // Loop back to start after error hold + if (currentViewRef.current === 'business' && currentModeRef.current === 'whatif') { + addTimer(() => { + if (currentViewRef.current === 'business' && currentModeRef.current === 'whatif') { + resetAllSvgStates() + setActiveSvg('whatif') + setTimeout(() => runWhatIf(), 0) + } + }, 500) + } + }) + }) + }, [addTimer, resetAllSvgStates, runWhatIfScenarioA, runWhatIfScenarioB]) + + // ══════════════════════════════════════════════════════ + // DEVELOPER VIEW ANIMATION + // ══════════════════════════════════════════════════════ + const stopDevAnimation = useCallback(() => { + clearDevTimers() + setRevealedLines(new Set()) + }, [clearDevTimers]) + + const startDevAnimation = useCallback(() => { + stopDevAnimation() + + const delays = [0, 200, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000, 3300] + + delays.forEach((d, i) => { + addDevTimer(() => { + setRevealedLines(prev => new Set([...prev, i])) + }, d) + }) + + addDevTimer(() => { + if (currentViewRef.current === 'developer') { + stopDevAnimation() + setTimeout(() => startDevAnimation(), 0) + } + }, 5500) + }, [addDevTimer, stopDevAnimation]) + + // ══════════════════════════════════════════════════════ + // SIMULATION CONTROLLER + // ══════════════════════════════════════════════════════ + const startSimulation = useCallback((mode: SimMode) => { + resetAllSvgStates() + clearTimers() + setActiveSvg(mode) + + // Use setTimeout(0) to ensure state updates from resetAllSvgStates have been applied + setTimeout(() => { + switch (mode) { + case 'walkthrough': + runWalkthrough() + break + case 'stepbystep': + initStepByStep() + break + case 'whatif': + runWhatIf() + break + } + }, 0) + }, [resetAllSvgStates, clearTimers, runWalkthrough, initStepByStep, runWhatIf]) + + // ══════════════════════════════════════════════════════ + // VIEW TOGGLE HANDLERS + // ══════════════════════════════════════════════════════ + const handleShowDev = useCallback(() => { + setShowDev(true) + currentViewRef.current = 'developer' + stopAllAnimations() + setTimeout(() => startDevAnimation(), 0) + }, [stopAllAnimations, startDevAnimation]) + + const handleShowBusiness = useCallback(() => { + setShowDev(false) + currentViewRef.current = 'business' + stopDevAnimation() + startSimulation(currentModeRef.current) + }, [stopDevAnimation, startSimulation]) + + // ══════════════════════════════════════════════════════ + // PILL TAB HANDLER + // ══════════════════════════════════════════════════════ + const handleTabClick = useCallback((mode: SimMode) => { + setCurrentMode(mode) + currentModeRef.current = mode + stopAllAnimations() + startSimulation(mode) + }, [stopAllAnimations, startSimulation]) + + // ══════════════════════════════════════════════════════ + // KEYBOARD SUPPORT + // ══════════════════════════════════════════════════════ + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (currentModeRef.current !== 'stepbystep' || currentViewRef.current !== 'business') return + if (e.key === 'ArrowRight' || e.key === ' ') { + e.preventDefault() + stepForward() + } else if (e.key === 'ArrowLeft') { + e.preventDefault() + stepBackward() + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [stepForward, stepBackward]) + + // ══════════════════════════════════════════════════════ + // INIT + // ══════════════════════════════════════════════════════ + useEffect(() => { + if (perspective === 'developer') { + startDevAnimation() + } else { + startSimulation('walkthrough') + } + + return () => { + // Cleanup ALL timeouts + animationTimers.current.forEach(t => clearTimeout(t)) + devAnimTimers.current.forEach(t => clearTimeout(t)) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // ══════════════════════════════════════════════════════ + // HELPERS + // ══════════════════════════════════════════════════════ + const getNodeClassName = (id: string) => { + const cls = nodeClasses[id] + return cls ? ` ${cls}` : '' + } + + const getEdgeClassName = (id: string) => { + const cls = edgeClasses[id] + return cls ? ` ${cls}` : '' + } + + const isTooltipVisible = (id: string) => !!tooltipVisible[id] + + // ══════════════════════════════════════════════════════ + // RENDER + // ══════════════════════════════════════════════════════ + return ( +
+ + {/* ===== BUSINESS VIEW ===== */} +
+
+ + Business Perspective +
+ +

Simulation & Testing

+

+ Test every path before it goes live. No technical skills needed. +

+ + {/* Pill tabs */} +
+ + + +
+ + {/* Flow diagram */} +
+
+ + + + {fileNames[currentMode]} +
+
+
+ {/* Scenario label overlay for what-if mode */} +
+ {scenarioLabelText} +
+ + {/* ══════════════════════════════════════════════════════ + SVG 1: WALK-THROUGH — Patient Intake (Healthcare) + ══════════════════════════════════════════════════════ */} + + + + + + + + + + + {/* Lane backgrounds */} + + + + + {/* Lane labels */} + RECEPTION + TRIAGE + CLINICAL + + {/* ALL edges */} + + + + + + + + + + + {/* Edge labels */} + urgent + routine + + {/* ALL nodes */} + {/* Check In (action) */} + + + ACTION + Check In + + {/* Verify Insurance (action) */} + + + ACTION + Verify Insurance + + {/* Initial Assessment (action) */} + + + ACTION + Initial Assessment + + {/* Priority Rating (switch) */} + + + SWITCH + Priority + + {/* Emergency Prep (action) */} + + + ACTION + Emergency Prep + + {/* Doctor Review (action) */} + + + ACTION + Doctor Review + + {/* Schedule Appointment (action) */} + + + ACTION + Schedule Appt + + {/* End (terminal) */} + + + End + + + {/* Ripple layer */} + + {wtRipples.map(r => ( + + ))} + + + + {/* ══════════════════════════════════════════════════════ + SVG 2: STEP-BY-STEP — Loan Application (Finance) + ══════════════════════════════════════════════════════ */} + + + + + + + + + + + {/* Lane backgrounds */} + + + + + {/* Lane labels */} + APPLICANT + UNDERWRITING + OPERATIONS + + {/* ALL edges */} + + + + + + + + + + + + + + + + {/* Edge labels */} + approved + denied + review + + {/* ALL nodes */} + {/* Submit Application (action) */} + + + ACTION + Submit Application + + {/* Upload Documents (action) */} + + + ACTION + Upload Documents + + {/* Credit Check (action) */} + + + ACTION + Credit Check + + {/* Risk Assessment (switch) */} + + + SWITCH + Risk + + {/* Generate Offer (action) */} + + + ACTION + Generate Offer + + {/* Disburse Funds (action) */} + + + ACTION + Disburse Funds + + {/* Send Rejection (action) */} + + + ACTION + Send Rejection + + {/* Manual Review (action) */} + + + ACTION + Manual Review + + {/* End (terminal) */} + + + End + + + {/* Ripple layer */} + + {sbRipples.map(r => ( + + ))} + + + {/* Data tooltips for step-by-step mode */} + + + In: {`{ applicant: "Jane Doe" }`} + Out: {`{ appId: "LN-8834" }`} + + + + In: {`{ docs: ["W2", "bank_stmt"] }`} + Out: {`{ verified: true, score: 0.94 }`} + + + + In: {`{ ssn: "***-**-4821" }`} + Out: {`{ creditScore: 742, tier: "A" }`} + + + + In: {`{ score: 742, amount: $85K }`} + Out: {`{ decision: "approved" }`} + + + + In: {`{ rate: 5.2%, term: 30yr }`} + Out: {`{ offerId: "OFF-1192" }`} + + + + In: {`{ amount: $85,000 }`} + Out: {`{ txId: "ACH-5510", ok: true }`} + + + + In: {`{ reason: "DTI > 43%" }`} + Out: {`{ letter: "REJ-4410" }`} + + + + In: {`{ flag: "income_mismatch" }`} + Out: {`{ override: "approve" }`} + + + + status: disbursed + + + + {/* ══════════════════════════════════════════════════════ + SVG 3: WHAT-IF — Insurance Claim + ══════════════════════════════════════════════════════ */} + + + + + + + + + + + + + + {/* Lane backgrounds */} + + + + + {/* Lane labels */} + CLAIMANT + ADJUSTER + SETTLEMENT + + {/* ALL edges */} + + + + + + + + + + {/* Edge labels */} + approve + investigate + + {/* ALL nodes */} + {/* File Claim (action) */} + + + ACTION + File Claim + + {/* Submit Evidence (action) */} + + + ACTION + Submit Evidence + + {/* Review Claim (action) */} + + + ACTION + Review Claim + + {/* Assess Damage (switch) */} + + + SWITCH + Assess + + {/* Calculate Payout (action) */} + + + ACTION + Calculate Payout + + {/* Issue Payment (action) */} + + + ACTION + Issue Payment + + {/* End (terminal) */} + + + End + + {/* Fraud Check (action) — investigate path, hidden by default */} + + + ACTION + Fraud Check + + + {/* Ripple layer */} + + {wiRipples.map(r => ( + + ))} + + + {/* Error tooltip for what-if mode */} + + + {'\u26A0'} Alert: Suspicious pattern — duplicate claim #CLM-7829 + + + +
+ + {/* Step control bar */} +
+ + + {stepIndex < 0 ? 'Ready' : `Step ${stepIndex + 1} of ${stepByStepPath.nodes.length}`} + + +
+ +
+
+ {statusText} + + {currentMode === 'walkthrough' ? 'walk-through' : currentMode === 'stepbystep' ? 'step-by-step' : 'what-if'} + +
+
+ + +
+ + {/* ===== DEVELOPER VIEW ===== */} +
+
+ + Developer Perspective +
+ +

Simulation & Testing

+

+ Debug flows locally. Run in CI. Catch structural issues before production. +

+ +
+
+
+ +
+ terminal +
+
+
+ $ flowprint run patient-intake.flowprint.yaml +
+
 
+
+ {'\u2714'} check_in 0ms +
+
+ {'\u2714'} verify_insurance 12ms +
+
+ {'\u2714'} initial_assessment 8ms +
+
+ {'\u2714'} priority_rating 3ms  {'\u2192'} urgent +
+
+ {'\u2714'} emergency_prep 22ms +
+
+ {'\u2714'} doctor_review 15ms +
+
+ {'\u2714'} end 0ms +
+
 
+
+ 7/7 nodes passed {'\u00B7'} 60ms total +
+
+ +
+
+
+ + +
+ +
+ ) +} diff --git a/packages/homepage/components/bridges/bridge-versioning.css b/packages/homepage/components/bridges/bridge-versioning.css new file mode 100644 index 0000000..2da977b --- /dev/null +++ b/packages/homepage/components/bridges/bridge-versioning.css @@ -0,0 +1,380 @@ +/* Bridge Versioning — ported 1:1 from prototype-bridge-versioning.html */ + +/* ══════════════════════════════════════════════════════ + CARD + ══════════════════════════════════════════════════════ */ +.bridge-versioning .card { + position: relative; + width: 780px; + border-radius: 20px; + background: var(--surface); + border: 1px solid var(--surface-border); + box-shadow: + 0 4px 24px rgba(0, 0, 0, 0.5), + 0 0 80px var(--accent-glow), + inset 0 1px 0 rgba(255, 255, 255, 0.04); + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* ══════════════════════════════════════════════════════ + HEADER + ══════════════════════════════════════════════════════ */ +.bridge-versioning .header { + padding: 28px 32px 0; + position: relative; + z-index: 10; +} + +.bridge-versioning .badge { + display: inline-block; + padding: 4px 10px; + border-radius: 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 500; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 12px; + transition: all 0.4s ease; + background: rgba(228, 70, 255, 0.08); + color: var(--accent); + border: 1px solid var(--accent-dim); +} + +.bridge-versioning .badge-dot { + display: none; +} + +.bridge-versioning .title { + font-family: 'Instrument Serif', serif; + font-size: 32px; + font-weight: 400; + color: var(--text-primary); + line-height: 1.1; + margin: 0 0 6px; +} + +.bridge-versioning .description { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.5; + max-width: 500px; + margin: 0; +} + +/* Flow container */ +.bridge-versioning .flow-container { + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.bridge-versioning .flow-svg { + width: 716px; + height: 420px; +} + +/* Lane labels */ +.bridge-versioning .lane-label { + font-family: 'JetBrains Mono', monospace; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + fill: var(--text-muted); +} + +/* Lane separators */ +.bridge-versioning .lane-sep { + stroke: var(--node-stroke); + stroke-width: 1; + stroke-dasharray: 4 4; + opacity: 0.4; +} + +/* Nodes */ +.bridge-versioning .node-group { + transition: transform 0.9s cubic-bezier(0.23, 1, 0.32, 1), + opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); +} + +.bridge-versioning .node-rect { + rx: 6; + ry: 6; + fill: var(--node-fill); + stroke: var(--node-stroke); + stroke-width: 1.2; + transition: stroke 0.5s, filter 0.5s, fill 0.5s; +} + +.bridge-versioning .node-label { + font-family: 'DM Sans', sans-serif; + font-size: 9.5px; + font-weight: 500; + fill: #f0e6df; + text-anchor: middle; + dominant-baseline: central; + pointer-events: none; + transition: opacity 0.4s; +} + +.bridge-versioning .node-type-badge { + font-family: 'JetBrains Mono', monospace; + font-size: 5.5px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + text-anchor: middle; + dominant-baseline: central; + pointer-events: none; + opacity: 0.6; +} + +/* Terminal circle nodes */ +.bridge-versioning .node-circle { + fill: var(--node-fill); + stroke: var(--node-type-terminal); + stroke-width: 1.2; + transition: stroke 0.5s, filter 0.5s; +} + +/* Edges */ +.bridge-versioning .edge { + fill: none; + stroke: #6b4d40; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; + marker-end: url(#bv-arrowhead); + transition: d 0.9s cubic-bezier(0.23, 1, 0.32, 1), + opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1), + stroke 0.5s; +} + +/* Glow effects for added/changed/removed nodes */ +.bridge-versioning .node-added .node-rect { + stroke: var(--node-type-terminal); + filter: drop-shadow(0 0 8px var(--green-glow)) drop-shadow(0 0 16px var(--green-glow)); +} + +.bridge-versioning .node-changed .node-rect { + stroke: var(--node-type-action); + filter: drop-shadow(0 0 8px var(--amber-glow)) drop-shadow(0 0 16px var(--amber-glow)); +} + +.bridge-versioning .node-entering { + opacity: 0; + transform: scale(0.5); + transform-origin: center; +} +.bridge-versioning .node-entering.visible { + opacity: 1; + transform: scale(1); +} + +.bridge-versioning .node-hidden { + opacity: 0; + transform: scale(0.5); + transform-origin: center; +} + +.bridge-versioning .edge-entering { + opacity: 0; +} +.bridge-versioning .edge-entering.visible { + opacity: 1; +} + +.bridge-versioning .edge-hidden { + opacity: 0; +} + +/* Change indicator badge */ +.bridge-versioning .change-badge { + font-family: 'JetBrains Mono', monospace; + font-size: 7px; + font-weight: 500; + text-anchor: middle; + dominant-baseline: central; + opacity: 0; + transition: opacity 0.5s; +} +.bridge-versioning .show-changes .change-badge { + opacity: 1; +} + +/* ═══════════════════════════════════════ + DEVELOPER VIEW: Diff + CI + ═══════════════════════════════════════ */ +.bridge-versioning .dev-content { + display: flex; + flex-direction: column; + gap: 14px; +} + +.bridge-versioning .dev-section { + background: var(--code-bg); + border: 1px solid rgba(61, 42, 34, 0.6); + border-radius: 10px; + overflow: hidden; +} + +.bridge-versioning .dev-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: #221510; + border-bottom: 1px solid rgba(61, 42, 34, 0.4); + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 500; + color: var(--text-muted); + letter-spacing: 0.03em; +} +.bridge-versioning .dev-section-header .icon { + font-size: 12px; +} + +.bridge-versioning .diff-section { + flex: 1; + min-height: 0; +} + +.bridge-versioning .ci-section { + flex: 0 0 auto; +} + +/* Git diff styling */ +.bridge-versioning .diff-body { + padding: 10px 0; + overflow-y: auto; + max-height: 260px; +} + +.bridge-versioning .diff-line { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + line-height: 1.7; + padding: 0 14px; + white-space: pre; + color: #a89585; +} + +.bridge-versioning .diff-line.file-header { + color: #f0e6df; + font-weight: 500; + padding-bottom: 4px; +} + +.bridge-versioning .diff-line.added { + background: rgba(63, 220, 119, 0.08); + color: #5ae07a; +} +.bridge-versioning .diff-line .plus { + color: #5ae07a; +} + +.bridge-versioning .diff-line.removed { + background: rgba(255, 107, 107, 0.08); + color: #e05a5a; +} +.bridge-versioning .diff-line .minus { + color: #e05a5a; +} + +.bridge-versioning .diff-line.context { + color: var(--text-muted); +} + +.bridge-versioning .diff-line .key { + color: #7aafff; +} +.bridge-versioning .diff-line .str { + color: #ffb86c; +} + +/* CI output styling */ +.bridge-versioning .ci-body { + padding: 10px 14px; +} + +.bridge-versioning .ci-line { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + line-height: 1.8; + display: flex; + align-items: center; + gap: 8px; +} + +.bridge-versioning .ci-check { + color: #5ae07a; +} + +.bridge-versioning .ci-text { + color: #a89585; +} + +.bridge-versioning .ci-highlight { + color: #f0e6df; + font-weight: 500; +} + +/* State label */ +.bridge-versioning .state-label { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + font-family: 'JetBrains Mono', monospace; + font-size: 9px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + opacity: 0.6; + transition: opacity 0.4s; + pointer-events: none; +} + +/* View transitions */ +.bridge-versioning .views { + position: relative; + overflow: hidden; + width: 100%; + transition: height 0.5s cubic-bezier(0.23, 1, 0.32, 1); +} + +.bridge-versioning .view { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 16px 0 24px; + transition: opacity 0.5s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.5s cubic-bezier(0.23, 1, 0.32, 1); +} + +.bridge-versioning .view--business { + opacity: 1; + transform: translateY(0); +} +.bridge-versioning .view--developer { + opacity: 0; + transform: translateY(12px); + pointer-events: none; +} + +.bridge-versioning.developer .view--business { + opacity: 0; + transform: translateY(-12px); + pointer-events: none; +} +.bridge-versioning.developer .view--developer { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} diff --git a/packages/homepage/components/bridges/bridge-versioning.tsx b/packages/homepage/components/bridges/bridge-versioning.tsx new file mode 100644 index 0000000..54eee1e --- /dev/null +++ b/packages/homepage/components/bridges/bridge-versioning.tsx @@ -0,0 +1,406 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import '../flow/flow.css' +import './bridge-versioning.css' + +interface BridgeVersioningProps { + perspective: 'business' | 'developer' +} + +// Layout positions for nodes in "before" and "after" states +// SVG viewBox is 716 x 420 +// Lanes: customer 0-140, routing 140-280, fulfillment 280-420 +// Lane centers: customer 70, routing 210, fulfillment 350 + +interface NodePos { + x: number + y: number + hidden?: boolean +} + +interface LayoutState { + start: NodePos + submit: NodePos + validate: NodePos + approve: NodePos + route: NodePos + ship: NodePos + notify: NodePos + end: NodePos + shipLabel: string +} + +const BEFORE: LayoutState = { + start: { x: 70, y: 70 }, + submit: { x: 190, y: 70 }, + validate: { x: 330, y: 210 }, + approve: { x: 330, y: 210, hidden: true }, // hidden, same spot as validate + route: { x: 460, y: 210 }, + ship: { x: 560, y: 350 }, + notify: { x: 560, y: 350, hidden: true }, // hidden, same spot as ship + end: { x: 660, y: 350 }, + shipLabel: 'Ship Standard', +} + +const AFTER: LayoutState = { + start: { x: 60, y: 70 }, + submit: { x: 160, y: 70 }, + validate: { x: 270, y: 210 }, + approve: { x: 380, y: 210, hidden: false }, + route: { x: 490, y: 210 }, + ship: { x: 540, y: 350 }, + notify: { x: 630, y: 350, hidden: false }, + end: { x: 690, y: 350 }, + shipLabel: 'Ship Priority', +} + +// Edge path helper +function edgePath( + fromX: number, + fromY: number, + toX: number, + toY: number, + fromR: number, + toR: number, +): string { + const startX = fromX + fromR + const endX = toX - toR + + // If same row, draw straight + if (Math.abs(fromY - toY) < 5) { + return `M ${startX} ${fromY} L ${endX} ${toY}` + } + + // Otherwise draw a smooth curve + const midX = (startX + endX) / 2 + return `M ${startX} ${fromY} C ${midX} ${fromY}, ${midX} ${toY}, ${endX} ${toY}` +} + +function computeEdges(s: LayoutState, isAfter: boolean) { + const startSubmit = edgePath(s.start.x, s.start.y, s.submit.x, s.submit.y, 13, 38) + const submitValidate = edgePath(s.submit.x, s.submit.y, s.validate.x, s.validate.y, 38, 38) + const routeShip = edgePath(s.route.x, s.route.y, s.ship.x, s.ship.y, 40, 43) + + let validateApprove: string + let toRoute: string + let shipNotify: string + let toEnd: string + + if (isAfter) { + validateApprove = edgePath(s.validate.x, s.validate.y, s.approve.x, s.approve.y, 38, 38) + toRoute = edgePath(s.approve.x, s.approve.y, s.route.x, s.route.y, 38, 40) + shipNotify = edgePath(s.ship.x, s.ship.y, s.notify.x, s.notify.y, 43, 38) + toEnd = edgePath(s.notify.x, s.notify.y, s.end.x, s.end.y, 38, 13) + } else { + // When hidden, compute paths from same positions so transitions animate smoothly + validateApprove = edgePath(s.validate.x, s.validate.y, s.approve.x, s.approve.y, 38, 38) + toRoute = edgePath(s.validate.x, s.validate.y, s.route.x, s.route.y, 38, 40) + shipNotify = edgePath(s.ship.x, s.ship.y, s.notify.x, s.notify.y, 43, 38) + toEnd = edgePath(s.ship.x, s.ship.y, s.end.x, s.end.y, 43, 13) + } + + return { startSubmit, submitValidate, validateApprove, toRoute, routeShip, shipNotify, toEnd } +} + +function BusinessViewContent({ isAfterState }: { isAfterState: boolean }) { + const state = isAfterState ? AFTER : BEFORE + const edges = computeEdges(state, isAfterState) + + const approveClasses = [ + 'node-group', + state.approve?.hidden ? 'node-entering' : 'visible', + !state.approve?.hidden ? 'node-added' : '', + ] + .filter(Boolean) + .join(' ') + + const notifyClasses = [ + 'node-group', + state.notify?.hidden ? 'node-entering' : 'visible', + !state.notify?.hidden ? 'node-added' : '', + ] + .filter(Boolean) + .join(' ') + + const shipClasses = ['node-group', isAfterState ? 'node-changed' : ''].filter(Boolean).join(' ') + + const svgClasses = ['flow-svg', isAfterState ? 'show-changes' : ''].filter(Boolean).join(' ') + + return ( + <> +
+ + + + + + + + + + + {/* Lane backgrounds */} + + + + + {/* Lane labels */} + Customer + Routing + Fulfillment + + {/* Lane separators */} + + + + {/* EDGES (drawn first, behind nodes) */} + {/* Start -> Submit */} + + {/* Submit -> Validate */} + + {/* Validate -> Approve (after state only) */} + + {/* Validate -> Route / Approve -> Route */} + + {/* Route -> Ship */} + + {/* Ship -> Notify (after state only) */} + + {/* Ship/Notify -> End */} + + + {/* NODES */} + {/* Start (terminal) */} + + + Start + + + {/* Submit (action) */} + + + ACTION + Submit + + + {/* Validate (action) */} + + + ACTION + Validate + + + {/* Approve (action, new in "after") */} + + + ACTION + Approve + + NEW + + + {/* Route (switch) */} + + + SWITCH + Route Order + + + {/* Ship (action, label changes) */} + + + ACTION + {state.shipLabel} + ~ CHANGED + + + {/* Notify (action, new in "after") */} + + + ACTION + Notify + + NEW + + + {/* End (terminal) */} + + + End + + +
+
{isAfterState ? 'After' : 'Before'}
+ + ) +} + +function DeveloperViewContent() { + return ( + <> +
+
+
+ 📝 + Git Diff + + order-fulfillment.flowprint.yaml + +
+
+
{' '}validate:
+
-{' '}next: route_order
+
+{' '}next: approve
+
+{' '}approve:
+
+{' '}type: action
+
+{' '}lane: routing
+
+{' '}next: route_order
+
{' '}route_order:
+
{' '}type: switch
+
{' '}ship_standard:
+
-{' '}label: Ship Standard
+
+{' '}label: Ship Priority
+
+{' '}next: notify
+
+{' '}notify:
+
+{' '}type: action
+
+{' '}lane: fulfillment
+
+
+ +
+
+ + CI Validation + All checks passed +
+
+
+ + Schema valid (flowprint/1.0) +
+
+ + No dangling refs +
+
+ + No cycles detected +
+
+ + Deterministic key order +
+
+ + + 8 nodes validated (was{' '} + 6) + +
+
+
+
+ + ) +} + +export function BridgeVersioning({ perspective }: BridgeVersioningProps) { + const [isAfterState, setIsAfterState] = useState(false) + const timerRef = useRef | null>(null) + const isFirstRender = useRef(true) + + // Dynamic height refs + const viewsRef = useRef(null) + const bizViewRef = useRef(null) + const devViewRef = useRef(null) + const initialRenderRef = useRef(true) + + const isDev = perspective === 'developer' + + // Dynamic height: measure active view and set container height + useEffect(() => { + const activeView = isDev ? devViewRef.current : bizViewRef.current + if (!activeView || !viewsRef.current) return + const h = activeView.scrollHeight + if (initialRenderRef.current) { + viewsRef.current.style.transition = 'none' + viewsRef.current.style.height = `${h}px` + requestAnimationFrame(() => { + if (viewsRef.current) viewsRef.current.style.transition = '' + }) + initialRenderRef.current = false + } else { + viewsRef.current.style.height = `${h}px` + } + }, [isDev]) + + // Morph loop: schedule next state flip as a reaction to current state. + // No side effects inside setState updaters — React Strict Mode calls them twice. + useEffect(() => { + // First render: 2s initial delay. Subsequent: 3s for "after", 2.5s for "before". + const delay = isFirstRender.current ? 2000 : (isAfterState ? 3000 : 2500) + isFirstRender.current = false + + timerRef.current = setTimeout(() => { + setIsAfterState((prev) => !prev) + }, delay) + + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current) + } + } + }, [isAfterState]) + + const wrapperClasses = [ + 'bridge-versioning', + perspective === 'developer' ? 'developer' : '', + ] + .filter(Boolean) + .join(' ') + + return ( +
+
+ {/* Header */} +
+
+ + + {isDev ? 'Developer Perspective' : 'Business Perspective'} + +
+

Version Control

+

+ {isDev + ? 'YAML diffs cleanly. Validate in CI. Catch issues in PR review.' + : 'See exactly what changed, visually. No YAML to read.'} +

+
+ + {/* Views */} +
+
+ +
+
+ +
+
+
+
+ ) +} diff --git a/packages/homepage/components/bridges/overview-parallax.css b/packages/homepage/components/bridges/overview-parallax.css new file mode 100644 index 0000000..751de6f --- /dev/null +++ b/packages/homepage/components/bridges/overview-parallax.css @@ -0,0 +1,684 @@ +/* ══════════════════════════════════════════════════════ + OVERVIEW PARALLAX — scoped styles + Ported 1:1 from prototype-consolidated.html + ══════════════════════════════════════════════════════ */ + +/* ── Root container — centers the 3D scene like the prototype body ── */ +.overview-parallax { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 1000px; + margin: 0 auto; + background: #110904; + border: 1px solid rgba(228, 70, 255, 0.12); + border-radius: 16px; + padding: 48px; + position: relative; +} + +/* ── Ambient background shapes ── */ +.overview-parallax .ambient { + position: fixed; + border-radius: 50%; + pointer-events: none; + filter: blur(120px); + opacity: 0.3; +} + +.overview-parallax .ambient--1 { + width: 400px; + height: 400px; + background: rgba(228, 70, 255, 0.08); + top: -100px; + right: -100px; +} + +.overview-parallax .ambient--2 { + width: 300px; + height: 300px; + background: rgba(228, 70, 255, 0.05); + bottom: -80px; + left: -60px; +} + +/* ── Scene + 3D card shell ── */ +.overview-parallax .scene { + width: 780px; + height: 720px; + perspective: 1400px; + perspective-origin: 50% 50%; + cursor: default; +} + +.overview-parallax .card { + position: relative; + width: 100%; + height: 100%; + transform-style: preserve-3d; + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); + will-change: transform; +} + +/* ── Shared layer base ── */ +.overview-parallax .layer { + position: absolute; + inset: 0; + border-radius: 20px; + padding: 32px 32px 28px; + backface-visibility: hidden; + overflow: hidden; +} + +/* ── Front layer (Business) ── */ +.overview-parallax .layer--front { + z-index: 3; + background: var(--surface, #1a0f0a); + border: 1px solid var(--surface-border, rgba(228, 70, 255, 0.12)); + transform: translateZ(60px); + transition: opacity 0.6s cubic-bezier(0.23, 1, 0.32, 1), + filter 0.6s cubic-bezier(0.23, 1, 0.32, 1); + box-shadow: + 0 4px 24px rgba(0, 0, 0, 0.5), + 0 0 80px var(--accent-glow, rgba(228, 70, 255, 0.15)), + inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +.overview-parallax .card.state-peeked .layer--front { + opacity: 0.15; + filter: blur(3px); + pointer-events: none; +} + +.overview-parallax .card.state-developer .layer--front { + opacity: 0; + filter: blur(4px); + pointer-events: none; +} + +/* ── Separator plane (Flowprint Engine badge) ── */ +.overview-parallax .layer--separator { + z-index: 2; + transform: translateZ(0px); + background: transparent; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + opacity: 0; + transition: opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); + border: none; + padding: 0; +} + +.overview-parallax .card.state-peeked .layer--separator { + opacity: 1; + transition-delay: 0.15s; +} + +.overview-parallax .card.state-developer .layer--separator { + opacity: 0; + transition-delay: 0s; +} + +.overview-parallax .separator-content { + position: relative; + display: flex; + align-items: center; + gap: 10px; + padding: 16px 40px; + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent, #E446FF); + border: 1px solid var(--accent-dim, rgba(228, 70, 255, 0.35)); + border-radius: 10px; + background: rgba(17, 9, 4, 0.92); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + box-shadow: + 0 0 40px rgba(228, 70, 255, 0.25), + 0 0 80px rgba(228, 70, 255, 0.1), + inset 0 0 20px rgba(228, 70, 255, 0.06); + text-shadow: 0 0 20px rgba(228, 70, 255, 0.5); + transform: scale(0.8); + transition: opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1), + transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); +} + +.overview-parallax .separator-content .engine-icon { + display: flex; + align-items: center; + justify-content: center; + filter: drop-shadow(0 0 6px rgba(228, 70, 255, 0.6)); + animation: overview-parallax-engine-icon-glow 2s ease-in-out infinite alternate; +} + +@keyframes overview-parallax-engine-icon-glow { + 0% { filter: drop-shadow(0 0 4px rgba(228, 70, 255, 0.4)); } + 100% { filter: drop-shadow(0 0 10px rgba(228, 70, 255, 0.8)); } +} + +.overview-parallax .card.state-peeked .separator-content { + transform: scale(1); +} + +.overview-parallax .card.state-developer .separator-content { + transform: scale(1.1); +} + +/* Separator extending lines */ +.overview-parallax .separator-content::before, +.overview-parallax .separator-content::after { + content: ''; + position: absolute; + top: 50%; + width: 80px; + height: 1px; +} + +.overview-parallax .separator-content::before { + right: 100%; + margin-right: 16px; + background: linear-gradient(90deg, transparent, var(--accent-dim, rgba(228, 70, 255, 0.35))); +} + +.overview-parallax .separator-content::after { + left: 100%; + margin-left: 16px; + background: linear-gradient(270deg, transparent, var(--accent-dim, rgba(228, 70, 255, 0.35))); +} + +/* Pulse ring on separator */ +.overview-parallax .separator-content .pulse-ring { + position: absolute; + inset: -4px; + border-radius: 14px; + border: 1.5px solid var(--accent, #E446FF); + opacity: 0; + animation: overview-parallax-separator-pulse 2.5s ease-in-out infinite; + pointer-events: none; +} + +@keyframes overview-parallax-separator-pulse { + 0%, 100% { opacity: 0; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(1.04); } +} + +/* ── Back layer (Developer) ── */ +.overview-parallax .layer--back { + z-index: 1; + transform: translateZ(-60px); + background: var(--surface, #1a0f0a); + border: 1px solid var(--surface-border, rgba(228, 70, 255, 0.12)); + filter: blur(6px); + opacity: 0.3; + transition: opacity 0.6s cubic-bezier(0.23, 1, 0.32, 1), + filter 0.6s cubic-bezier(0.23, 1, 0.32, 1); + box-shadow: + 0 4px 24px rgba(0, 0, 0, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.02); + /* Flexbox layout so CTA button is never clipped below the toolchain grid */ + display: flex; + flex-direction: column; +} + +.overview-parallax .card.state-peeked .layer--back { + opacity: 0.55; + filter: blur(2px); +} + +.overview-parallax .card.state-developer .layer--back { + opacity: 1; + filter: blur(0); + transform: translateZ(0px); + z-index: 5; +} + +/* ── Layer badge (perspective label) ── */ +.overview-parallax .layer-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + border-radius: 100px; + margin-bottom: 16px; +} + +.overview-parallax .layer-badge--business { + color: var(--accent, #E446FF); + background: rgba(228, 70, 255, 0.1); + border: 1px solid rgba(228, 70, 255, 0.2); +} + +.overview-parallax .layer-badge--developer { + color: var(--type-teal, #5ae0c2); + background: rgba(90, 224, 194, 0.1); + border: 1px solid rgba(90, 224, 194, 0.2); +} + +.overview-parallax .layer-badge .dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.overview-parallax .layer-badge--business .dot { background: var(--accent, #E446FF); } +.overview-parallax .layer-badge--developer .dot { background: var(--type-teal, #5ae0c2); } + +/* ── Typography ── */ +.overview-parallax .card-title { + font-family: 'Instrument Serif', Georgia, serif; + font-size: 32px; + font-weight: 400; + font-style: normal; + letter-spacing: -0.01em; + line-height: 1.15; + margin-bottom: 10px; +} + +.overview-parallax .card-description { + font-size: 13.5px; + font-weight: 400; + line-height: 1.6; + color: var(--text-secondary, #a89585); + margin-bottom: 20px; +} + +/* ── CTA button ── */ +.overview-parallax .cta { + display: inline-flex; + align-items: center; + gap: 10px; + margin-top: 16px; + padding: 11px 22px; + font-family: 'Space Grotesk', system-ui, sans-serif; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--text-primary, #f0e6df); + background: linear-gradient(135deg, rgba(228, 70, 255, 0.15), rgba(228, 70, 255, 0.05)); + border: 1px solid rgba(228, 70, 255, 0.25); + border-radius: 10px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + z-index: 10; +} + +.overview-parallax .cta::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(228, 70, 255, 0.1), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + +.overview-parallax .cta:hover::before { opacity: 1; } + +.overview-parallax .cta:hover { + border-color: rgba(228, 70, 255, 0.45); + box-shadow: 0 0 30px rgba(228, 70, 255, 0.15); +} + +.overview-parallax .cta-icon { + display: inline-flex; + transition: transform 0.4s cubic-bezier(0.23, 1, 0.32, 1); +} + +/* ── Flow diagram container with editor header ── */ +.overview-parallax .flow-container { + border: 1px solid rgba(228, 70, 255, 0.08); + border-radius: 12px; + overflow: hidden; + background: var(--code-bg, #0d0805); + margin-top: 4px; +} + +.overview-parallax .flow-panel-header { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: var(--table-header, #2a1a12); + border-bottom: 1px solid rgba(228, 70, 255, 0.08); + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + color: var(--text-muted, #6b5a4d); + letter-spacing: 0.02em; +} + +.overview-parallax .flow-panel-header svg { + width: 12px; + height: 12px; + opacity: 0.6; +} + +.overview-parallax .flow-panel-header .header-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent, #E446FF); + opacity: 0.4; + margin-left: auto; + box-shadow: 0 0 6px rgba(228, 70, 255, 0.3); +} + +.overview-parallax .flow-panel-body { + padding: 8px 4px 4px; +} + +/* ── SVG flow diagram — swimlane styling ── */ +.overview-parallax .flow-svg .lane-group { + cursor: default; + filter: drop-shadow(0 0 0 transparent); + transition: filter 0.15s ease; +} + +.overview-parallax .flow-svg .lane-bg-rect { + fill: rgba(228, 70, 255, 0.03); + stroke: rgba(228, 70, 255, 0.08); + stroke-width: 0.5; + transition: fill 0.15s ease, stroke 0.15s ease, stroke-width 0.15s ease; +} + +.overview-parallax .flow-svg .lane-inner-glow { + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; +} + +.overview-parallax .lane-customer .lane-bg-rect { fill: rgba(228, 70, 255, 0.03); stroke: rgba(228, 70, 255, 0.08); } +.overview-parallax .lane-routing .lane-bg-rect { fill: rgba(228, 70, 255, 0.02); stroke: rgba(228, 70, 255, 0.06); } +.overview-parallax .lane-fulfillment .lane-bg-rect { fill: rgba(228, 70, 255, 0.015); stroke: rgba(228, 70, 255, 0.05); } + +.overview-parallax .flow-svg .lane-label { + font-family: 'JetBrains Mono', monospace; + font-size: 8px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + fill: rgba(160, 139, 122, 0.5); + transition: fill 0.15s ease; +} + +/* ── Nodes ── */ +.overview-parallax .flow-svg .node-group { + cursor: default; +} + +.overview-parallax .flow-svg .node-rect { + rx: 6; + ry: 6; + fill: #2a1a14; + stroke: #3d2a22; + stroke-width: 1.2; + transition: fill 0.15s ease, stroke 0.15s ease; +} + +.overview-parallax .flow-svg .node-label { + font-family: 'DM Sans', sans-serif; + font-size: 9.5px; + font-weight: 500; + fill: #f0e6df; + text-anchor: middle; + dominant-baseline: central; + pointer-events: none; + transition: fill 0.15s ease; +} + +.overview-parallax .flow-svg .node-type-badge { + font-family: 'JetBrains Mono', monospace; + font-size: 5.5px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + text-anchor: middle; + dominant-baseline: central; + opacity: 0.6; + pointer-events: none; +} + +.overview-parallax .flow-svg .node-circle { + fill: #2a1a14; + stroke: #3FDC77; + stroke-width: 1.2; + transition: fill 0.15s ease, stroke 0.15s ease; +} + +.overview-parallax .flow-svg .edge { + fill: none; + stroke: #6b4d40; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; + transition: stroke 0.15s ease; +} + +.overview-parallax .flow-svg .edge-label { + font-family: 'JetBrains Mono', monospace; + font-size: 7px; + font-weight: 500; + fill: var(--text-muted, #6b5a4d); + opacity: 0.7; + transition: fill 0.15s ease; +} + +/* ── Swimlane hover states ── */ + +/* Customer lane hover */ +.overview-parallax .lane-customer:hover { + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5)) + drop-shadow(0 0 20px rgba(255, 107, 107, 0.08)); +} +.overview-parallax .lane-customer:hover .lane-bg-rect { + fill: url(#grad-customer-hover); + stroke: rgba(255, 107, 107, 0.20); + stroke-width: 1; +} +.overview-parallax .lane-customer:hover .lane-inner-glow { opacity: 1; } +.overview-parallax .lane-customer:hover .lane-label { fill: var(--lane-customer, #FF6B6B); } + +/* Routing lane hover */ +.overview-parallax .lane-routing:hover { + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5)) + drop-shadow(0 0 20px rgba(228, 70, 255, 0.08)); +} +.overview-parallax .lane-routing:hover .lane-bg-rect { + fill: url(#grad-routing-hover); + stroke: rgba(228, 70, 255, 0.20); + stroke-width: 1; +} +.overview-parallax .lane-routing:hover .lane-inner-glow { opacity: 1; } +.overview-parallax .lane-routing:hover .lane-label { fill: var(--lane-routing, #E446FF); } +.overview-parallax .lane-routing:hover .edge { stroke: rgba(228, 70, 255, 0.35); } +.overview-parallax .lane-routing:hover .edge-label { fill: rgba(228, 70, 255, 0.6); } + +/* Fulfillment lane hover */ +.overview-parallax .lane-fulfillment:hover { + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5)) + drop-shadow(0 0 20px rgba(6, 182, 212, 0.08)); +} +.overview-parallax .lane-fulfillment:hover .lane-bg-rect { + fill: url(#grad-fulfillment-hover); + stroke: rgba(6, 182, 212, 0.20); + stroke-width: 1; +} +.overview-parallax .lane-fulfillment:hover .lane-inner-glow { opacity: 1; } +.overview-parallax .lane-fulfillment:hover .lane-label { fill: var(--lane-fulfillment, #06B6D4); } + +/* Node-type coloring on ANY lane hover */ +.overview-parallax .lane-group:hover .node-type-terminal .node-circle { + fill: rgba(63, 220, 119, 0.15); + stroke: var(--node-type-terminal, #3FDC77); +} +.overview-parallax .lane-group:hover .node-type-terminal .node-label { fill: var(--node-type-terminal, #3FDC77); } + +.overview-parallax .lane-group:hover .node-type-action .node-rect { + fill: rgba(255, 146, 67, 0.15); + stroke: var(--node-type-action, #FF9243); +} +.overview-parallax .lane-group:hover .node-type-action .node-label { fill: var(--node-type-action, #FF9243); } + +.overview-parallax .lane-group:hover .node-type-switch .node-rect { + fill: rgba(163, 116, 255, 0.15); + stroke: var(--node-type-switch, #A374FF); +} +.overview-parallax .lane-group:hover .node-type-switch .node-label { fill: var(--node-type-switch, #A374FF); } + +/* ── Developer toolchain grid ── */ +.overview-parallax .toolchain-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 12px; + flex: 1; + min-height: 0; +} + +.overview-parallax .tool-card { + background: var(--code-bg, #0d0805); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 10px; + padding: 14px; + display: flex; + flex-direction: column; + gap: 8px; + overflow: hidden; + transition: border-color 0.3s ease; +} + +.overview-parallax .tool-card:hover { + border-color: rgba(228, 70, 255, 0.15); +} + +.overview-parallax .tool-card-header { + display: flex; + align-items: center; + gap: 8px; +} + +.overview-parallax .tool-card-icon { + width: 28px; + height: 28px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.overview-parallax .tool-card-icon svg { + width: 16px; + height: 16px; +} + +.overview-parallax .tool-card-title { + font-size: 12px; + font-weight: 600; + letter-spacing: 0.01em; + color: var(--text-primary, #f0e6df); + white-space: nowrap; +} + +.overview-parallax .tool-card-body { + flex: 1; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + line-height: 1.6; + color: var(--text-muted, #6b5a4d); + overflow: hidden; +} + +/* Card-specific icon styles */ +.overview-parallax .tool-card--ci .tool-card-icon { background: rgba(90, 224, 122, 0.12); } +.overview-parallax .tool-card--ci .tool-card-icon svg { color: var(--true-green, #5ae07a); } + +.overview-parallax .tool-card--yaml .tool-card-icon { background: rgba(228, 70, 255, 0.12); } +.overview-parallax .tool-card--yaml .tool-card-icon svg { color: var(--accent, #E446FF); } + +.overview-parallax .tool-card--npm .tool-card-icon { background: rgba(228, 70, 255, 0.12); } +.overview-parallax .tool-card--npm .tool-card-icon svg { color: #CB3837; } + +.overview-parallax .tool-card--codegen .tool-card-icon { background: rgba(122, 175, 255, 0.12); } +.overview-parallax .tool-card--codegen .tool-card-icon svg { color: var(--keyword-blue, #7aafff); } + +.overview-parallax .tool-card--git .tool-card-icon { background: rgba(255, 184, 108, 0.12); } +.overview-parallax .tool-card--git .tool-card-icon svg { color: var(--string-amber, #ffb86c); } + +.overview-parallax .tool-card--schema .tool-card-icon { background: rgba(90, 224, 194, 0.12); } +.overview-parallax .tool-card--schema .tool-card-icon svg { color: var(--type-teal, #5ae0c2); } + +/* CI output lines */ +.overview-parallax .ci-line { display: flex; align-items: center; gap: 5px; font-size: 9.5px; line-height: 1.8; } +.overview-parallax .ci-check { color: var(--true-green, #5ae07a); font-size: 10px; flex-shrink: 0; } +.overview-parallax .ci-label { color: var(--text-muted, #6b5a4d); } +.overview-parallax .ci-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; font-size: 9px; font-weight: 600; border-radius: 4px; margin-bottom: 6px; } +.overview-parallax .ci-badge--pass { color: var(--true-green, #5ae07a); background: rgba(90, 224, 122, 0.1); border: 1px solid rgba(90, 224, 122, 0.2); } + +/* YAML snippet */ +.overview-parallax .yaml-snippet { font-size: 9.5px; line-height: 1.7; } +.overview-parallax .yaml-key { color: var(--keyword-blue, #7aafff); } +.overview-parallax .yaml-val { color: var(--string-amber, #ffb86c); } + +/* npm install */ +.overview-parallax .npm-cmd { font-size: 10px; line-height: 1.7; color: var(--text-muted, #6b5a4d); word-break: break-all; } +.overview-parallax .npm-prompt { color: var(--true-green, #5ae07a); } +.overview-parallax .npm-pkg { color: var(--accent, #E446FF); } +.overview-parallax .npm-result { margin-top: 6px; font-size: 9px; color: var(--text-muted, #6b5a4d); opacity: 0.7; } + +/* Codegen arrow */ +.overview-parallax .codegen-flow { display: flex; align-items: center; gap: 6px; font-size: 10px; margin-bottom: 4px; } +.overview-parallax .codegen-file { padding: 3px 8px; border-radius: 4px; font-size: 9.5px; white-space: nowrap; } +.overview-parallax .codegen-from { background: rgba(228, 70, 255, 0.1); color: var(--accent, #E446FF); border: 1px solid rgba(228, 70, 255, 0.15); } +.overview-parallax .codegen-to { background: rgba(122, 175, 255, 0.1); color: var(--keyword-blue, #7aafff); border: 1px solid rgba(122, 175, 255, 0.15); } +.overview-parallax .codegen-arrow { color: var(--text-muted, #6b5a4d); font-size: 14px; opacity: 0.5; } +.overview-parallax .codegen-desc { font-size: 9px; color: var(--text-muted, #6b5a4d); opacity: 0.7; margin-top: 4px; } + +/* Git diff */ +.overview-parallax .diff-line { font-size: 9.5px; line-height: 1.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.overview-parallax .diff-add { color: var(--true-green, #5ae07a); } +.overview-parallax .diff-del { color: var(--false-red, #e05a5a); opacity: 0.7; } +.overview-parallax .diff-ctx { color: var(--text-muted, #6b5a4d); opacity: 0.5; } +.overview-parallax .diff-header { font-size: 9px; color: var(--text-muted, #6b5a4d); opacity: 0.5; margin-bottom: 4px; } + +/* Schema validation */ +.overview-parallax .schema-line { display: flex; align-items: center; gap: 5px; font-size: 9.5px; line-height: 1.8; } +.overview-parallax .schema-icon { flex-shrink: 0; font-size: 10px; } +.overview-parallax .schema-ok { color: var(--type-teal, #5ae0c2); } +.overview-parallax .schema-label { color: var(--text-muted, #6b5a4d); } + +/* ── Liquid color bleed overlay ── */ +.overview-parallax .color-bleed { + position: absolute; + inset: 0; + border-radius: 20px; + z-index: 4; + pointer-events: none; + background: radial-gradient( + circle at 90% 10%, + rgba(228, 70, 255, 0.12) 0%, + rgba(228, 70, 255, 0.06) 30%, + rgba(228, 70, 255, 0.02) 60%, + transparent 80% + ); + clip-path: circle(0% at 90% 10%); + transition: clip-path 0.9s cubic-bezier(0.4, 0, 0.2, 1); + will-change: clip-path; + transform: translateZ(61px); + backface-visibility: hidden; +} + +.overview-parallax .card.state-peeked .color-bleed, +.overview-parallax .card.state-developer .color-bleed { + clip-path: circle(160% at 90% 10%); +} diff --git a/packages/homepage/components/bridges/overview-parallax.tsx b/packages/homepage/components/bridges/overview-parallax.tsx new file mode 100644 index 0000000..d65e345 --- /dev/null +++ b/packages/homepage/components/bridges/overview-parallax.tsx @@ -0,0 +1,541 @@ +'use client' + +import { useRef, useState, useEffect, useCallback } from 'react' +import './overview-parallax.css' +import '../flow/flow.css' + +type ViewState = 'business' | 'peeked' | 'developer' +type Direction = 'forward' | 'backward' + +const TILT_MAX = 3 +const PEEK_ANGLE = 30 +const LERP_FACTOR = 0.08 + +interface OverviewParallaxProps { + perspective?: 'business' | 'developer' + onPerspectiveChange?: (p: 'business' | 'developer') => void +} + +export function OverviewParallax({ perspective, onPerspectiveChange }: OverviewParallaxProps = {}) { + const sceneRef = useRef(null) + const cardRef = useRef(null) + const rafRef = useRef(0) + const mouseRef = useRef({ x: 0, y: 0 }) + const rotateRef = useRef({ x: 0, y: 0 }) + const peekTimerRef = useRef | null>(null) + + const [state, setState] = useState('business') + const [direction, setDirection] = useState('forward') + + // Store state/direction in refs so the rAF loop and setTimeout see latest values + const stateRef = useRef(state) + const directionRef = useRef(direction) + useEffect(() => { stateRef.current = state }, [state]) + useEffect(() => { directionRef.current = direction }, [direction]) + + /* ── Set state with auto-advance from peeked ── */ + const setViewState = useCallback((newState: ViewState) => { + if (peekTimerRef.current) { + clearTimeout(peekTimerRef.current) + peekTimerRef.current = null + } + setState(newState) + + if (newState === 'peeked') { + peekTimerRef.current = setTimeout(() => { + if (directionRef.current === 'forward') { + setState('developer') + } else { + setState('business') + } + }, 900) + } + }, []) + + /* ── Cycle through states ── */ + const cycleState = useCallback(() => { + const current = stateRef.current + if (current === 'business') { + setDirection('forward') + directionRef.current = 'forward' + setViewState('peeked') + } else if (current === 'peeked') { + if (directionRef.current === 'forward') { + setViewState('developer') + } else { + setViewState('business') + } + } else { + // developer -> peeked (backward), then auto-advance to business + setDirection('backward') + directionRef.current = 'backward' + setViewState('peeked') + } + }, [setViewState]) + + /* ── Sync with external perspective prop ── */ + useEffect(() => { + if (!perspective) return + const current = stateRef.current + if (perspective === 'developer' && current !== 'developer') { + setDirection('forward') + directionRef.current = 'forward' + setViewState('peeked') + } else if (perspective === 'business' && current !== 'business') { + setDirection('backward') + directionRef.current = 'backward' + setViewState('peeked') + } + }, [perspective, setViewState]) + + /* ── Animation loop: lerp-smoothed tilt + state-driven base rotation ── */ + const animate = useCallback(() => { + const card = cardRef.current + if (!card) { + rafRef.current = requestAnimationFrame(animate) + return + } + + const mouse = mouseRef.current + const rotate = rotateRef.current + + const tiltX = -mouse.y * TILT_MAX + const tiltY = mouse.x * TILT_MAX + + const baseY = stateRef.current === 'peeked' ? PEEK_ANGLE : 0 + + const targetRotateX = tiltX + const targetRotateY = baseY + tiltY + + rotate.x += (targetRotateX - rotate.x) * LERP_FACTOR + rotate.y += (targetRotateY - rotate.y) * LERP_FACTOR + + card.style.transform = + 'rotateX(' + rotate.x.toFixed(3) + 'deg) ' + + 'rotateY(' + rotate.y.toFixed(3) + 'deg)' + + rafRef.current = requestAnimationFrame(animate) + }, []) + + /* ── Setup mousemove listener and animation loop ── */ + useEffect(() => { + const scene = sceneRef.current + if (!scene) return + + const handleMouseMove = (e: MouseEvent) => { + const rect = scene.getBoundingClientRect() + mouseRef.current.x = ((e.clientX - rect.left) / rect.width - 0.5) * 2 + mouseRef.current.y = ((e.clientY - rect.top) / rect.height - 0.5) * 2 + } + + const handleMouseLeave = () => { + mouseRef.current.x = 0 + mouseRef.current.y = 0 + } + + scene.addEventListener('mousemove', handleMouseMove) + scene.addEventListener('mouseleave', handleMouseLeave) + + rafRef.current = requestAnimationFrame(animate) + + return () => { + scene.removeEventListener('mousemove', handleMouseMove) + scene.removeEventListener('mouseleave', handleMouseLeave) + cancelAnimationFrame(rafRef.current) + if (peekTimerRef.current) { + clearTimeout(peekTimerRef.current) + } + } + }, [animate]) + + /* ── Card class based on state ── */ + const cardClassName = + 'card' + + (state === 'peeked' ? ' state-peeked' : '') + + (state === 'developer' ? ' state-developer' : '') + + /* ── Click on card when peeked ── */ + const handleCardClick = useCallback(() => { + if (stateRef.current === 'peeked') { + cycleState() + } + }, [cycleState]) + + const handleCtaClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + if (onPerspectiveChange) { + const current = stateRef.current + onPerspectiveChange(current === 'developer' ? 'business' : 'developer') + } else { + cycleState() + } + }, + [cycleState, onPerspectiveChange], + ) + + return ( +
+
+
+ +
+
+ {/* ===== FRONT FACE — Business Perspective ===== */} +
+
+ + Business Perspective +
+ +

Service Blueprints

+

+ Design configurable service flows visually — drag nodes, connect paths, + define swimlanes, and embed decision tables at every routing point. + Your entire business process in one executable specification. +

+ + {/* Flow diagram sub-container with editor header */} +
+
+ + + + order-fulfillment.flowprint +
+
+
+ + + + + + + {/* Hover gradient fills */} + + + + + + + + + + + + + + + + + {/* Radial inner glow */} + + + + + + + + + + + + + + + {/* CUSTOMER Lane (y: 0-75) */} + + + + CUSTOMER + + {/* Start node (terminal) */} + + + Start + + + {/* Submit Order (action) */} + + + ACTION + Submit Order + + + + {/* ROUTING Lane (y: 81-172) */} + + + + ROUTING + + {/* Edge labels from switch */} + express + standard + review + + {/* Decision -> Express */} + + {/* Decision -> Standard */} + + {/* Decision -> Review */} + + + {/* Validate (action) */} + + + ACTION + Validate + + + {/* Route (switch) */} + + + SWITCH + Route + + + {/* Review node (action) */} + + + ACTION + Review + + + + {/* FULFILLMENT Lane (y: 178-258) */} + + + + FULFILLMENT + + {/* Confirm (action) */} + + + ACTION + Confirm + + + {/* Ship Standard (action) */} + + + ACTION + Ship Standard + + + {/* Ship Express (action) */} + + + ACTION + Ship Express + + + {/* End node (terminal) */} + + + End + + + + {/* Cross-lane edges */} + {/* Start -> Submit Order */} + + + {/* Submit Order -> Validate */} + + + {/* Validate -> Route Decision */} + + + {/* Review -> Confirm */} + + + {/* Confirm -> Ship Standard */} + + + {/* Ship Standard -> End (route through bottom) */} + + + {/* Ship Express -> End */} + + +
+
+ + +
+ + {/* ===== SEPARATOR PLANE (Flowprint Engine) ===== */} +
+
+ + + + + + + Flowprint Engine +
+
+ + {/* ===== BACK FACE — Developer Perspective ===== */} +
+
+ + Developer Perspective +
+ +

Service Blueprints

+

+ Validated specifications feed your existing toolchain — CI catches structural + errors, deterministic YAML diffs cleanly, and one command generates production + Temporal TypeScript. No Flowprint runtime dependency. +

+ + {/* Toolchain grid (3x2) */} +
+ {/* 1. GitHub CI/CD */} +
+
+
+ +
+
GitHub CI/CD
+
+
+
+ CI Passed +
+
Schema valid
+
Refs resolved
+
Types matched
+
No cycles
+
+
+ + {/* 2. YAML Artifact */} +
+
+
+ +
+
.flowprint.yaml
+
+
+
+ schema: flowprint/1.0
+ name: order-fulfillment
+ nodes:
+   route_order:
+     type: switch +
+
+
+ + {/* 3. npm */} +
+
+
+ +
+
npm
+
+
+
+ $ npm i @ruminaider/
flowprint-editor
+
+
+ added 1 package in 2.1s
+ 0 vulnerabilities +
+
+
+ + {/* 4. Code Generation */} +
+
+
+ +
+
Code Generation
+
+
+
+ .yaml + + .ts +
+
+ Temporal workflows
+ + typed routing from
+ hit-policy definitions +
+
+
+ + {/* 5. Git Diff */} +
+
+
+ +
+
Git
+
+
+
order-fulfillment.flowprint.yaml
+
  route_order:
+
-   hit: collect
+
+   hit: first
+
  conditions:
+
+   - membership
+
+
+ + {/* 6. Schema Validation */} +
+
+
+ +
+
Schema Validation
+
+
+
flowprint/1.0 conformant
+
0 dangling refs
+
Deterministic key order
+
6 nodes validated
+
+
+
+ + +
+ + {/* ===== LIQUID COLOR BLEED OVERLAY ===== */} +
+
+
+
+ ) +} diff --git a/packages/homepage/components/flow/flow-defs.tsx b/packages/homepage/components/flow/flow-defs.tsx new file mode 100644 index 0000000..28c8c44 --- /dev/null +++ b/packages/homepage/components/flow/flow-defs.tsx @@ -0,0 +1,15 @@ +export function FlowDefs({ prefix = '' }: { prefix?: string }) { + return ( + + + + + + + + + + + + ) +} diff --git a/packages/homepage/components/flow/flow-node.tsx b/packages/homepage/components/flow/flow-node.tsx new file mode 100644 index 0000000..477127e --- /dev/null +++ b/packages/homepage/components/flow/flow-node.tsx @@ -0,0 +1,37 @@ +interface FlowNodeProps { + id?: string + cx: number + cy: number + type: 'action' | 'switch' | 'terminal' + label: string + width?: number + className?: string +} + +const TYPE_COLORS = { + action: { stroke: 'rgba(255,146,67,0.3)', badge: '#FF9243', text: 'ACTION' }, + switch: { stroke: 'rgba(163,116,255,0.3)', badge: '#A374FF', text: 'SWITCH' }, + terminal: { stroke: '', badge: '#3FDC77', text: '' }, +} + +export function FlowNode({ id, cx, cy, type, label, width = 80, className = '' }: FlowNodeProps) { + const colors = TYPE_COLORS[type] + const halfW = width / 2 + + if (type === 'terminal') { + return ( + + + {label} + + ) + } + + return ( + + + {colors.text} + {label} + + ) +} diff --git a/packages/homepage/components/flow/flow.css b/packages/homepage/components/flow/flow.css new file mode 100644 index 0000000..6c241d3 --- /dev/null +++ b/packages/homepage/components/flow/flow.css @@ -0,0 +1,78 @@ +/* Shared flow SVG styles — ported 1:1 from HTML prototypes */ + +/* Node rectangles (action, switch) */ +.flow-svg .node-rect { + rx: 6; + ry: 6; + fill: #2a1a14; + stroke: #3d2a22; + stroke-width: 1.2; + transition: fill 0.15s ease, stroke 0.15s ease; +} + +/* Node labels */ +.flow-svg .node-label { + font-family: 'DM Sans', sans-serif; + font-size: 9.5px; + font-weight: 500; + fill: #f0e6df; + text-anchor: middle; + dominant-baseline: central; + pointer-events: none; +} + +/* Type badges (ACTION, SWITCH) */ +.flow-svg .node-type-badge { + font-family: 'JetBrains Mono', monospace; + font-size: 5.5px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + text-anchor: middle; + dominant-baseline: central; + opacity: 0.6; + pointer-events: none; +} + +/* Terminal circles */ +.flow-svg .node-circle { + fill: #2a1a14; + stroke: #3FDC77; + stroke-width: 1.2; +} + +/* Edges */ +.flow-svg .edge { + fill: none; + stroke: #6b4d40; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; + transition: stroke 0.15s ease; +} + +/* Edge labels */ +.flow-svg .edge-label { + font-family: 'JetBrains Mono', monospace; + font-size: 7px; + font-weight: 500; + fill: #6b5a4d; + opacity: 0.7; +} + +/* Lane backgrounds */ +.flow-svg .lane-bg-rect { + fill: rgba(26, 15, 10, 0.5); + stroke: rgba(61, 42, 34, 0.3); + stroke-width: 0.5; +} + +/* Lane labels */ +.flow-svg .lane-label { + font-family: 'JetBrains Mono', monospace; + font-size: 8px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + fill: rgba(160, 139, 122, 0.5); +} diff --git a/packages/homepage/components/flow/topology-preview.tsx b/packages/homepage/components/flow/topology-preview.tsx new file mode 100644 index 0000000..c74529e --- /dev/null +++ b/packages/homepage/components/flow/topology-preview.tsx @@ -0,0 +1,270 @@ +'use client' + +/** + * Renders a compact SVG flow diagram from topology data. + * Adapted from packages/app TopologyPreview for the marketing site. + */ + +interface TopologyPreviewProps { + topo: { + lanes: number + nodes: [number, number, string][] + edges: [number, number][] + } + width?: number + height?: number + className?: string + expanded?: boolean +} + +const TYPE_COLOR: Record = { + a: '#FF9243', // action — orange + s: '#A374FF', // switch — purple + p: '#A374FF', // parallel — purple + w: '#F59E0B', // wait — amber + e: '#FF362B', // error — red + t: '#3FDC77', // terminal — green +} + +const LANE_COLORS = [ + 'rgba(255, 107, 107, 0.12)', // customer/claimant + 'rgba(228, 70, 255, 0.10)', // routing/agent + 'rgba(6, 182, 212, 0.10)', // fulfillment/adjuster + 'rgba(163, 116, 255, 0.08)', // legal + 'rgba(255, 146, 67, 0.08)', // finance +] + +export function TopologyPreview({ + topo, + width = 320, + height = 140, + className = '', + expanded = false, +}: TopologyPreviewProps) { + const { lanes, nodes, edges } = topo + const pad = expanded ? { x: 44, y: 18 } : { x: 36, y: 12 } + const maxCol = Math.max(...nodes.map((n) => n[0])) + const colW = maxCol > 0 ? (width - pad.x * 2) / maxCol : 0 + const fullLaneH = height / lanes + const laneH = (height - pad.y * 2) / lanes + const r = expanded + ? Math.max(6, Math.min(9, 10 - lanes * 0.5)) + : Math.max(4, Math.min(7, 8 - lanes * 0.4)) + + const pos = nodes.map(([col, lane]) => ({ + x: pad.x + col * colW, + y: pad.y + lane * laneH + laneH / 2, + })) + + return ( + + {/* Lane bands */} + {Array.from({ length: lanes }, (_, i) => ( + + ))} + + {/* Edges */} + {edges.map(([from, to], i) => { + const a = pos[from] + const b = pos[to] + if (!a || !b) return null + + if (Math.abs(a.y - b.y) < 1) { + return ( + + ) + } + + const midX = (a.x + b.x) / 2 + return ( + + ) + })} + + {/* Nodes */} + {nodes.map(([, , type], i) => { + const p = pos[i] + if (!p) return null + const color = TYPE_COLOR[type] ?? '#6b5a4d' + return + })} + + ) +} + +function NodeShape({ + x, + y, + type, + color, + r, +}: { + x: number + y: number + type: string + color: string + r: number +}) { + switch (type) { + case 't': + return ( + + ) + + case 's': { + const d = r * 0.85 + return ( + + ) + } + + case 'p': { + const hw = r * 1.1 + const hh = r * 0.7 + return ( + + + + + + ) + } + + case 'w': + return ( + + + + + + ) + + case 'e': + return ( + + ) + + default: + return ( + + ) + } +} diff --git a/packages/homepage/components/hero-background.tsx b/packages/homepage/components/hero-background.tsx new file mode 100644 index 0000000..00b5422 --- /dev/null +++ b/packages/homepage/components/hero-background.tsx @@ -0,0 +1,173 @@ +"use client" + +import { useEffect, useRef } from "react" + +interface Node { + id: number + x: number + y: number + size: number + type: "start" | "action" | "decision" | "end" + connections: number[] + animationDelay: number +} + +export function HeroBackground() { + const canvasRef = useRef(null) + const animationRef = useRef(null) + const nodesRef = useRef([]) + const timeRef = useRef(0) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext("2d") + if (!ctx) return + + const resizeCanvas = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + generateNodes() + } + + const generateNodes = () => { + const nodes: Node[] = [] + const nodeCount = Math.floor((canvas.width * canvas.height) / 50000) + const types: Node["type"][] = ["start", "action", "decision", "action", "action", "end"] + + for (let i = 0; i < nodeCount; i++) { + const connections: number[] = [] + const connectionCount = Math.floor(Math.random() * 2) + 1 + for (let j = 0; j < connectionCount; j++) { + const targetId = Math.floor(Math.random() * nodeCount) + if (targetId !== i) connections.push(targetId) + } + + nodes.push({ + id: i, + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + size: Math.random() * 4 + 2, + type: types[Math.floor(Math.random() * types.length)], + connections, + animationDelay: Math.random() * 5, + }) + } + nodesRef.current = nodes + } + + const drawNode = (node: Node, alpha: number) => { + ctx.save() + ctx.globalAlpha = alpha * 0.6 + + const colors = { + start: "#E446FF", + action: "#E446FF", + decision: "#ff6b9d", + end: "#E446FF", + } + + ctx.fillStyle = colors[node.type] + ctx.shadowColor = colors[node.type] + ctx.shadowBlur = 10 + + ctx.beginPath() + if (node.type === "decision") { + ctx.moveTo(node.x, node.y - node.size) + ctx.lineTo(node.x + node.size, node.y) + ctx.lineTo(node.x, node.y + node.size) + ctx.lineTo(node.x - node.size, node.y) + } else if (node.type === "start" || node.type === "end") { + ctx.arc(node.x, node.y, node.size, 0, Math.PI * 2) + } else { + ctx.roundRect(node.x - node.size, node.y - node.size / 2, node.size * 2, node.size, 2) + } + ctx.fill() + ctx.restore() + } + + const drawConnection = (from: Node, to: Node, progress: number) => { + ctx.save() + ctx.globalAlpha = 0.15 * progress + + const gradient = ctx.createLinearGradient(from.x, from.y, to.x, to.y) + gradient.addColorStop(0, "#E446FF") + gradient.addColorStop(1, "transparent") + + ctx.strokeStyle = gradient + ctx.lineWidth = 1 + + ctx.beginPath() + ctx.moveTo(from.x, from.y) + + const midX = (from.x + to.x) / 2 + const midY = (from.y + to.y) / 2 - 20 + ctx.quadraticCurveTo(midX, midY, to.x, to.y) + ctx.stroke() + + // Animated particle along the line + if (progress > 0.5) { + const t = ((progress - 0.5) * 2) + const particleX = from.x + (to.x - from.x) * t + const particleY = from.y + (to.y - from.y) * t - 20 * Math.sin(Math.PI * t) + + ctx.globalAlpha = 0.8 + ctx.fillStyle = "#E446FF" + ctx.shadowColor = "#E446FF" + ctx.shadowBlur = 8 + ctx.beginPath() + ctx.arc(particleX, particleY, 2, 0, Math.PI * 2) + ctx.fill() + } + + ctx.restore() + } + + const animate = () => { + timeRef.current += 0.005 + ctx.clearRect(0, 0, canvas.width, canvas.height) + + const nodes = nodesRef.current + + // Draw connections first + nodes.forEach((node) => { + node.connections.forEach((targetId) => { + const target = nodes[targetId] + if (target) { + const progress = (Math.sin(timeRef.current + node.animationDelay) + 1) / 2 + drawConnection(node, target, progress) + } + }) + }) + + // Draw nodes + nodes.forEach((node) => { + const pulse = Math.sin(timeRef.current * 2 + node.animationDelay) * 0.3 + 0.7 + drawNode(node, pulse) + }) + + animationRef.current = requestAnimationFrame(animate) + } + + resizeCanvas() + animate() + + window.addEventListener("resize", resizeCanvas) + + return () => { + window.removeEventListener("resize", resizeCanvas) + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + } + }, []) + + return ( + + ) +} diff --git a/packages/homepage/components/sections/capabilities-section.css b/packages/homepage/components/sections/capabilities-section.css new file mode 100644 index 0000000..5c2145b --- /dev/null +++ b/packages/homepage/components/sections/capabilities-section.css @@ -0,0 +1,88 @@ +.capabilities { + width: 100%; + max-width: 1100px; + margin: 0 auto; + padding: 0 24px; +} + +.perspective-toggle { + display: flex; + justify-content: center; + gap: 4px; + padding: 4px; + background: rgba(26, 15, 10, 0.6); + border: 1px solid rgba(228, 70, 255, 0.12); + border-radius: 12px; + width: fit-content; + margin: 0 auto 40px; + position: sticky; + top: 80px; /* 64px fixed header + 16px breathing room */ + z-index: 10; + backdrop-filter: blur(12px); +} + +.perspective-toggle button { + padding: 10px 28px; + font-family: 'DM Sans', system-ui, sans-serif; + font-size: 14px; + font-weight: 500; + color: #6b5a4d; + background: transparent; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.perspective-toggle button:hover:not(.active) { + color: #a89585; +} + +.perspective-toggle button.active { + color: #f0e6df; + background: rgba(228, 70, 255, 0.12); + box-shadow: 0 0 12px rgba(228, 70, 255, 0.15); +} + +.bridge-stack { + display: flex; + flex-direction: column; + gap: 48px; +} + +/* Bridge card — transparent wrapper, bridge components provide own styling */ +.bridge-card { + display: flex; + justify-content: center; + overflow: visible; +} + +/* Only the placeholder card gets visible styling */ +.bridge-card-placeholder { + background: #1a0f0a; + border: 1px solid rgba(228, 70, 255, 0.12); + border-radius: 16px; + padding: 32px; +} + +.bridge-card-placeholder h3 { + font-family: 'Instrument Serif', Georgia, serif; + font-size: 24px; + font-weight: 400; + color: #fafafa; + margin: 0 0 20px; + letter-spacing: -0.01em; +} + +.bridge-placeholder { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; + background: rgba(26, 15, 10, 0.4); + border: 1px dashed rgba(228, 70, 255, 0.12); + border-radius: 8px; + color: #6b5a4d; + font-family: 'DM Sans', system-ui, sans-serif; + font-size: 14px; +} diff --git a/packages/homepage/components/sections/capabilities-section.tsx b/packages/homepage/components/sections/capabilities-section.tsx new file mode 100644 index 0000000..d184fc1 --- /dev/null +++ b/packages/homepage/components/sections/capabilities-section.tsx @@ -0,0 +1,51 @@ +'use client' + +import { useState } from 'react' +import { BridgeDecision } from '@/components/bridges/bridge-decision' +import { BridgeSimulation } from '@/components/bridges/bridge-simulation' +import { BridgeVersioning } from '@/components/bridges/bridge-versioning' +import './capabilities-section.css' + +type Perspective = 'business' | 'developer' + +export function CapabilitiesSection() { + const [perspective, setPerspective] = useState('business') + + return ( +
+ {/* Pinned pill toggle — sticky below fixed header */} +
+ + +
+ + {/* Bridge cards stacked vertically */} +
+ {/* Bridge 1: Decision Tables */} +
+ +
+ + {/* Bridge 2: Simulation */} +
+ +
+ + {/* Bridge 3: Version Control */} +
+ +
+
+
+ ) +} diff --git a/packages/homepage/components/sections/footer.tsx b/packages/homepage/components/sections/footer.tsx new file mode 100644 index 0000000..b1a4ea1 --- /dev/null +++ b/packages/homepage/components/sections/footer.tsx @@ -0,0 +1,138 @@ +import { Github, Package, BookOpen } from "lucide-react" + +const footerLinks = { + product: [ + { label: "Web App", href: "#" }, + { label: "CLI", href: "#" }, + { label: "Documentation", href: "#" }, + { label: "Templates", href: "#" }, + ], + packages: [ + { label: "@ruminaider/flowprint-schema", href: "#" }, + { label: "@ruminaider/flowprint-editor", href: "#" }, + { label: "flowprint (CLI)", href: "#" }, + ], + resources: [ + { label: "GitHub", href: "#" }, + { label: "Changelog", href: "#" }, + { label: "License (MIT)", href: "#" }, + ], +} + +export function FooterSection() { + return ( +
+
+
+ {/* Brand */} +
+
+
+ + + + + + +
+ Flowprint +
+

+ Executable service blueprints. Business defines, engineering ships. +

+
+ + {/* Product links */} +
+

Product

+ +
+ + {/* Packages */} +
+

Packages

+ +
+ + {/* Resources */} +
+

Resources

+ +
+
+ + {/* Bottom bar */} +
+

+ Open source under MIT License +

+ + +
+
+
+ ) +} diff --git a/packages/homepage/components/sections/getting-started.tsx b/packages/homepage/components/sections/getting-started.tsx new file mode 100644 index 0000000..cc67307 --- /dev/null +++ b/packages/homepage/components/sections/getting-started.tsx @@ -0,0 +1,131 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { ArrowRight, Copy, ExternalLink } from "lucide-react" +import { useState } from "react" + +const cliSteps = [ + { command: "flowprint init my-workflow", comment: "# Create a new blueprint" }, + { command: "flowprint validate", comment: "# Check for errors" }, + { command: "flowprint generate", comment: "# Generate TypeScript" }, +] + +export function GettingStartedSection() { + const [copiedIndex, setCopiedIndex] = useState(null) + + const handleCopy = (command: string, index: number) => { + navigator.clipboard.writeText(command) + setCopiedIndex(index) + setTimeout(() => setCopiedIndex(null), 2000) + } + + return ( +
+
+ {/* Section header */} +
+

Get started in 30 seconds

+

+ No signup required. Open the app or install locally — your choice. +

+
+ + {/* Two paths */} +
+ {/* Path A: Web App */} +
+ {/* Decorative gradient */} +
+ +
+
+ + Recommended +
+ +

Use the web app

+

+ Open the hosted editor and start designing immediately. No installation, + no account, no friction. +

+ + + + {/* Preview mockup */} +
+
+
+
+
+ app.flowprint.dev +
+
+
+
+
+
+
+
+
Canvas
+
+
+
+
+
+ + {/* Path B: CLI */} +
+
+ For developers +
+ +

Install locally

+

+ Full CLI access for validation, generation, and CI integration. + Works with any editor. +

+ + {/* CLI commands */} +
+ {cliSteps.map((step, index) => ( +
+
+ ${" "} + {step.command} + {step.comment} +
+ +
+ ))} +
+ + {/* Links */} + +
+
+
+
+ ) +} diff --git a/packages/homepage/components/sections/header.tsx b/packages/homepage/components/sections/header.tsx new file mode 100644 index 0000000..9337870 --- /dev/null +++ b/packages/homepage/components/sections/header.tsx @@ -0,0 +1,110 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { Github, Menu, X } from "lucide-react" +import { useState } from "react" + +const navLinks = [ + { label: "How it works", href: "#how-it-works" }, + { label: "Templates", href: "#templates" }, + { label: "Docs", href: "#" }, +] + +export function Header() { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + + return ( +
+
+
+ {/* Logo */} + +
+ + + + + + +
+ Flowprint +
+ + {/* Desktop nav */} + + + {/* Desktop CTAs */} +
+ + + + +
+ + {/* Mobile menu button */} + +
+ + {/* Mobile menu */} + {mobileMenuOpen && ( +
+ +
+ )} +
+
+ ) +} diff --git a/packages/homepage/components/sections/hero.tsx b/packages/homepage/components/sections/hero.tsx new file mode 100644 index 0000000..01eee71 --- /dev/null +++ b/packages/homepage/components/sections/hero.tsx @@ -0,0 +1,100 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { HeroBackground } from "@/components/hero-background" +import { Copy, ArrowRight } from "lucide-react" +import { useState } from "react" + +export function HeroSection() { + const [copied, setCopied] = useState(false) + const installCommand = "npm i -g flowprint" + + const handleCopy = () => { + navigator.clipboard.writeText(installCommand) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+ + + {/* Gradient overlay */} +
+ +
+ {/* Badge */} +
+ Open Source + | + MIT Licensed +
+ + {/* Headline */} +

+ Executable +
+ service blueprints +

+ + {/* Subheadline */} +

+ Visual design, decision tables, simulation, code generation, and version control — + all in one specification that business defines and engineering ships. +

+ + {/* CTAs */} +
+ + +
+ {installCommand} + +
+
+ + {/* YAML Preview */} +
+
+
+
+
+
+ patient-intake.flowprint.yaml +
+
+              
+                name: patient-intake{"\n"}
+                version: 1.0.0{"\n"}
+                lanes:{"\n"}
+                {"  "}- id: patient{"\n"}
+                {"  "}- id: intake-staff{"\n"}
+                {"  "}- id: clinical{"\n"}
+                nodes:{"\n"}
+                {"  "}- id: check-in{"\n"}
+                {"    "}type: action{"\n"}
+                {"    "}lane: patient
+              
+            
+
+
+
+ + {/* Scroll indicator */} +
+
+
+
+
+
+ ) +} diff --git a/packages/homepage/components/sections/homepage-tabs.tsx b/packages/homepage/components/sections/homepage-tabs.tsx new file mode 100644 index 0000000..bb1546f --- /dev/null +++ b/packages/homepage/components/sections/homepage-tabs.tsx @@ -0,0 +1,36 @@ +'use client' + +import * as Tabs from '@radix-ui/react-tabs' +import { OverviewSection } from './overview-section' +import { CapabilitiesSection } from './capabilities-section' +import { TryItSection } from './tryit-section' + +export function HomepageTabs() { + return ( + + + + Overview + + + Capabilities + + + Try It + + + + + + + + + + + + + + + + ) +} diff --git a/packages/homepage/components/sections/how-it-works.tsx b/packages/homepage/components/sections/how-it-works.tsx new file mode 100644 index 0000000..55757f2 --- /dev/null +++ b/packages/homepage/components/sections/how-it-works.tsx @@ -0,0 +1,282 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import { Palette, Table2, Play, CheckCircle, Code, Zap } from "lucide-react" + +const steps = [ + { + id: "design", + icon: Palette, + title: "Design", + description: "Start with a visual canvas. Drag nodes, connect flows, define swimlanes. Your business process takes shape in real-time.", + layer: "canvas", + }, + { + id: "rules", + icon: Table2, + title: "Define Rules", + description: "Add decision tables alongside your flows. Complex routing logic becomes a spreadsheet anyone can understand.", + layer: "rules", + }, + { + id: "simulate", + icon: Play, + title: "Simulate", + description: "Run your blueprint in the browser. Watch data flow through nodes, see which paths execute, catch edge cases before they ship.", + layer: "simulate", + }, + { + id: "validate", + icon: CheckCircle, + title: "Validate", + description: "CI integration catches schema errors, dangling references, and structural issues. Nothing broken merges.", + layer: "validate", + }, + { + id: "generate", + icon: Code, + title: "Generate", + description: "One command produces Temporal TypeScript. Type-safe, production-ready, with no Flowprint runtime dependency.", + layer: "generate", + }, + { + id: "execute", + icon: Zap, + title: "Execute", + description: "Your generated code runs on Temporal. Durable, scalable, observable. The whole stack is alive.", + layer: "execute", + }, +] + +function VisualLayer({ activeStep }: { activeStep: number }) { + return ( +
+ {/* Canvas layer */} +
= 0 ? "opacity-100" : "opacity-0" + } ${activeStep > 0 ? "opacity-30" : ""}`} + > +
+
Canvas
+
+ {/* Swimlanes */} +
+
Patient
+
Start
+
+
Check-in
+
+
+
Staff
+
Verify
+
+
Triage
+
+
+
Clinical
+
Assess
+
+
End
+
+
+
+
+ + {/* Rules layer */} +
= 1 ? "opacity-100 translate-x-0" : "opacity-0 translate-x-4" + } ${activeStep > 1 ? "opacity-30" : ""}`} + > +
+
Decision Table
+
+
+ severity + lane +
+
+ high + clinical +
+
+ medium + staff +
+
+ low + staff +
+
+
+
+ + {/* Simulate layer */} +
= 2 ? "opacity-100" : "opacity-0" + } ${activeStep > 2 ? "opacity-30" : ""}`} + > +
+
+
+ +
+ Simulating... + Step 3/6 +
+
+ + {/* Validate layer */} +
= 3 ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4" + } ${activeStep > 3 ? "opacity-30" : ""}`} + > +
+
+ + CI Passed +
+
+

Schema: valid

+

References: resolved

+

Structure: clean

+
+
+
+ + {/* Generate layer */} +
= 4 ? "opacity-100 translate-x-0" : "opacity-0 translate-x-4" + } ${activeStep > 4 ? "opacity-30" : ""}`} + > +
+
Generated Code
+
+{`export async function
+  patientIntake(
+    ctx: Context
+  ) {
+    await checkIn(ctx);
+    // ...
+  }`}
+          
+
+
+ + {/* Execute layer */} +
= 5 ? "opacity-100 scale-100" : "opacity-0 scale-95" + }`} + > +
+
+
+ Live on Temporal +
+
+
+
+ ) +} + +export function HowItWorksSection() { + const [activeStep, setActiveStep] = useState(0) + const sectionRef = useRef(null) + const stepsRef = useRef<(HTMLDivElement | null)[]>([]) + + useEffect(() => { + const observers: IntersectionObserver[] = [] + + stepsRef.current.forEach((step, index) => { + if (!step) return + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveStep(index) + } + }) + }, + { threshold: 0.5, rootMargin: "-20% 0px -20% 0px" } + ) + + observer.observe(step) + observers.push(observer) + }) + + return () => { + observers.forEach((observer) => observer.disconnect()) + } + }, []) + + return ( +
+
+ {/* Section header */} +
+

How it works

+

+ From design to execution in six layers. Each step builds on the last. +

+
+ + {/* Scroll-triggered layout */} +
+ {/* Steps */} +
+ {steps.map((step, index) => ( +
{ + stepsRef.current[index] = el + }} + className={`transition-all duration-300 ${ + activeStep === index + ? "opacity-100" + : activeStep > index + ? "opacity-40" + : "opacity-60" + }`} + > +
+
+ +
+
+
+ Step {index + 1} +
+

{step.title}

+

{step.description}

+
+
+
+ ))} +
+ + {/* Sticky visual */} +
+ +
+
+ + {/* Mobile visual (non-sticky) */} +
+ +
+
+
+ ) +} diff --git a/packages/homepage/components/sections/integrations.tsx b/packages/homepage/components/sections/integrations.tsx new file mode 100644 index 0000000..844081c --- /dev/null +++ b/packages/homepage/components/sections/integrations.tsx @@ -0,0 +1,63 @@ +"use client" + +import { GitBranch, Clock, GitMerge, Package, Search } from "lucide-react" + +const integrations = [ + { + icon: GitBranch, + name: "Git", + description: "Plain files, deterministic diffs. Branch and merge like code.", + }, + { + icon: Clock, + name: "Temporal", + description: "First-class code generation target. Production-ready workflows.", + }, + { + icon: GitMerge, + name: "CI/CD", + description: "Validation in any pipeline. GitHub Actions, GitLab, and more.", + }, + { + icon: Package, + name: "npm", + description: "Embeddable editor component. CLI installable via package manager.", + }, + { + icon: Search, + name: "Code Search", + description: "Optional symbol lookup. Link blueprint nodes to codebase functions.", + }, +] + +export function IntegrationsSection() { + return ( +
+
+ {/* Section header */} +
+

Fits your toolchain

+

+ Flowprint plugs into what you already use. No new infrastructure required. +

+
+ + {/* Integration grid */} +
+ {integrations.map((integration, index) => ( +
+
+ +
+

{integration.name}

+

{integration.description}

+
+ ))} +
+
+
+ ) +} diff --git a/packages/homepage/components/sections/overview-section.tsx b/packages/homepage/components/sections/overview-section.tsx new file mode 100644 index 0000000..76210e6 --- /dev/null +++ b/packages/homepage/components/sections/overview-section.tsx @@ -0,0 +1,9 @@ +import { OverviewParallax } from '@/components/bridges/overview-parallax' + +export function OverviewSection() { + return ( +
+ +
+ ) +} diff --git a/packages/homepage/components/sections/templates.tsx b/packages/homepage/components/sections/templates.tsx new file mode 100644 index 0000000..2c50e07 --- /dev/null +++ b/packages/homepage/components/sections/templates.tsx @@ -0,0 +1,177 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { ArrowRight } from "lucide-react" + +interface Template { + name: string + industry: string + nodes: number + lanes: number + complexity: "starter" | "intermediate" | "advanced" +} + +const templates: Template[] = [ + { + name: "Patient Intake", + industry: "Healthcare", + nodes: 15, + lanes: 4, + complexity: "intermediate", + }, + { + name: "Insurance Claims", + industry: "Finance", + nodes: 24, + lanes: 5, + complexity: "advanced", + }, + { + name: "Order Fulfillment", + industry: "E-commerce", + nodes: 21, + lanes: 5, + complexity: "advanced", + }, + { + name: "CI/CD Pipeline", + industry: "DevOps", + nodes: 16, + lanes: 3, + complexity: "intermediate", + }, +] + +function TemplateTopo({ nodes, lanes }: { nodes: number; lanes: number }) { + // Generate a simplified topology visualization + const nodePositions: { x: number; y: number; type: "start" | "node" | "end" }[] = [] + const cols = Math.ceil(nodes / lanes) + + for (let i = 0; i < Math.min(nodes, 12); i++) { + const col = Math.floor(i / lanes) + const row = i % lanes + nodePositions.push({ + x: (col / (cols - 1)) * 100 || 0, + y: (row / (lanes - 1)) * 100 || 0, + type: i === 0 ? "start" : i === Math.min(nodes, 12) - 1 ? "end" : "node", + }) + } + + return ( + + {/* Connections */} + {nodePositions.slice(0, -1).map((pos, i) => { + const next = nodePositions[i + 1] + if (!next) return null + return ( + + ) + })} + + {/* Nodes */} + {nodePositions.map((pos, i) => ( + + {pos.type === "start" || pos.type === "end" ? ( + + ) : ( + + )} + + ))} + + ) +} + +function ComplexityBadge({ complexity }: { complexity: Template["complexity"] }) { + const colors = { + starter: "bg-chart-5/10 text-chart-5 border-chart-5/20", + intermediate: "bg-chart-4/10 text-chart-4 border-chart-4/20", + advanced: "bg-primary/10 text-primary border-primary/20", + } + + return ( + + {complexity} + + ) +} + +export function TemplatesSection() { + return ( +
+
+ {/* Section header */} +
+

Real-world templates

+

+ Start with production-ready blueprints across industries. Complexity you can handle. +

+
+ + {/* Template cards */} +
+ {templates.map((template) => ( +
+ {/* Topology preview */} +
+ +
+ + {/* Card content */} +
+
+ + {template.industry} + + +
+ +

{template.name}

+ +
+ {template.nodes} nodes + {template.lanes} lanes +
+
+
+ ))} +
+ + {/* CTA */} +
+ +
+
+
+ ) +} diff --git a/packages/homepage/components/sections/tryit-section.css b/packages/homepage/components/sections/tryit-section.css new file mode 100644 index 0000000..55941bc --- /dev/null +++ b/packages/homepage/components/sections/tryit-section.css @@ -0,0 +1,365 @@ +/* Try It Section */ + +.tryit { + width: 100%; + max-width: 1100px; + margin: 0 auto; + padding: 0 24px; +} + +.tryit-intro { + text-align: center; + margin-bottom: 40px; +} + +.tryit-title { + font-family: 'Instrument Serif', Georgia, serif; + font-size: 32px; + font-weight: 400; + color: var(--foreground); + margin: 0 0 12px; +} + +.tryit-description { + font-family: var(--font-sans); + font-size: 15px; + color: var(--text-muted); + max-width: 600px; + margin: 0 auto; + line-height: 1.6; +} + +.tryit-description code { + font-family: var(--font-mono); + font-size: 13px; + background: var(--code-bg); + padding: 2px 6px; + border-radius: 4px; + color: var(--string-amber); +} + +/* ── Template Grid ── */ + +.template-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + margin-bottom: 32px; +} + +.template-card { + background: var(--card); + border: 1px solid var(--surface-border); + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.2s ease; + cursor: pointer; + text-align: left; + padding: 0; + font: inherit; + color: inherit; +} + +.template-card:hover { + border-color: rgba(228, 70, 255, 0.25); + box-shadow: 0 0 20px rgba(228, 70, 255, 0.08); + transform: translateY(-2px); +} + +.template-card.selected { + border-color: rgba(228, 70, 255, 0.5); + box-shadow: 0 0 24px rgba(228, 70, 255, 0.15), 0 0 0 1px rgba(228, 70, 255, 0.2); +} + +.template-preview { + height: 140px; + background: rgba(26, 15, 10, 0.4); + overflow: hidden; +} + +.template-body { + padding: 16px; + flex: 1; + display: flex; + flex-direction: column; +} + +.template-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; +} + +.template-title { + font-family: 'Instrument Serif', Georgia, serif; + font-size: 18px; + font-weight: 400; + color: var(--foreground); + margin: 0; +} + +.template-badge { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 3px 8px; + border-radius: 4px; + white-space: nowrap; +} + +.badge-showcase { + color: #E446FF; + background: rgba(228, 70, 255, 0.12); +} + +.badge-advanced { + color: #FF9243; + background: rgba(255, 146, 67, 0.12); +} + +.template-description { + font-family: var(--font-sans); + font-size: 13px; + color: var(--text-muted); + line-height: 1.5; + margin: 0 0 12px; + flex: 1; +} + +.template-meta { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); +} + +.meta-dot { + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--text-muted); + opacity: 0.4; +} + +/* ── Expanded Preview ── */ + +.template-expanded { + background: var(--card); + border: 1px solid var(--surface-border); + border-radius: 16px; + overflow: hidden; + margin-bottom: 40px; + max-height: 0; + opacity: 0; + transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease; + margin-top: 0; +} + +.template-expanded.visible { + max-height: 600px; + opacity: 1; + margin-top: 0; +} + +.expanded-header { + padding: 24px 28px 0; +} + +.expanded-title-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.expanded-title { + font-family: 'Instrument Serif', Georgia, serif; + font-size: 24px; + font-weight: 400; + color: var(--foreground); + margin: 0; +} + +.expanded-description { + font-family: var(--font-sans); + font-size: 14px; + color: var(--text-muted); + line-height: 1.5; + margin: 0; + max-width: 700px; +} + +.expanded-content { + display: flex; + gap: 0; + padding: 20px 28px 28px; +} + +.expanded-preview { + flex: 1; + min-height: 280px; + background: rgba(17, 9, 4, 0.6); + border: 1px solid rgba(61, 42, 34, 0.3); + border-radius: 10px; + overflow: hidden; +} + +.expanded-sidebar { + width: 220px; + flex-shrink: 0; + padding-left: 24px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.sidebar-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.sidebar-heading { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-muted); + margin: 0; + opacity: 0.7; +} + +.lane-list, +.type-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.lane-item, +.type-item { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--font-sans); + font-size: 12px; + color: #a89585; +} + +.lane-dot, +.type-dot { + width: 8px; + height: 8px; + border-radius: 2px; + flex-shrink: 0; +} + +.lane-name, +.type-label { + flex: 1; +} + +.type-count { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); + opacity: 0.6; +} + +.install-snippet { + background: var(--code-bg); + border: 1px solid rgba(61, 42, 34, 0.4); + border-radius: 6px; + padding: 10px 12px; + overflow-x: auto; +} + +.install-snippet code { + font-family: var(--font-mono); + font-size: 11px; + color: var(--string-amber); + white-space: nowrap; +} + +/* ── CTAs ── */ + +.tryit-ctas { + display: flex; + justify-content: center; + gap: 16px; +} + +.cta-primary { + display: inline-flex; + align-items: center; + padding: 12px 32px; + font-family: var(--font-sans); + font-size: 14px; + font-weight: 600; + color: #110904; + background: #E446FF; + border-radius: 8px; + text-decoration: none; + transition: opacity 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; +} + +.cta-primary:hover { + opacity: 0.9; + box-shadow: 0 0 24px rgba(228, 70, 255, 0.35); + transform: translateY(-1px); +} + +.cta-secondary { + display: inline-flex; + align-items: center; + padding: 12px 32px; + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + color: var(--foreground); + background: transparent; + border: 1px solid var(--surface-border); + border-radius: 8px; + text-decoration: none; + transition: border-color 0.2s ease, transform 0.15s ease; +} + +.cta-secondary:hover { + border-color: rgba(228, 70, 255, 0.3); + transform: translateY(-1px); +} + +/* ── Responsive ── */ + +@media (max-width: 900px) { + .template-grid { + grid-template-columns: 1fr; + } + + .expanded-content { + flex-direction: column; + } + + .expanded-sidebar { + width: 100%; + padding-left: 0; + padding-top: 20px; + flex-direction: row; + flex-wrap: wrap; + gap: 24px; + } + + .sidebar-section { + min-width: 140px; + flex: 1; + } +} diff --git a/packages/homepage/components/sections/tryit-section.tsx b/packages/homepage/components/sections/tryit-section.tsx new file mode 100644 index 0000000..11b5a3a --- /dev/null +++ b/packages/homepage/components/sections/tryit-section.tsx @@ -0,0 +1,261 @@ +'use client' + +import { useState } from 'react' +import { TopologyPreview } from '@/components/flow/topology-preview' +import './tryit-section.css' + +interface TemplateTopo { + lanes: number + nodes: [number, number, string][] + edges: [number, number][] +} + +interface Template { + id: string + title: string + description: string + complexity: 'advanced' | 'showcase' + nodes: number + lanes: number + domain: string + topo: TemplateTopo + laneNames: string[] +} + +const templates: Template[] = [ + { + id: 'e-commerce-fulfillment', + title: 'E-Commerce Fulfillment', + description: + 'End-to-end order lifecycle from cart checkout to last-mile delivery, returns, and loyalty rewards.', + complexity: 'showcase', + nodes: 20, + lanes: 5, + domain: 'Commerce', + laneNames: ['Customer', 'Sales', 'Warehouse', 'Shipping', 'Finance'], + topo: { + lanes: 5, + nodes: [ + [0, 0, 'a'], [1, 4, 'a'], [2, 4, 's'], [3, 4, 'w'], [3, 1, 'a'], + [4, 1, 'a'], [5, 2, 's'], [6, 2, 'w'], [6, 2, 'p'], [7, 2, 'a'], + [8, 3, 's'], [9, 3, 'a'], [10, 3, 'w'], [11, 4, 'a'], [12, 4, 'a'], + [13, 0, 's'], [14, 2, 'a'], [15, 4, 'a'], [16, 0, 't'], [16, 0, 't'], + ], + edges: [ + [0, 1], [1, 2], [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [5, 6], + [6, 7], [6, 8], [7, 9], [8, 9], [9, 10], [10, 11], [11, 12], [12, 13], + [13, 14], [13, 16], [14, 15], [15, 18], [1, 19], + ], + }, + }, + { + id: 'patient-intake', + title: 'Patient Intake', + description: + 'Hospital onboarding covering registration, insurance verification, clinical triage, and provider assignment.', + complexity: 'advanced', + nodes: 14, + lanes: 4, + domain: 'Healthcare', + laneNames: ['Patient', 'Reception', 'Clinical', 'Insurance'], + topo: { + lanes: 4, + nodes: [ + [0, 0, 'a'], [1, 1, 'a'], [2, 1, 's'], [3, 1, 'a'], [3, 1, 'a'], + [4, 1, 'a'], [5, 3, 's'], [6, 1, 'a'], [6, 1, 'a'], [7, 2, 's'], + [8, 2, 'a'], [9, 2, 'a'], [10, 1, 'a'], [11, 0, 't'], [11, 0, 't'], + ], + edges: [ + [0, 1], [1, 2], [2, 3], [2, 4], [3, 5], [4, 5], [5, 6], [6, 7], + [6, 8], [7, 9], [8, 9], [9, 10], [9, 11], [10, 12], [11, 14], [12, 13], + ], + }, + }, + { + id: 'insurance-claims', + title: 'Insurance Claims', + description: + 'Full claims pipeline: submission, adjudication, fraud detection, settlement, payment, and appeals.', + complexity: 'showcase', + nodes: 23, + lanes: 5, + domain: 'Insurance', + laneNames: ['Claimant', 'Agent', 'Adjuster', 'Legal', 'Finance'], + topo: { + lanes: 5, + nodes: [ + [0, 0, 'a'], [1, 1, 'a'], [2, 1, 's'], [3, 1, 's'], [4, 2, 's'], + [5, 2, 'a'], [6, 2, 'w'], [7, 2, 's'], [8, 2, 'a'], [9, 4, 'a'], + [10, 2, 'a'], [11, 2, 's'], [12, 2, 'a'], [12, 3, 'a'], [13, 2, 'a'], + [14, 1, 'a'], [15, 0, 'w'], [16, 1, 's'], [17, 4, 'a'], [18, 3, 'a'], + [19, 3, 's'], [20, 1, 'a'], [21, 0, 't'], [21, 0, 't'], + ], + edges: [ + [0, 1], [1, 2], [2, 3], [2, 8], [3, 4], [3, 8], [4, 5], [4, 8], + [5, 6], [6, 7], [7, 8], [7, 20], [7, 9], [8, 10], [9, 10], [10, 11], + [11, 12], [11, 13], [11, 9], [12, 14], [13, 14], [14, 15], [15, 16], + [16, 17], [16, 18], [16, 10], [17, 21], [18, 19], [19, 15], [19, 20], + [20, 22], + ], + }, + }, +] + +const NODE_TYPE_LABELS: Record = { + a: { label: 'Action', color: '#FF9243' }, + s: { label: 'Switch', color: '#A374FF' }, + p: { label: 'Parallel', color: '#A374FF' }, + w: { label: 'Wait', color: '#F59E0B' }, + e: { label: 'Error', color: '#FF362B' }, + t: { label: 'Terminal', color: '#3FDC77' }, +} + +function getNodeTypeCounts(topo: TemplateTopo) { + const counts: Record = {} + for (const [, , type] of topo.nodes) { + counts[type] = (counts[type] ?? 0) + 1 + } + return counts +} + +export function TryItSection() { + const [selectedId, setSelectedId] = useState(null) + const selected = templates.find((t) => t.id === selectedId) ?? null + + return ( +
+
+

Start from a template

+

+ Pick a service blueprint and explore its structure. Each template is a + real .flowprint.yaml file you can open in the editor. +

+
+ + {/* Template picker */} +
+ {templates.map((tpl) => ( + + ))} +
+ + {/* Expanded preview */} +
+ {selected && ( + <> +
+
+

{selected.title}

+ + {selected.complexity} + +
+

{selected.description}

+
+ +
+ {/* Large topology preview */} +
+ +
+ + {/* Sidebar info */} +
+ {/* Lanes */} +
+

Lanes

+
+ {selected.laneNames.map((name, i) => ( +
+ + {name} +
+ ))} +
+
+ + {/* Node types */} +
+

Node Types

+
+ {Object.entries(getNodeTypeCounts(selected.topo)).map( + ([type, count]) => { + const info = NODE_TYPE_LABELS[type] + if (!info) return null + return ( +
+ + {info.label} + {count} +
+ ) + }, + )} +
+
+ + {/* Quick start */} +
+

Quick Start

+
+ npx flowprint init --template {selected.id} +
+
+
+
+ + )} +
+ + {/* Get started CTAs */} + +
+ ) +} diff --git a/packages/homepage/components/sections/value-propositions.tsx b/packages/homepage/components/sections/value-propositions.tsx new file mode 100644 index 0000000..24154ae --- /dev/null +++ b/packages/homepage/components/sections/value-propositions.tsx @@ -0,0 +1,189 @@ +"use client" + +import { Palette, Table2, Play, Code, GitBranch, CheckCircle, ArrowRight, FileCode } from "lucide-react" + +const businessFeatures = [ + { + icon: Palette, + title: "Design workflows visually", + description: "Define how workflows execute — visually, precisely, without filing tickets.", + }, + { + icon: Table2, + title: "Encode business rules", + description: "Routing logic and business rules in decision tables you own and understand.", + }, + { + icon: Play, + title: "Simulate outcomes", + description: "Verify outcomes before anything is built. See exactly how flows execute.", + }, +] + +const engineeringFeatures = [ + { + icon: Code, + title: "Generate Temporal TypeScript", + description: "Production-ready code from validated specifications. No Flowprint runtime dependency.", + }, + { + icon: CheckCircle, + title: "Validate in CI", + description: "Schema errors, dangling references, structural problems caught before merge.", + }, + { + icon: GitBranch, + title: "Deterministic diffs", + description: "Every diff line is an intentional change. Branch, merge, and review like code.", + }, +] + +export function ValuePropositionsSection() { + return ( +
+ {/* Background gradient */} +
+ +
+ {/* Section header */} +
+

+ One specification.
+ Two perspectives. +

+

+ Business defines the blueprint. Engineering ships it. The artifact is the contract. +

+
+ + {/* Two-panel layout */} +
+ {/* Business Panel */} +
+
+ Business Defines +
+ +
+ {businessFeatures.map((feature, index) => ( +
+
+ +
+
+

{feature.title}

+

{feature.description}

+
+
+ ))} +
+ + {/* Visual preview - Editor canvas mockup */} +
+
+
+ Visual Editor +
+
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+
+
+ + {/* Center artifact */} +
+
+ {/* Arrow from business */} +
+ + {/* Artifact */} +
+
+
+ + .flowprint.yaml +
+
+ + {/* Arrow to engineering */} +
+
+ + {/* Mobile: horizontal arrow */} +
+
+
+
+
+ +
+
+
+
+
+ + {/* Engineering Panel */} +
+
+ Engineering Ships +
+ +
+ {engineeringFeatures.map((feature, index) => ( +
+
+ +
+
+

{feature.title}

+

{feature.description}

+
+
+ ))} +
+ + {/* Visual preview - CLI/Code mockup */} +
+
+
+ Terminal +
+
+

$ flowprint validate

+

✓ Schema valid

+

✓ References resolved

+

$ flowprint generate

+

→ src/workflows/patient-intake.ts

+
+
+
+
+ + {/* Bottom callout */} +
+

+ The artifact is the interface. No meetings to align. No tickets to clarify. The specification speaks for itself. +

+
+
+
+ ) +} diff --git a/packages/homepage/components/theme-provider.tsx b/packages/homepage/components/theme-provider.tsx new file mode 100644 index 0000000..55c2f6e --- /dev/null +++ b/packages/homepage/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/packages/homepage/components/ui/accordion.tsx b/packages/homepage/components/ui/accordion.tsx new file mode 100644 index 0000000..e538a33 --- /dev/null +++ b/packages/homepage/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +'use client' + +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { ChevronDownIcon } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180', + className, + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/packages/homepage/components/ui/alert-dialog.tsx b/packages/homepage/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..9704452 --- /dev/null +++ b/packages/homepage/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +'use client' + +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/packages/homepage/components/ui/alert.tsx b/packages/homepage/components/ui/alert.tsx new file mode 100644 index 0000000..e6751ab --- /dev/null +++ b/packages/homepage/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/packages/homepage/components/ui/aspect-ratio.tsx b/packages/homepage/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..40bb120 --- /dev/null +++ b/packages/homepage/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/packages/homepage/components/ui/avatar.tsx b/packages/homepage/components/ui/avatar.tsx new file mode 100644 index 0000000..aa98465 --- /dev/null +++ b/packages/homepage/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '@/lib/utils' + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/packages/homepage/components/ui/badge.tsx b/packages/homepage/components/ui/badge.tsx new file mode 100644 index 0000000..fc4126b --- /dev/null +++ b/packages/homepage/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/packages/homepage/components/ui/breadcrumb.tsx b/packages/homepage/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..1750ff2 --- /dev/null +++ b/packages/homepage/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { ChevronRight, MoreHorizontal } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return