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/error.tsx b/packages/homepage/app/error.tsx new file mode 100644 index 0000000..37f8cd1 --- /dev/null +++ b/packages/homepage/app/error.tsx @@ -0,0 +1,75 @@ +'use client' + +import { useEffect } from 'react' + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + console.error('Unhandled error:', error) + }, [error]) + + return ( +
+
+

+ Something went wrong +

+

+ An unexpected error occurred. +

+ +
+
+ ) +} diff --git a/packages/homepage/app/globals.css b/packages/homepage/app/globals.css new file mode 100644 index 0000000..733f0e1 --- /dev/null +++ b/packages/homepage/app/globals.css @@ -0,0 +1,295 @@ +@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; +} + +/* Dark scrollbar for all scrollable elements */ +* { + scrollbar-width: thin; + scrollbar-color: rgba(228, 70, 255, 0.15) transparent; +} + +*::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background: rgba(228, 70, 255, 0.15); + border-radius: 3px; +} + +*::-webkit-scrollbar-thumb:hover { + background: rgba(228, 70, 255, 0.3); +} + +@theme inline { + --font-sans: 'DM Sans', system-ui, sans-serif; + --font-serif: var(--font-instrument-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); + + /* Bridge theme — surface & text */ + --color-surface: var(--surface); + --color-surface-elevated: var(--surface-elevated); + --color-surface-border: var(--surface-border); + --color-code-bg: var(--code-bg); + --color-table-header: var(--table-header); + --color-fg: var(--text-primary); + --color-fg-secondary: var(--text-secondary); + --color-fg-muted: var(--text-muted); + + /* Bridge theme — accent variants */ + --color-accent-dim: var(--accent-dim); + --color-accent-glow: var(--accent-glow); + + /* Bridge theme — syntax & status colors */ + --color-true-green: var(--true-green); + --color-false-red: var(--false-red); + --color-keyword-blue: var(--keyword-blue); + --color-string-amber: var(--string-amber); + --color-type-teal: var(--type-teal); + + /* Bridge theme — node/edge type colors */ + --color-node-action: var(--node-type-action); + --color-node-switch: var(--node-type-switch); + --color-node-terminal: var(--node-type-terminal); + + /* Custom easing */ + --ease-out-expo: cubic-bezier(0.23, 1, 0.32, 1); + + /* Shared bridge card shadow */ + --shadow-bridge-card: 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); +} + +@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); } +} + +@keyframes bridge-badge-pulse-dot { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.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..3864f64 --- /dev/null +++ b/packages/homepage/app/layout.tsx @@ -0,0 +1,67 @@ +import type { Metadata, Viewport } from 'next' +import { DM_Sans, JetBrains_Mono, Instrument_Serif } from 'next/font/google' +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', +}) + +const instrumentSerif = Instrument_Serif({ + subsets: ['latin'], + weight: ['400'], + style: ['normal', 'italic'], + variable: '--font-instrument-serif', +}) + +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-svg.css b/packages/homepage/components/bridges/bridge-decision-svg.css new file mode 100644 index 0000000..de39a15 --- /dev/null +++ b/packages/homepage/components/bridges/bridge-decision-svg.css @@ -0,0 +1,87 @@ +/* Bridge Decision — SVG arrows, particles, and GSAP animation states */ + +/* ── Arrow states (GSAP toggles .active) ─────────── */ +.bridge-decision .arrow-line { + stroke: var(--fg-muted); + stroke-width: 1.5; + opacity: 0.3; + transition: opacity 0.4s, stroke 0.4s; +} + +.bridge-decision .arrow-head { + fill: var(--fg-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; +} + +/* ── Flow particle (positioned dot with glow) ────── */ +.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; +} + +/* ── JSON box initial states (GSAP targets) ──────── */ +.bridge-decision .flow-json--input { + opacity: 0; + transform: translateX(-20px); +} + +.bridge-decision .flow-json--output { + opacity: 0; + transform: translateX(20px); +} + +/* ── JSON box active glow (GSAP sets inline) ─────── */ +.bridge-decision .flow-json--active { + border-color: rgba(228, 70, 255, 0.3); + box-shadow: 0 0 20px rgba(228, 70, 255, 0.1); +} + +/* ── Table row scan states (GSAP toggles) ────────── */ +.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(--fg); +} + +.bridge-decision .flow-table tbody tr.row-dim td { + opacity: 0.35; +} + +/* ── Particle keyframe ───────────────────────────── */ +@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..50f0d33 --- /dev/null +++ b/packages/homepage/components/bridges/bridge-decision.tsx @@ -0,0 +1,506 @@ +'use client' + +import { useState, useRef, useCallback } from 'react' +import gsap from 'gsap' +import { useGSAP } from '@gsap/react' +import { cn } from '@/lib/utils' +import { Badge } from './shared/badge' +import { ViewTransition } from './shared/view-transition' +import '../flow/flow.css' +import './bridge-decision-svg.css' + +gsap.registerPlugin(useGSAP) + +interface BridgeDecisionProps { + perspective: 'business' | 'developer' +} + +const DECISION_ROWS = [ + { tier: 'Enterprise', value: 'Any', valueClass: 'any' as const, route: 'express' }, + { tier: 'Business', value: '> $10k', valueClass: 'input' as const, route: 'review' }, + { tier: 'Business', value: '\u2264 $10k', valueClass: 'input' as const, route: 'standard' }, + { tier: 'Starter', value: 'Any', valueClass: 'any' as const, route: 'standard' }, +] + +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.', + }, +} + +const VALUE_CLASSES = { + input: 'text-node-action', + any: 'text-fg-muted italic', +} as const + +export function BridgeDecision({ perspective }: BridgeDecisionProps) { + const isDev = perspective === 'developer' + const [hitPolicy, setHitPolicy] = useState('first') + + // Text content state (kept as React state for rendering) + const [inputTier, setInputTier] = useState("'enterprise'") + const [inputValue, setInputValue] = useState('50,000') + const [outputRoute, setOutputRoute] = useState("'express'") + + const containerRef = useRef(null) + const inputJsonRef = useRef(null) + const outputJsonRef = useRef(null) + const particleInRef = useRef(null) + const particleOutRef = useRef(null) + + // useDynamicHeight is now handled internally by ViewTransition + + const animateParticle = useCallback( + (particle: HTMLDivElement | null, onComplete?: () => void) => { + if (!particle) return + const parent = particle.parentElement + if (!parent) return + const width = parent.offsetWidth + + gsap.fromTo( + particle, + { left: 0, opacity: 0 }, + { + left: width - 6, + duration: 0.4, + ease: 'power3.out', + keyframes: { + opacity: [0, 1, 1, 0], + }, + onComplete: () => { + gsap.set(particle, { opacity: 0 }) + onComplete?.() + }, + }, + ) + }, + [], + ) + + useGSAP( + () => { + if (!isDev) return + + const inputJson = inputJsonRef.current + const outputJson = outputJsonRef.current + const container = containerRef.current + if (!inputJson || !outputJson || !container) return + + const rows = container.querySelectorAll('.flow-table tbody tr') + if (!rows.length) return + + let caseIndex = 0 + + const runCycle = () => { + const tc = testCases[caseIndex % testCases.length] + + // Update text content via React state + setInputTier(`'${tc.tier}'`) + setInputValue(tc.value.toLocaleString()) + setOutputRoute(`'${tc.route}'`) + + const tl = gsap.timeline({ + onComplete: () => { + caseIndex++ + gsap.delayedCall(0.4, runCycle) + }, + }) + + // Phase 1: Input slides in (0s) + tl.fromTo( + inputJson, + { opacity: 0, x: -20 }, + { opacity: 1, x: 0, duration: 0.5, ease: 'power2.out' }, + 0, + ).set( + inputJson, + { + borderColor: 'rgba(228, 70, 255, 0.3)', + boxShadow: '0 0 12px rgba(228, 70, 255, 0.15)', + }, + 0, + ) + + // Phase 2: Arrow in + particle (0.6s) + tl.call( + () => { + container.querySelectorAll('.arrow-line').forEach((el, i) => { + if (i === 0) el.classList.add('active') + }) + container.querySelectorAll('.arrow-head').forEach((el, i) => { + if (i === 0) el.classList.add('active') + }) + animateParticle(particleInRef.current) + }, + [], + 0.6, + ) + + // Phase 3: Table rows scan (1.2s, staggered 0.2s each) + rows.forEach((_, i) => { + tl.call( + () => { + rows.forEach((r, j) => { + r.classList.remove('row-match', 'row-dim') + if (j === i) r.classList.add('row-match') + }) + }, + [], + 1.2 + i * 0.2, + ) + }) + + // Phase 4: Settle on matched row (2.2s) + tl.call( + () => { + rows.forEach((r, i) => { + r.classList.remove('row-match', 'row-dim') + r.classList.add(i === tc.matchRow - 1 ? 'row-match' : 'row-dim') + }) + }, + [], + 2.2, + ) + + // Phase 5: Arrow out + particle (2.8s) + tl.call( + () => { + container.querySelectorAll('.arrow-line').forEach((el, i) => { + if (i === 1) el.classList.add('active') + }) + container.querySelectorAll('.arrow-head').forEach((el, i) => { + if (i === 1) el.classList.add('active') + }) + animateParticle(particleOutRef.current) + }, + [], + 2.8, + ) + + // Phase 6: Output appears (3.2s) + tl.fromTo( + outputJson, + { opacity: 0, x: 20 }, + { opacity: 1, x: 0, duration: 0.5, ease: 'power2.out' }, + 3.2, + ).set( + outputJson, + { + borderColor: 'rgba(228, 70, 255, 0.3)', + boxShadow: '0 0 12px rgba(228, 70, 255, 0.15)', + }, + 3.2, + ) + + // Phase 7: Hold then reset (4.8s) + tl.call( + () => { + gsap.set(inputJson, { + opacity: 0, + x: -20, + clearProps: 'borderColor,boxShadow', + }) + gsap.set(outputJson, { + opacity: 0, + x: 20, + clearProps: 'borderColor,boxShadow', + }) + container.querySelectorAll('.arrow-line').forEach((el) => { + el.classList.remove('active') + }) + container.querySelectorAll('.arrow-head').forEach((el) => { + el.classList.remove('active') + }) + rows.forEach((r) => r.classList.remove('row-match', 'row-dim')) + gsap.set([particleInRef.current, particleOutRef.current], { + opacity: 0, + }) + }, + [], + 4.8, + ) + } + + gsap.delayedCall(0.3, runCycle) + + // Cleanup: remove DOM classes that GSAP context.revert() won't catch + return () => { + container.querySelectorAll('.arrow-line, .arrow-head').forEach((el) => { + el.classList.remove('active') + }) + rows.forEach((r) => r.classList.remove('row-match', 'row-dim')) + } + }, + { scope: containerRef, dependencies: [isDev, animateParticle] }, + ) + + 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 */} + +
+
+ + Hit Policy + +
+
+ {['first', 'collect', 'all', 'priority'].map((policy) => ( + + ))} +
+
+ +
+ + + + + + + + + + + {DECISION_ROWS.map((row, i) => ( + + + + + + + ))} + +
+ # + + Tier + + input + + + Value + + input + + + Route + + output + +
+ {i + 1} + + + {row.tier} + + + + {row.value} + + + + {row.route} + +
+
+ +
+ + {policyInfo.icon} + + {policyInfo.text} +
+ + } + devContent={ + <> +
+ {/* Input JSON */} +
+ {'{'} +
+    + tier + :{' '} + {inputTier} + , +
+    + value + :{' '} + {inputValue} +
+ {'}'} +
+ + {/* Arrow in */} +
+ + + + +
+
+ + {/* Decision Table */} +
+ + + + + + + + + + {DECISION_ROWS.map((row, i) => ( + + + + + + ))} + +
+ Tier + + Value + + Route +
+ {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-svg.css b/packages/homepage/components/bridges/bridge-simulation-svg.css new file mode 100644 index 0000000..8ba9f94 --- /dev/null +++ b/packages/homepage/components/bridges/bridge-simulation-svg.css @@ -0,0 +1,493 @@ +/* Bridge Simulation — SVG styles, keyframes, and state classes + that cannot be expressed as Tailwind utilities. + Scoped under .bridge-simulation */ + +/* ══════════════════════════════════════════════════════ + VIEW LAYERS (CSS-based transitions tied to .show-dev) + ══════════════════════════════════════════════════════ */ +.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; +} + +/* ══════════════════════════════════════════════════════ + 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 ── */ +.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 + ══════════════════════════════════════════════════════ */ +.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 ── */ +.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); +} + +/* ── Debug active (step-by-step) ── */ +.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 ── */ +.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 states ── */ +.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; +} + +.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; +} + +.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 CONTROLS — display toggle + ══════════════════════════════════════════════════════ */ +.bridge-simulation .step-controls { + display: none; +} + +.bridge-simulation .step-controls.visible { + display: flex; +} + +/* ══════════════════════════════════════════════════════ + SCENARIO LABEL — display toggle + color variants + ══════════════════════════════════════════════════════ */ +.bridge-simulation .scenario-label { + display: none; +} + +.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; +} + +/* ══════════════════════════════════════════════════════ + TERMINAL LINE REVEAL + ══════════════════════════════════════════════════════ */ +.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); +} + +/* ── Terminal syntax tokens ── */ +.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 DOT — pulse + variants + ══════════════════════════════════════════════════════ */ +.bridge-simulation .status-dot { + animation: bridge-sim-pulse-dot 2s ease-in-out infinite; +} + +.bridge-simulation .status-dot.paused { + background: var(--string-amber); + animation: none; +} + +.bridge-simulation .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); } +} + +/* ══════════════════════════════════════════════════════ + KEYFRAMES + ══════════════════════════════════════════════════════ */ +@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; + } +} + +@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; +} + +@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; +} + +@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; } + +/* ══════════════════════════════════════════════════════ + RESPONSIVE BREAKPOINTS + ══════════════════════════════════════════════════════ */ +@media (max-width: 840px) { + .bridge-simulation .view { + padding: 24px 24px 20px; + } +} + +@media (max-width: 640px) { + .bridge-simulation .view { + padding: 20px 16px 16px; + } + + .bridge-simulation .term-node-name { + min-width: 100px; + } +} diff --git a/packages/homepage/components/bridges/bridge-simulation.tsx b/packages/homepage/components/bridges/bridge-simulation.tsx new file mode 100644 index 0000000..bb0ec1e --- /dev/null +++ b/packages/homepage/components/bridges/bridge-simulation.tsx @@ -0,0 +1,843 @@ +'use client' + +import { useState, useRef, useCallback } from 'react' +import gsap from 'gsap' +import { useGSAP } from '@gsap/react' +import { cn } from '@/lib/utils' +import { useDynamicHeight } from '@/hooks/use-dynamic-height' +import { WalkthroughSvg } from './walkthrough-svg' +import { StepByStepSvg } from './step-by-step-svg' +import { WhatIfSvg } from './what-if-svg' +import { Badge } from './shared/badge' +import { BridgeCTA } from './shared/bridge-cta' +import { + walkthroughPath, + stepByStepPath, + whatIfPathA, + whatIfPathB, + fileNames, + nodeCenters, + type BridgeSimulationProps, + type RippleObj, + type SimMode, +} from './simulation-data' +import './bridge-simulation-svg.css' +import '../flow/flow.css' + +gsap.registerPlugin(useGSAP) + +// ══════════════════════════════════════════════════════ +// DOM HELPERS — target SVG elements via data attributes +// ══════════════════════════════════════════════════════ + +const q = { + node: (el: Element, id: string) => el.querySelector(`[data-node-id="${id}"]`), + edge: (el: Element, id: string) => el.querySelector(`[data-edge-id="${id}"]`), + tooltip: (el: Element, id: string) => el.querySelector(`[data-tooltip-id="${id}"]`), + edgeLabel: (el: Element, id: string) => el.querySelector(`[data-edge-label="${id}"]`), +} + +/** Get the mutable className string for an SVG or HTML element */ +function getBaseClassName(el: Element): string { + const cn = el.className + if (typeof cn === 'string') return cn + // SVGAnimatedString + return (cn as SVGAnimatedString).baseVal +} + +function setBaseClassName(el: Element, val: string) { + const cn = el.className + if (typeof cn === 'string') { + // HTML element + ;(el as HTMLElement).className = val + } else { + // SVG element + ;(cn as SVGAnimatedString).baseVal = val + } +} + +function setNodeClass(el: Element, nodeId: string, cls: string) { + const g = q.node(el, nodeId) + if (!g) return + const base = getBaseClassName(g).replace(/\s*(active|visited|debug-active|active-error)\b/g, '') + setBaseClassName(g, cls ? `${base} ${cls}` : base) +} + +function clearNodeClass(el: Element, nodeId: string) { + setNodeClass(el, nodeId, '') +} + +function setEdgeClass(el: Element, edgeId: string, cls: string) { + const e = q.edge(el, edgeId) + if (!e) return + const base = getBaseClassName(e).replace(/\s*(active-edge|visited-edge|debug-edge|debug-visited-edge|error-edge|error-visited-edge)\b/g, '') + setBaseClassName(e, cls ? `${base} ${cls}` : base) +} + +function clearEdgeClass(el: Element, edgeId: string) { + setEdgeClass(el, edgeId, '') +} + +function setTooltipVisible(el: Element, tooltipId: string, visible: boolean) { + const t = q.tooltip(el, tooltipId) + if (!t) return + if (visible) { + t.classList.add('visible') + } else { + t.classList.remove('visible') + } +} + +/** Reset all node/edge/tooltip classes in a container to their base state */ +function resetSvgClasses(el: Element) { + el.querySelectorAll('[data-node-id]').forEach(g => { + const base = getBaseClassName(g).replace(/\s*(active|visited|debug-active|active-error)\b/g, '') + setBaseClassName(g, base) + }) + el.querySelectorAll('[data-edge-id]').forEach(e => { + const base = getBaseClassName(e).replace(/\s*(active-edge|visited-edge|debug-edge|debug-visited-edge|error-edge|error-visited-edge)\b/g, '') + setBaseClassName(e, base) + }) + el.querySelectorAll('[data-tooltip-id]').forEach(t => { + t.classList.remove('visible') + }) +} + +// ══════════════════════════════════════════════════════ +// COMPONENT +// ══════════════════════════════════════════════════════ + +export function BridgeSimulation({ perspective }: BridgeSimulationProps) { + const [showDev, setShowDev] = useState(perspective === 'developer') + const [currentMode, setCurrentMode] = useState('walkthrough') + const [statusText, setStatusText] = useState('Simulating...') + const [activeSvg, setActiveSvg] = useState('walkthrough') + const [stepIndex, setStepIndex] = useState(-1) + const [scenarioLabelText, setScenarioLabelText] = useState('') + + // Ripple state — kept as React state (CSS keyframe-animated) + const [wtRipples, setWtRipples] = useState([]) + const [sbRipples, setSbRipples] = useState([]) + const [wiRipples, setWiRipples] = useState([]) + + // Developer terminal + const [revealedLines, setRevealedLines] = useState>(new Set()) + + // Refs + const rippleIdCounter = useRef(0) + const simRef = useRef(null) + const bizTlRef = useRef(null) + const devTlRef = useRef(null) + const stepIndexRef = useRef(-1) + const currentModeRef = useRef('walkthrough') + const showDevRef = useRef(perspective === 'developer') + + const { containerRef, bizRef: bizViewRef, devRef: devViewRef } = useDynamicHeight(showDev) + + // ══════════════════════════════════════════════════════ + // RIPPLE EFFECT (React state + CSS keyframes) + // ══════════════════════════════════════════════════════ + 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]) + + // Auto-remove after animation + setTimeout(() => { + const ids = new Set(newRipples.map(r => r.id)) + setter(prev => prev.filter(p => !ids.has(p.id))) + }, 1800) + }, []) + + // ══════════════════════════════════════════════════════ + // RESET HELPERS + // ══════════════════════════════════════════════════════ + const resetFraudNode = useCallback(() => { + const el = simRef.current + if (!el) return + const fraud = q.node(el, 'wi-node-fraud') as SVGGElement | null + if (fraud) { + gsap.set(fraud, { opacity: 0, scale: 0, transformOrigin: '780px 325px' }) + const rect = fraud.querySelector('.node-rect') + if (rect) gsap.set(rect, { attr: { style: 'stroke: rgba(255,146,67,0.3)' }, clearProps: 'filter' }) + } + // Hide investigate edges and label + const invH = q.edge(el, 'wi-edge-assess-investigate-h') + const invV = q.edge(el, 'wi-edge-assess-investigate-v') + const invLabel = q.edgeLabel(el, 'investigate') + if (invH) gsap.set(invH, { opacity: 0 }) + if (invV) gsap.set(invV, { opacity: 0 }) + if (invLabel) gsap.set(invLabel, { opacity: 0 }) + }, []) + + const resetAllVisuals = useCallback(() => { + const el = simRef.current + if (!el) return + resetSvgClasses(el) + setWtRipples([]) + setSbRipples([]) + setWiRipples([]) + resetFraudNode() + }, [resetFraudNode]) + + const killBizTimeline = useCallback(() => { + if (bizTlRef.current) { + bizTlRef.current.kill() + bizTlRef.current = null + } + }, []) + + const killDevTimeline = useCallback(() => { + if (devTlRef.current) { + devTlRef.current.kill() + devTlRef.current = null + } + }, []) + + // ══════════════════════════════════════════════════════ + // MODE 1: WALK-THROUGH (GSAP timeline) + // ══════════════════════════════════════════════════════ + const runWalkthrough = useCallback(() => { + const el = simRef.current + if (!el) return + + const path = walkthroughPath + const delay = 0.5 + + const tl = gsap.timeline({ + onComplete: () => { + if (showDevRef.current || currentModeRef.current !== 'walkthrough') return + resetAllVisuals() + setActiveSvg('walkthrough') + // Small gap before looping + gsap.delayedCall(0.1, () => { + if (!showDevRef.current && currentModeRef.current === 'walkthrough') { + runWalkthrough() + } + }) + }, + }) + bizTlRef.current = tl + + // Animate through each node + path.nodes.forEach((nodeId, i) => { + tl.call(() => { + // Mark previous as visited + if (i > 0) { + setNodeClass(el, path.nodes[i - 1], 'visited') + if (path.edges[i - 1]) setEdgeClass(el, path.edges[i - 1], 'visited-edge') + } + // Activate current + setNodeClass(el, nodeId, 'active') + if (path.edges[i]) setEdgeClass(el, 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`) + }, undefined, i === 0 ? 0 : `>+=${delay}`) + }) + + // Mark last as visited + tl.call(() => { + const lastNode = path.nodes[path.nodes.length - 1] + setNodeClass(el, lastNode, 'visited') + const lastEdge = path.edges[path.edges.length - 1] + if (lastEdge) setEdgeClass(el, lastEdge, 'visited-edge') + setStatusText(`Walking: complete \u2022 ${path.nodes.length}/${path.nodes.length} nodes`) + }, undefined, `>+=${delay}`) + + // Hold before loop + tl.call(() => {}, undefined, '+=1.5') + }, [spawnRipple, resetAllVisuals]) + + // ══════════════════════════════════════════════════════ + // MODE 2: STEP-BY-STEP (user-driven) + // ══════════════════════════════════════════════════════ + const stepForward = useCallback(() => { + const el = simRef.current + if (!el) return + + 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) { + setNodeClass(el, path.nodes[idx], 'visited') + if (path.tooltips[idx]) setTooltipVisible(el, path.tooltips[idx]!, false) + if (path.edges[idx]) setEdgeClass(el, path.edges[idx], 'debug-visited-edge') + } + + const newIdx = idx + 1 + stepIndexRef.current = newIdx + setStepIndex(newIdx) + + // Activate new node + setNodeClass(el, 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(el, path.tooltips[newIdx]!, true) + + // Activate leading edge + if (path.edges[newIdx]) setEdgeClass(el, path.edges[newIdx], 'debug-edge') + + const nodeName = path.labels[newIdx] + setStatusText(`Step ${newIdx + 1} of ${path.nodes.length} \u2022 ${nodeName}`) + }, [spawnRipple]) + + const stepBackward = useCallback(() => { + const el = simRef.current + if (!el) return + + const path = stepByStepPath + const idx = stepIndexRef.current + if (idx <= 0) return + + // Remove current node activation + clearNodeClass(el, path.nodes[idx]) + if (path.tooltips[idx]) setTooltipVisible(el, path.tooltips[idx]!, false) + if (path.edges[idx]) clearEdgeClass(el, path.edges[idx]) + + const newIdx = idx - 1 + stepIndexRef.current = newIdx + setStepIndex(newIdx) + + // Re-activate previous node + setNodeClass(el, path.nodes[newIdx], 'debug-active') + if (path.tooltips[newIdx]) setTooltipVisible(el, path.tooltips[newIdx]!, true) + if (path.edges[newIdx]) setEdgeClass(el, 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) + setStatusText('Step-by-step \u2022 click Next to begin') + // Auto-advance to first step after a tick + gsap.delayedCall(0, () => stepForward()) + }, [stepForward]) + + // ══════════════════════════════════════════════════════ + // MODE 3: WHAT-IF (GSAP timeline) + // ══════════════════════════════════════════════════════ + const runWhatIf = useCallback(() => { + const el = simRef.current + if (!el) return + + const pathA = whatIfPathA + const pathB = whatIfPathB + const delayA = 0.45 + const delayB = 0.5 + + const tl = gsap.timeline({ + onComplete: () => { + if (showDevRef.current || currentModeRef.current !== 'whatif') return + gsap.delayedCall(0.5, () => { + if (!showDevRef.current && currentModeRef.current === 'whatif') { + resetAllVisuals() + setActiveSvg('whatif') + gsap.delayedCall(0.1, () => runWhatIf()) + } + }) + }, + }) + bizTlRef.current = tl + + // ── SCENARIO A ── + tl.call(() => { + setScenarioLabelText('Scenario A: Normal Claim') + }) + + pathA.nodes.forEach((nodeId, i) => { + tl.call(() => { + if (i > 0) { + setNodeClass(el, pathA.nodes[i - 1], 'visited') + if (pathA.edges[i - 1]) setEdgeClass(el, pathA.edges[i - 1], 'visited-edge') + } + setNodeClass(el, nodeId, 'active') + if (pathA.edges[i]) setEdgeClass(el, pathA.edges[i], 'active-edge') + const center = nodeCenters[nodeId] + if (center) spawnRipple(center.x, center.y, '#3FDC77', 'whatif') + setStatusText(`Scenario A \u2022 ${pathA.labels[i]} \u2022 ${i + 1}/${pathA.nodes.length}`) + }, undefined, i === 0 ? 0 : `>+=${delayA}`) + }) + + // Mark last node visited + tl.call(() => { + const lastNode = pathA.nodes[pathA.nodes.length - 1] + setNodeClass(el, lastNode, 'visited') + const lastEdge = pathA.edges[pathA.edges.length - 1] + if (lastEdge) setEdgeClass(el, lastEdge, 'visited-edge') + setStatusText('Scenario A \u2022 All nodes passed') + }, undefined, `>+=${delayA}`) + + // Pause, then transition to Scenario B + tl.call(() => { + if (showDevRef.current || currentModeRef.current !== 'whatif') { tl.kill(); return } + // Reset for scenario B + resetSvgClasses(el) + resetFraudNode() + setWiRipples([]) + setActiveSvg('whatif') + setScenarioLabelText('Scenario B: + Fraud Detection') + setStatusText('Adding Fraud Check node...') + }, undefined, '+=1.5') + + // ── SCENARIO B: Show fraud node ── + tl.call(() => { + const fraud = q.node(el, 'wi-node-fraud') as SVGGElement | null + if (!fraud) return + gsap.to(fraud, { + opacity: 1, + scale: 1, + duration: 0.6, + ease: 'back.out(1.7)', + transformOrigin: '780px 325px', + }) + const rect = fraud.querySelector('.node-rect') + if (rect) { + gsap.to(rect, { + attr: { style: 'stroke: #3FDC77; filter: drop-shadow(0 0 16px rgba(63, 220, 119, 0.7))' }, + duration: 0.3, + }) + gsap.to(rect, { + attr: { style: 'stroke: #3FDC77; filter: drop-shadow(0 0 8px rgba(63, 220, 119, 0.3))' }, + duration: 0.3, + delay: 0.4, + }) + gsap.to(rect, { + attr: { style: 'stroke: #3FDC77; filter: drop-shadow(0 0 16px rgba(63, 220, 119, 0.7))' }, + duration: 0.3, + delay: 0.8, + }) + gsap.to(rect, { + attr: { style: 'stroke: rgba(255,146,67,0.3)' }, + duration: 0.3, + delay: 1.2, + clearProps: 'filter', + }) + } + }, undefined, '+=0.4') + + // Show investigate edges + label + tl.call(() => { + const invH = q.edge(el, 'wi-edge-assess-investigate-h') + const invV = q.edge(el, 'wi-edge-assess-investigate-v') + const invLabel = q.edgeLabel(el, 'investigate') + if (invH) gsap.to(invH, { opacity: 1, duration: 0.4 }) + if (invV) gsap.to(invV, { opacity: 1, duration: 0.4 }) + if (invLabel) gsap.to(invLabel, { opacity: 1, duration: 0.4 }) + }, undefined, '+=0.3') + + // Wait for fraud node animation to settle + tl.call(() => {}, undefined, '+=1.0') + + // ── SCENARIO B: Run path through to error ── + const bStartLabel = 'scenarioB' + pathB.nodes.forEach((nodeId, i) => { + const isLast = i === pathB.nodes.length - 1 + + tl.call(() => { + if (i > 0) { + setNodeClass(el, pathB.nodes[i - 1], 'visited') + if (pathB.edges[i - 1]) setEdgeClass(el, pathB.edges[i - 1], 'visited-edge') + } + + if (isLast) { + // ERROR STATE + setNodeClass(el, nodeId, 'active-error') + const center = nodeCenters[nodeId] + if (center) { + spawnRipple(center.x, center.y, '#FF362B', 'whatif') + setTimeout(() => spawnRipple(center.x, center.y, '#FF362B', 'whatif'), 300) + setTimeout(() => spawnRipple(center.x, center.y, '#FF362B', 'whatif'), 600) + } + // Show error tooltip + setTimeout(() => { + const errTip = q.tooltip(el, 'wi-error-tooltip') + if (errTip) errTip.classList.add('visible') + }, 200) + setStatusText('Scenario B \u2022 ERROR at Fraud Check') + setScenarioLabelText('Scenario B: + Fraud Detection \u2192 Alert detected') + } else { + setNodeClass(el, nodeId, 'active') + const center = nodeCenters[nodeId] + if (center) spawnRipple(center.x, center.y, '#3FDC77', 'whatif') + if (pathB.edges[i]) setEdgeClass(el, pathB.edges[i], 'active-edge') + setStatusText(`Scenario B \u2022 ${pathB.labels[i]} \u2022 ${i + 1}/${pathB.nodes.length}`) + } + }, undefined, i === 0 ? `${bStartLabel}` : `>+=${delayB}`) + }) + + // Hold on error for 3 seconds before looping + tl.call(() => {}, undefined, '+=3.0') + }, [spawnRipple, resetAllVisuals, resetFraudNode]) + + // ══════════════════════════════════════════════════════ + // DEVELOPER VIEW (GSAP timeline) + // ══════════════════════════════════════════════════════ + const startDevAnimation = useCallback(() => { + killDevTimeline() + setRevealedLines(new Set()) + + const delays = [0, 0.2, 0.6, 0.9, 1.2, 1.5, 1.8, 2.1, 2.4, 2.7, 3.0, 3.3] + + const tl = gsap.timeline({ + onComplete: () => { + if (showDevRef.current) { + gsap.delayedCall(2.2, () => { + if (showDevRef.current) startDevAnimation() + }) + } + }, + }) + devTlRef.current = tl + + delays.forEach((d, i) => { + tl.call(() => { + setRevealedLines(prev => new Set([...prev, i])) + }, undefined, d) + }) + }, [killDevTimeline]) + + // ══════════════════════════════════════════════════════ + // SIMULATION CONTROLLER + // ══════════════════════════════════════════════════════ + const startSimulation = useCallback((mode: SimMode) => { + killBizTimeline() + resetAllVisuals() + setActiveSvg(mode) + + // Defer start to let React flush the SVG swap + gsap.delayedCall(0, () => { + switch (mode) { + case 'walkthrough': + runWalkthrough() + break + case 'stepbystep': + initStepByStep() + break + case 'whatif': + runWhatIf() + break + } + }) + }, [killBizTimeline, resetAllVisuals, runWalkthrough, initStepByStep, runWhatIf]) + + // ══════════════════════════════════════════════════════ + // VIEW TOGGLE HANDLERS + // ══════════════════════════════════════════════════════ + const handleShowDev = useCallback(() => { + setShowDev(true) + showDevRef.current = true + killBizTimeline() + resetAllVisuals() + startDevAnimation() + }, [killBizTimeline, resetAllVisuals, startDevAnimation]) + + const handleShowBusiness = useCallback(() => { + setShowDev(false) + showDevRef.current = false + killDevTimeline() + setRevealedLines(new Set()) + startSimulation(currentModeRef.current) + }, [killDevTimeline, startSimulation]) + + // ══════════════════════════════════════════════════════ + // PILL TAB HANDLER + // ══════════════════════════════════════════════════════ + const handleTabClick = useCallback((mode: SimMode) => { + setCurrentMode(mode) + currentModeRef.current = mode + killBizTimeline() + resetAllVisuals() + startSimulation(mode) + }, [killBizTimeline, resetAllVisuals, startSimulation]) + + // ══════════════════════════════════════════════════════ + // INIT + CLEANUP via useGSAP + // ══════════════════════════════════════════════════════ + useGSAP(() => { + if (perspective === 'developer') { + startDevAnimation() + } else { + startSimulation('walkthrough') + } + + return () => { + killBizTimeline() + killDevTimeline() + gsap.killTweensOf('*') + } + }, { scope: simRef, dependencies: [] }) + + // Sync showDev with parent perspective prop + const prevPerspective = useRef(perspective) + useGSAP(() => { + if (prevPerspective.current === perspective) return + prevPerspective.current = perspective + + const isDev = perspective === 'developer' + setShowDev(isDev) + showDevRef.current = isDev + + if (isDev) { + killBizTimeline() + resetAllVisuals() + startDevAnimation() + } else { + killDevTimeline() + setRevealedLines(new Set()) + startSimulation(currentModeRef.current) + } + }, { dependencies: [perspective] }) + + // ══════════════════════════════════════════════════════ + // KEYBOARD SUPPORT (scoped to container) + // ══════════════════════════════════════════════════════ + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (currentModeRef.current !== 'stepbystep' || showDevRef.current) return + if (e.key === 'ArrowRight' || e.key === ' ') { + e.preventDefault() + stepForward() + } else if (e.key === 'ArrowLeft') { + e.preventDefault() + stepBackward() + } + }, [stepForward, stepBackward]) + + // ══════════════════════════════════════════════════════ + // DERIVED STATE + // ══════════════════════════════════════════════════════ + const statusDotClass = + currentMode === 'stepbystep' && !showDev ? 'paused' : + scenarioLabelText.includes('ERROR') || scenarioLabelText.includes('Alert') ? 'error' : '' + const scenarioLabelClass = scenarioLabelText.includes('Scenario B') ? 'scenario-b' : scenarioLabelText.includes('Scenario A') ? 'scenario-a' : '' + const scenarioLabelVisible = scenarioLabelText.length > 0 && currentMode === 'whatif' && !showDev + const stepControlsVisible = currentMode === 'stepbystep' && !showDev + + // ══════════════════════════════════════════════════════ + // RENDER + // ══════════════════════════════════════════════════════ + return ( +
{ + // Share the ref between simRef and containerRef (from useDynamicHeight) + simRef.current = node + // containerRef from useDynamicHeight is a MutableRefObject + ;(containerRef as { current: HTMLDivElement | null }).current = node + }} + className={cn( + 'bridge-simulation relative w-[min(780px,calc(100vw-48px))] max-w-full rounded-[20px] bg-surface border border-surface-border shadow-bridge-card overflow-hidden transition-[height] duration-500 ease-out-expo outline-none', + showDev && 'show-dev', + )} + tabIndex={0} + onKeyDown={handleKeyDown} + > + + {/* ===== 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} +
+ + + + + + + +
+ + {/* 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'} + +
+
+ + + See the developer view + + + + + + +
+ + {/* ===== 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 +
+
+ +
+
+
+ + + See the business view + + + + + + +
+ +
+ ) +} diff --git a/packages/homepage/components/bridges/bridge-versioning-svg.css b/packages/homepage/components/bridges/bridge-versioning-svg.css new file mode 100644 index 0000000..5f7aeea --- /dev/null +++ b/packages/homepage/components/bridges/bridge-versioning-svg.css @@ -0,0 +1,125 @@ +/* Bridge Versioning — SVG node/edge transitions and state styles */ + +.bridge-versioning .flow-svg { + width: min(716px, 100%); + height: auto; + aspect-ratio: 716 / 420; +} + +.bridge-versioning .lane-label { + font-family: var(--font-mono); + font-size: 9px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + fill: var(--text-muted); +} + +.bridge-versioning .lane-sep { + stroke: var(--node-stroke); + stroke-width: 1; + stroke-dasharray: 4 4; + opacity: 0.4; +} + +.bridge-versioning .node-group { + transition: transform 0.9s var(--ease-out-expo), + opacity 0.7s var(--ease-out-expo); +} + +.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: var(--font-sans); + font-size: 9.5px; + font-weight: 500; + fill: var(--text-primary); + text-anchor: middle; + dominant-baseline: central; + pointer-events: none; + transition: opacity 0.4s; +} + +.bridge-versioning .node-type-badge { + font-family: var(--font-mono); + 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-versioning .node-circle { + fill: var(--node-fill); + stroke: var(--node-type-terminal); + stroke-width: 1.2; + transition: stroke 0.5s, filter 0.5s; +} + +.bridge-versioning .edge { + fill: none; + stroke: var(--edge-color, #6b4d40); + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; + marker-end: url(#bv-arrowhead); + transition: d 0.9s var(--ease-out-expo), + opacity 0.7s var(--ease-out-expo), + stroke 0.5s; +} + +/* Glow effects for added/changed 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)); +} + +/* Enter/exit states */ +.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: var(--font-mono); + 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; +} diff --git a/packages/homepage/components/bridges/bridge-versioning.tsx b/packages/homepage/components/bridges/bridge-versioning.tsx new file mode 100644 index 0000000..30a9a44 --- /dev/null +++ b/packages/homepage/components/bridges/bridge-versioning.tsx @@ -0,0 +1,343 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import { cn } from '@/lib/utils' +import { ViewTransition } from './shared/view-transition' +import '../flow/flow.css' +import './bridge-versioning-svg.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 }, + route: { x: 460, y: 210 }, + ship: { x: 560, y: 350 }, + notify: { x: 560, y: 350, hidden: true }, + 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', +} + +// Diff line style constants +const DL = 'font-mono text-[11px] leading-[1.7] px-3.5 whitespace-pre' +const DL_ADD = 'bg-true-green/[0.08] text-true-green' +const DL_DEL = 'bg-false-red/[0.08] text-false-red' +const DL_CTX = 'text-fg-muted' + +function edgePath( + fromX: number, + fromY: number, + toX: number, + toY: number, + fromR: number, + toR: number, +): string { + const startX = fromX + fromR + const endX = toX - toR + + if (Math.abs(fromY - toY) < 5) { + return `M ${startX} ${fromY} L ${endX} ${toY}` + } + + 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 { + 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 = cn( + 'node-group', + state.approve?.hidden ? 'node-entering' : 'visible', + !state.approve?.hidden && 'node-added', + ) + + const notifyClasses = cn( + 'node-group', + state.notify?.hidden ? 'node-entering' : 'visible', + !state.notify?.hidden && 'node-added', + ) + + const shipClasses = cn('node-group', isAfterState && 'node-changed') + const svgClasses = cn('flow-svg', isAfterState && 'show-changes') + + return ( + <> +
+ + + + + + + + + + + {/* Lane backgrounds */} + + + + + {/* Lane labels */} + Customer + Routing + Fulfillment + + {/* Lane separators */} + + + + {/* EDGES */} + + + + + + + + + {/* NODES */} + + + Start + + + + + ACTION + Submit + + + + + ACTION + Validate + + + + + ACTION + Approve + + NEW + + + + + SWITCH + Route Order + + + + + ACTION + {state.shipLabel} + ~ CHANGED + + + + + ACTION + Notify + + NEW + + + + + End + + +
+
+ {isAfterState ? 'After' : 'Before'} +
+ + ) +} + +function DeveloperViewContent() { + return ( +
+ {/* Diff section */} +
+
+ 📝 + 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 section */} +
+
+ + 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 isDev = perspective === 'developer' + const [isAfterState, setIsAfterState] = useState(false) + const timerRef = useRef | null>(null) + const isFirstRender = useRef(true) + // useDynamicHeight is now handled internally by ViewTransition + + useEffect(() => { + 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]) + + 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 */} + } + devContent={} + /> +
+
+ ) +} diff --git a/packages/homepage/components/bridges/overview-flow-svg.tsx b/packages/homepage/components/bridges/overview-flow-svg.tsx new file mode 100644 index 0000000..cea25b1 --- /dev/null +++ b/packages/homepage/components/bridges/overview-flow-svg.tsx @@ -0,0 +1,139 @@ +export function OverviewFlowSvg() { + return ( + + + + + + + {/* Hover gradient fills */} + + + + + + + + + + + + + + + + + {/* Radial inner glow */} + + + + + + + + + + + + + + + {/* CUSTOMER Lane (y: 0-75) */} + + + + CUSTOMER + + + + Start + + + + + ACTION + Submit Order + + + + {/* ROUTING Lane (y: 81-172) */} + + + + ROUTING + + express + standard + review + + + + + + + + ACTION + Validate + + + + + SWITCH + Route + + + + + ACTION + Review + + + + {/* FULFILLMENT Lane (y: 178-258) */} + + + + FULFILLMENT + + + + ACTION + Confirm + + + + + ACTION + Ship Standard + + + + + ACTION + Ship Express + + + + + End + + + + {/* Cross-lane edges */} + + + + + + + + + ) +} diff --git a/packages/homepage/components/bridges/overview-parallax.css b/packages/homepage/components/bridges/overview-parallax.css new file mode 100644 index 0000000..4876417 --- /dev/null +++ b/packages/homepage/components/bridges/overview-parallax.css @@ -0,0 +1,642 @@ +/* ══════════════════════════════════════════════════════ + 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: min(780px, calc(100vw - 96px)); + aspect-ratio: 780 / 720; + 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; +} + +/* Badge, CTA, typography, and flow-container base styles migrated to + Tailwind classes directly in overview-parallax.tsx. + Badge and CTA use shared components (badge.tsx, bridge-cta.tsx). */ + +/* ── 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%); +} + +/* ── Responsive breakpoints ── */ +@media (max-width: 840px) { + .overview-parallax { + padding: 32px; + } + + .overview-parallax .scene { + width: min(780px, calc(100vw - 64px)); + } + + .overview-parallax .layer { + padding: 24px 24px 20px; + } + + .overview-parallax .separator-content { + font-size: 12px; + padding: 12px 28px; + } + + .overview-parallax .separator-content::before, + .overview-parallax .separator-content::after { + width: 40px; + } + + .overview-parallax .toolchain-grid { + gap: 8px; + } + + .overview-parallax .tool-card { + padding: 10px; + } + + .overview-parallax .tool-card-body { + font-size: 9px; + } +} + +@media (max-width: 640px) { + .overview-parallax { + padding: 20px; + border-radius: 12px; + } + + .overview-parallax .scene { + width: min(780px, calc(100vw - 40px)); + } + + .overview-parallax .layer { + padding: 20px 16px 16px; + border-radius: 12px; + } + + .overview-parallax .separator-content { + font-size: 10px; + padding: 10px 20px; + gap: 6px; + } + + .overview-parallax .separator-content::before, + .overview-parallax .separator-content::after { + width: 24px; + } + + .overview-parallax .toolchain-grid { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: 6px; + } + + .overview-parallax .tool-card { + padding: 8px; + gap: 4px; + } + + .overview-parallax .tool-card-title { + font-size: 10px; + } + + .overview-parallax .tool-card-body { + font-size: 8px; + } + + .overview-parallax .tool-card-icon { + width: 22px; + height: 22px; + } + + .overview-parallax .tool-card-icon svg { + width: 12px; + height: 12px; + } +} diff --git a/packages/homepage/components/bridges/overview-parallax.tsx b/packages/homepage/components/bridges/overview-parallax.tsx new file mode 100644 index 0000000..e960e28 --- /dev/null +++ b/packages/homepage/components/bridges/overview-parallax.tsx @@ -0,0 +1,382 @@ +'use client' + +import { useRef, useState, useEffect, useCallback } from 'react' +import { OverviewFlowSvg } from './overview-flow-svg' +import { Badge } from './shared/badge' +import { BridgeCTA } from './shared/bridge-cta' +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 +
+
+
+ +
+
+ + + See the developer toolchain + + + + + + +
+ + {/* ===== 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
+
+
+
+ + + + + + + + See the business workflow + +
+ + {/* ===== LIQUID COLOR BLEED OVERLAY ===== */} +
+
+
+
+ ) +} diff --git a/packages/homepage/components/bridges/shared/badge.tsx b/packages/homepage/components/bridges/shared/badge.tsx new file mode 100644 index 0000000..5b745b3 --- /dev/null +++ b/packages/homepage/components/bridges/shared/badge.tsx @@ -0,0 +1,47 @@ +import { cn } from '@/lib/utils' + +type BadgeVariant = 'business' | 'developer' +type ColorScheme = 'magenta-teal' | 'purple-magenta' + +const COLOR_MAP: Record> = { + 'magenta-teal': { + business: 'text-accent bg-accent/10 border-accent/20', + developer: 'text-type-teal bg-type-teal/10 border-type-teal/20', + }, + 'purple-magenta': { + business: 'text-node-switch bg-node-switch/[0.12] border-node-switch/20', + developer: 'text-accent bg-accent/[0.12] border-accent/20', + }, +} + +export function Badge({ + variant, + colorScheme = 'magenta-teal', + pulse = false, + children, + className, +}: { + variant: BadgeVariant + colorScheme?: ColorScheme + pulse?: boolean + children: React.ReactNode + className?: string +}) { + return ( +
+ + {children} +
+ ) +} diff --git a/packages/homepage/components/bridges/shared/bridge-cta.tsx b/packages/homepage/components/bridges/shared/bridge-cta.tsx new file mode 100644 index 0000000..1f9cd74 --- /dev/null +++ b/packages/homepage/components/bridges/shared/bridge-cta.tsx @@ -0,0 +1,27 @@ +import { cn } from '@/lib/utils' + +export function BridgeCTA({ + onClick, + children, + className, +}: { + onClick: (e: React.MouseEvent) => void + children: React.ReactNode + className?: string +}) { + return ( + + ) +} diff --git a/packages/homepage/components/bridges/shared/view-transition.tsx b/packages/homepage/components/bridges/shared/view-transition.tsx new file mode 100644 index 0000000..a002a73 --- /dev/null +++ b/packages/homepage/components/bridges/shared/view-transition.tsx @@ -0,0 +1,57 @@ +'use client' + +import { cn } from '@/lib/utils' +import { useDynamicHeight } from '@/hooks/use-dynamic-height' + +export function ViewTransition({ + showDev, + bizContent, + devContent, + className, + bizClassName, + devClassName, +}: { + showDev: boolean + bizContent: React.ReactNode + devContent: React.ReactNode + className?: string + bizClassName?: string + devClassName?: string +}) { + const { containerRef, bizRef, devRef } = useDynamicHeight(showDev) + + return ( +
+
+ {bizContent} +
+
+ {devContent} +
+
+ ) +} diff --git a/packages/homepage/components/bridges/simulation-data.ts b/packages/homepage/components/bridges/simulation-data.ts new file mode 100644 index 0000000..cd7e354 --- /dev/null +++ b/packages/homepage/components/bridges/simulation-data.ts @@ -0,0 +1,92 @@ +// ══════════════════════════════════════════════════════ +// TYPES +// ══════════════════════════════════════════════════════ + +export interface BridgeSimulationProps { + perspective: 'business' | 'developer' +} + +export interface RippleObj { + id: string + cx: number + cy: number + color: string + delay: number +} + +export type SimMode = 'walkthrough' | 'stepbystep' | 'whatif' + +/** @deprecated Kept for backward-compat; SVG components now use data attributes. */ +export interface FlowSvgProps { + getNodeClassName: (id: string) => string + getEdgeClassName: (id: string) => string +} + +// ══════════════════════════════════════════════════════ +// NODE & EDGE DEFINITIONS +// ══════════════════════════════════════════════════════ + +// Walk-through: Patient Intake — urgent path +export 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 +export 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) +export 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 +export 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'], +} + +export const fileNames: Record = { + walkthrough: 'patient-intake.flowprint', + stepbystep: 'loan-application.flowprint', + whatif: 'insurance-claim.flowprint', +} + +// Node center positions (from data-cx, data-cy) +export 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 }, +} diff --git a/packages/homepage/components/bridges/step-by-step-svg.tsx b/packages/homepage/components/bridges/step-by-step-svg.tsx new file mode 100644 index 0000000..137fd7a --- /dev/null +++ b/packages/homepage/components/bridges/step-by-step-svg.tsx @@ -0,0 +1,168 @@ +import type { RippleObj } from './simulation-data' + +export interface StepByStepSvgProps { + isActive: boolean + sbRipples: RippleObj[] +} + +export function StepByStepSvg({ isActive, sbRipples }: StepByStepSvgProps) { + return ( + + + + + + + + + + + {/* 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 + + + ) +} diff --git a/packages/homepage/components/bridges/walkthrough-svg.tsx b/packages/homepage/components/bridges/walkthrough-svg.tsx new file mode 100644 index 0000000..fd7386d --- /dev/null +++ b/packages/homepage/components/bridges/walkthrough-svg.tsx @@ -0,0 +1,110 @@ +import type { RippleObj } from './simulation-data' + +export interface WalkthroughSvgProps { + isActive: boolean + wtRipples: RippleObj[] +} + +export function WalkthroughSvg({ isActive, wtRipples }: WalkthroughSvgProps) { + return ( + + + + + + + + + + + {/* 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 => ( + + ))} + + + ) +} diff --git a/packages/homepage/components/bridges/what-if-svg.tsx b/packages/homepage/components/bridges/what-if-svg.tsx new file mode 100644 index 0000000..31c8007 --- /dev/null +++ b/packages/homepage/components/bridges/what-if-svg.tsx @@ -0,0 +1,118 @@ +import type { RippleObj } from './simulation-data' + +export interface WhatIfSvgProps { + isActive: boolean + wiRipples: RippleObj[] +} + +export function WhatIfSvg({ isActive, wiRipples }: WhatIfSvgProps) { + return ( + + + + + + + + + + + + + + {/* 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 + + + ) +} 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..ac09f70 --- /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: var(--node-fill, #2a1a14); + stroke: var(--node-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: var(--text-primary, #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: var(--node-fill, #2a1a14); + stroke: var(--node-type-terminal, #3FDC77); + stroke-width: 1.2; +} + +/* Edges */ +.flow-svg .edge { + fill: none; + stroke: var(--edge-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: var(--edge-stroke, #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.tsx b/packages/homepage/components/sections/capabilities-section.tsx new file mode 100644 index 0000000..8fb1d82 --- /dev/null +++ b/packages/homepage/components/sections/capabilities-section.tsx @@ -0,0 +1,58 @@ +'use client' + +import { useState } from 'react' +import { cn } from '@/lib/utils' +import { BridgeDecision } from '@/components/bridges/bridge-decision' +import { BridgeSimulation } from '@/components/bridges/bridge-simulation' +import { BridgeVersioning } from '@/components/bridges/bridge-versioning' + +type Perspective = 'business' | 'developer' + +export function CapabilitiesSection() { + const [perspective, setPerspective] = useState('business') + + return ( +
+ {/* Pinned pill toggle — sticky below fixed header */} +
+ + +
+ + {/* Bridge cards stacked vertically */} +
+
+ +
+ +
+ +
+ +
+ +
+
+
+ ) +} diff --git a/packages/homepage/components/sections/footer.tsx b/packages/homepage/components/sections/footer.tsx new file mode 100644 index 0000000..d492f76 --- /dev/null +++ b/packages/homepage/components/sections/footer.tsx @@ -0,0 +1,141 @@ +import { Github, Package, BookOpen } from "lucide-react" + +const GITHUB_URL = "https://github.com/ruminaider/flowprint" + +type FooterLink = { label: string; href?: string } + +const footerLinks: Record = { + product: [ + { label: "Web App" }, + { label: "CLI" }, + { label: "Documentation" }, + { label: "Templates" }, + ], + packages: [ + { label: "@ruminaider/flowprint-schema" }, + { label: "@ruminaider/flowprint-editor" }, + { label: "flowprint (CLI)" }, + ], + resources: [ + { label: "GitHub", href: GITHUB_URL }, + { label: "Changelog" }, + { label: "License (MIT)", href: `${GITHUB_URL}/blob/main/LICENSE` }, + ], +} + +export function FooterSection() { + return ( +
+
+
+ {/* Brand */} +
+
+
+ + + + + + +
+ Flowprint +
+

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

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

Product

+
    + {footerLinks.product.map((link) => ( +
  • + {link.href ? ( + {link.label} + ) : ( + {link.label} + )} +
  • + ))} +
+
+ + {/* Packages */} +
+

Packages

+
    + {footerLinks.packages.map((link) => ( +
  • + {link.href ? ( + {link.label} + ) : ( + {link.label} + )} +
  • + ))} +
+
+ + {/* Resources */} +
+

Resources

+
    + {footerLinks.resources.map((link) => ( +
  • + {link.href ? ( + {link.label} + ) : ( + {link.label} + )} +
  • + ))} +
+
+
+ + {/* 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..28e3bdf --- /dev/null +++ b/packages/homepage/components/sections/getting-started.tsx @@ -0,0 +1,135 @@ +"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 = async (command: string, index: number) => { + try { + await navigator.clipboard.writeText(command) + setCopiedIndex(index) + setTimeout(() => setCopiedIndex(null), 2000) + } catch { + // Clipboard API unavailable (insecure context, permission denied, etc.) + } + } + + 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 */} +
+ + Documentation + + + + GitHub + + + + npm + + +
+
+
+
+
+ ) +} diff --git a/packages/homepage/components/sections/header.tsx b/packages/homepage/components/sections/header.tsx new file mode 100644 index 0000000..8e24ea8 --- /dev/null +++ b/packages/homepage/components/sections/header.tsx @@ -0,0 +1,114 @@ +"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: "https://github.com/ruminaider/flowprint#readme" }, +] + +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..8484614 --- /dev/null +++ b/packages/homepage/components/sections/hero.tsx @@ -0,0 +1,104 @@ +"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 = async () => { + try { + await navigator.clipboard.writeText(installCommand) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + // Clipboard API unavailable (insecure context, permission denied, etc.) + } + } + + 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..e239548 --- /dev/null +++ b/packages/homepage/components/sections/tryit-section.css @@ -0,0 +1,370 @@ +/* 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: var(--font-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: var(--font-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: var(--font-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:not(:disabled) { + opacity: 0.9; + box-shadow: 0 0 24px rgba(228, 70, 255, 0.35); + transform: translateY(-1px); +} + +.cta-disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.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..eb44acd --- /dev/null +++ b/packages/homepage/components/sections/tryit-section.tsx @@ -0,0 +1,266 @@ +'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 */} +
+ + + Read the Docs + +
+
+ ) +} 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/ui/button.tsx b/packages/homepage/components/ui/button.tsx new file mode 100644 index 0000000..f64632d --- /dev/null +++ b/packages/homepage/components/ui/button.tsx @@ -0,0 +1,60 @@ +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 buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-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", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : 'button' + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/packages/homepage/hooks/use-dynamic-height.ts b/packages/homepage/hooks/use-dynamic-height.ts new file mode 100644 index 0000000..8fabe32 --- /dev/null +++ b/packages/homepage/hooks/use-dynamic-height.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef } from 'react' + +export function useDynamicHeight(isDev: boolean) { + const containerRef = useRef(null) + const bizRef = useRef(null) + const devRef = useRef(null) + const initialRender = useRef(true) + + useEffect(() => { + const activeView = isDev ? devRef.current : bizRef.current + if (!activeView || !containerRef.current) return + + const applyHeight = () => { + const h = activeView.scrollHeight + if (!containerRef.current) return + 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` + } + } + + applyHeight() + + // Watch for content size changes within the active view + const ro = new ResizeObserver(applyHeight) + ro.observe(activeView) + + return () => ro.disconnect() + }, [isDev]) + + return { containerRef, bizRef, devRef } +} diff --git a/packages/homepage/lib/utils.ts b/packages/homepage/lib/utils.ts new file mode 100644 index 0000000..fed2fe9 --- /dev/null +++ b/packages/homepage/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/packages/homepage/next.config.mjs b/packages/homepage/next.config.mjs new file mode 100644 index 0000000..ea3d1c5 --- /dev/null +++ b/packages/homepage/next.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + images: { + unoptimized: true, + }, +} + +export default nextConfig diff --git a/packages/homepage/package.json b/packages/homepage/package.json new file mode 100644 index 0000000..6c5c551 --- /dev/null +++ b/packages/homepage/package.json @@ -0,0 +1,35 @@ +{ + "name": "@ruminaider/flowprint-homepage", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint ." + }, + "dependencies": { + "@gsap/react": "^2.1.2", + "@radix-ui/react-slot": "1.2.4", + "@radix-ui/react-tabs": "1.1.13", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "gsap": "^3.14.2", + "lucide-react": "^0.564.0", + "next": "16.1.6", + "react": "19.2.4", + "react-dom": "19.2.4", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.2.0", + "@types/node": "^22", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "postcss": "^8.5", + "tailwindcss": "^4.2.0", + "tw-animate-css": "1.3.3", + "typescript": "5.7.3" + } +} diff --git a/packages/homepage/pnpm-lock.yaml b/packages/homepage/pnpm-lock.yaml new file mode 100644 index 0000000..b2e564f --- /dev/null +++ b/packages/homepage/pnpm-lock.yaml @@ -0,0 +1,1372 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-slot': + specifier: 1.2.4 + version: 1.2.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-tabs': + specifier: 1.1.13 + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.24(postcss@8.5.6) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.564.0 + version: 0.564.0(react@19.2.4) + next: + specifier: 16.1.6 + version: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 19.2.4 + version: 19.2.4 + react-dom: + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) + tailwind-merge: + specifier: ^3.3.1 + version: 3.4.0 + devDependencies: + '@tailwindcss/postcss': + specifier: ^4.2.0 + version: 4.2.0 + '@types/node': + specifier: ^22 + version: 22.19.11 + '@types/react': + specifier: 19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) + postcss: + specifier: ^8.5 + version: 8.5.6 + tailwindcss: + specifier: ^4.2.0 + version: 4.2.0 + tw-animate-css: + specifier: 1.3.3 + version: 1.3.3 + typescript: + specifier: 5.7.3 + version: 5.7.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.2.0': + resolution: {integrity: sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==} + + '@tailwindcss/oxide-android-arm64@4.2.0': + resolution: {integrity: sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.0': + resolution: {integrity: sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.0': + resolution: {integrity: sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.0': + resolution: {integrity: sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0': + resolution: {integrity: sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.0': + resolution: {integrity: sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.0': + resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.0': + resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.0': + resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.0': + resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.0': + resolution: {integrity: sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.0': + resolution: {integrity: sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.0': + resolution: {integrity: sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.0': + resolution: {integrity: sha512-u6YBacGpOm/ixPfKqfgrJEjMfrYmPD7gEFRoygS/hnQaRtV0VCBdpkx5Ouw9pnaLRwwlgGCuJw8xLpaR0hOrQg==} + + '@types/node@22.19.11': + resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + autoprefixer@10.4.24: + resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + lucide-react@0.564.0: + resolution: {integrity: sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + + tailwindcss@4.2.0: + resolution: {integrity: sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.3.3: + resolution: {integrity: sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==} + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@next/env@16.1.6': {} + + '@next/swc-darwin-arm64@16.1.6': + optional: true + + '@next/swc-darwin-x64@16.1.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.1.6': + optional: true + + '@next/swc-linux-arm64-musl@16.1.6': + optional: true + + '@next/swc-linux-x64-gnu@16.1.6': + optional: true + + '@next/swc-linux-x64-musl@16.1.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.1.6': + optional: true + + '@next/swc-win32-x64-msvc@16.1.6': + optional: true + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.2.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.0 + + '@tailwindcss/oxide-android-arm64@4.2.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.0': + optional: true + + '@tailwindcss/oxide@4.2.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.0 + '@tailwindcss/oxide-darwin-arm64': 4.2.0 + '@tailwindcss/oxide-darwin-x64': 4.2.0 + '@tailwindcss/oxide-freebsd-x64': 4.2.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.0 + '@tailwindcss/oxide-linux-x64-musl': 4.2.0 + '@tailwindcss/oxide-wasm32-wasi': 4.2.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.0 + + '@tailwindcss/postcss@4.2.0': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.0 + '@tailwindcss/oxide': 4.2.0 + postcss: 8.5.6 + tailwindcss: 4.2.0 + + '@types/node@22.19.11': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + autoprefixer@10.4.24(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001769 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.9.19: {} + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + caniuse-lite@1.0.30001769: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.286: {} + + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + escalade@3.2.0: {} + + fraction.js@5.3.4: {} + + graceful-fs@4.2.11: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + lucide-react@0.564.0(react@19.2.4): + dependencies: + react: 19.2.4 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + nanoid@3.3.11: {} + + next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@next/env': 16.1.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + postcss: 8.4.31 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + styled-jsx: 5.1.6(react@19.2.4) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-releases@2.0.27: {} + + picocolors@1.1.1: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react@19.2.4: {} + + scheduler@0.27.0: {} + + semver@7.7.4: + optional: true + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + source-map-js@1.2.1: {} + + styled-jsx@5.1.6(react@19.2.4): + dependencies: + client-only: 0.0.1 + react: 19.2.4 + + tailwind-merge@3.4.0: {} + + tailwindcss@4.2.0: {} + + tapable@2.3.0: {} + + tslib@2.8.1: {} + + tw-animate-css@1.3.3: {} + + typescript@5.7.3: {} + + undici-types@6.21.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 diff --git a/packages/homepage/postcss.config.mjs b/packages/homepage/postcss.config.mjs new file mode 100644 index 0000000..a869506 --- /dev/null +++ b/packages/homepage/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +} + +export default config diff --git a/packages/homepage/public/_headers b/packages/homepage/public/_headers new file mode 100644 index 0000000..7bd78da --- /dev/null +++ b/packages/homepage/public/_headers @@ -0,0 +1,7 @@ +/* + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Permissions-Policy: camera=(), microphone=(), geolocation=() + Strict-Transport-Security: max-age=31536000; includeSubDomains + Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: blob:; connect-src 'self' diff --git a/packages/homepage/tsconfig.json b/packages/homepage/tsconfig.json new file mode 100644 index 0000000..a67e4b2 --- /dev/null +++ b/packages/homepage/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "target": "ES2022", + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d253a4..9a3229b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,22 +16,22 @@ importers: version: 1.58.2 '@size-limit/file': specifier: ^12.0.0 - version: 12.0.0(size-limit@12.0.0) + version: 12.0.0(size-limit@12.0.0(jiti@2.6.1)) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.2.2)(jsdom@28.0.0)(yaml@2.8.2)) + version: 4.0.18(vitest@4.0.18(@types/node@25.2.2)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.31.1)(yaml@2.8.2)) eslint: specifier: ^9 - version: 9.39.2 + version: 9.39.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2) + version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.2) + version: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.2) + version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) globals: specifier: ^17.3.0 version: 17.3.0 @@ -43,7 +43,7 @@ importers: version: 3.8.1 size-limit: specifier: ^12.0.0 - version: 12.0.0 + version: 12.0.0(jiti@2.6.1) turbo: specifier: ^2.3.3 version: 2.8.3 @@ -52,10 +52,10 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.54.0 - version: 8.54.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.2.2)(jsdom@28.0.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.2)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.31.1)(yaml@2.8.2) packages/app: dependencies: @@ -89,13 +89,13 @@ importers: version: 19.2.3(@types/react@19.2.13) '@vitejs/plugin-react': specifier: ^5.1.3 - version: 5.1.3(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2)) + version: 5.1.3(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) typescript: specifier: ^5.7.3 version: 5.9.3 vite: specifier: ^6.0.11 - version: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + version: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) packages/cli: dependencies: @@ -138,13 +138,13 @@ importers: version: 25.2.2 tsup: specifier: ^8.3.6 - version: 8.5.1(@microsoft/api-extractor@7.56.3(@types/node@25.2.2))(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.56.3(@types/node@25.2.2))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.3 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.2.2)(jsdom@28.0.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.2)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.31.1)(yaml@2.8.2) packages/editor: dependencies: @@ -172,7 +172,7 @@ importers: version: 8.6.15(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.8.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@8.6.15(prettier@3.8.1))(typescript@5.9.3) '@storybook/react-vite': specifier: ^8.6.14 - version: 8.6.15(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.8.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1)(storybook@8.6.15(prettier@3.8.1))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2)) + version: 8.6.15(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.8.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1)(storybook@8.6.15(prettier@3.8.1))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) '@storybook/test': specifier: ^8.6.14 version: 8.6.15(storybook@8.6.15(prettier@3.8.1)) @@ -190,7 +190,7 @@ importers: version: 19.2.3(@types/react@19.2.13) '@vitejs/plugin-react': specifier: ^5.1.3 - version: 5.1.3(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2)) + version: 5.1.3(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) jsdom: specifier: ^28.0.0 version: 28.0.0 @@ -208,13 +208,13 @@ importers: version: 5.9.3 vite: specifier: ^6.0.11 - version: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + version: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) vite-plugin-dts: specifier: ^4.4.0 - version: 4.5.4(@types/node@25.2.2)(rollup@4.57.1)(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2)) + version: 4.5.4(@types/node@25.2.2)(rollup@4.57.1)(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.2.2)(jsdom@28.0.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.2)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.31.1)(yaml@2.8.2) packages/engine: dependencies: @@ -233,13 +233,13 @@ importers: version: 25.2.2 tsup: specifier: ^8.3.6 - version: 8.5.1(@microsoft/api-extractor@7.56.3(@types/node@25.2.2))(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.56.3(@types/node@25.2.2))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.3 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.2.2)(jsdom@28.0.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.2)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.31.1)(yaml@2.8.2) packages/schema: dependencies: @@ -258,7 +258,7 @@ importers: version: 15.0.4 tsup: specifier: ^8.3.6 - version: 8.5.1(@microsoft/api-extractor@7.56.3(@types/node@25.2.2))(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@microsoft/api-extractor@7.56.3(@types/node@25.2.2))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -1893,8 +1893,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001769: - resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} @@ -2086,6 +2086,10 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + diff@8.0.3: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} @@ -2683,6 +2687,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} @@ -2764,6 +2772,76 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -4381,9 +4459,9 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -4581,12 +4659,12 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: glob: 10.5.0 magic-string: 0.27.0 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) optionalDependencies: typescript: 5.9.3 @@ -4823,9 +4901,9 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@size-limit/file@12.0.0(size-limit@12.0.0)': + '@size-limit/file@12.0.0(size-limit@12.0.0(jiti@2.6.1))': dependencies: - size-limit: 12.0.0 + size-limit: 12.0.0(jiti@2.6.1) '@standard-schema/spec@1.1.0': {} @@ -4916,13 +4994,13 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@storybook/builder-vite@8.6.15(storybook@8.6.15(prettier@3.8.1))(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2))': + '@storybook/builder-vite@8.6.15(storybook@8.6.15(prettier@3.8.1))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: '@storybook/csf-plugin': 8.6.15(storybook@8.6.15(prettier@3.8.1)) browser-assert: 1.2.1 storybook: 8.6.15(prettier@3.8.1) ts-dedent: 2.2.0 - vite: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) '@storybook/components@8.6.15(storybook@8.6.15(prettier@3.8.1))': dependencies: @@ -4992,11 +5070,11 @@ snapshots: react-dom: 19.2.4(react@19.2.4) storybook: 8.6.15(prettier@3.8.1) - '@storybook/react-vite@8.6.15(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.8.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1)(storybook@8.6.15(prettier@3.8.1))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2))': + '@storybook/react-vite@8.6.15(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.8.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1)(storybook@8.6.15(prettier@3.8.1))(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - '@storybook/builder-vite': 8.6.15(storybook@8.6.15(prettier@3.8.1))(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2)) + '@storybook/builder-vite': 8.6.15(storybook@8.6.15(prettier@3.8.1))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) '@storybook/react': 8.6.15(@storybook/test@8.6.15(storybook@8.6.15(prettier@3.8.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@8.6.15(prettier@3.8.1))(typescript@5.9.3) find-up: 5.0.0 magic-string: 0.30.21 @@ -5006,7 +5084,7 @@ snapshots: resolve: 1.22.11 storybook: 8.6.15(prettier@3.8.1) tsconfig-paths: 4.2.0 - vite: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) optionalDependencies: '@storybook/test': 8.6.15(storybook@8.6.15(prettier@3.8.1)) transitivePeerDependencies: @@ -5172,15 +5250,15 @@ snapshots: '@types/uuid@9.0.8': {} - '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 - eslint: 9.39.2 + eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -5188,14 +5266,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 debug: 4.4.3 - eslint: 9.39.2 + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5218,13 +5296,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2 + eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -5247,13 +5325,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - eslint: 9.39.2 + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5263,7 +5341,7 @@ snapshots: '@typescript-eslint/types': 8.54.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.1.3(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2))': + '@vitejs/plugin-react@5.1.3(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -5271,11 +5349,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.2 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.2.2)(jsdom@28.0.0)(yaml@2.8.2))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.2.2)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -5287,7 +5365,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.2.2)(jsdom@28.0.0)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.2.2)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.31.1)(yaml@2.8.2) '@vitest/expect@2.0.5': dependencies: @@ -5305,13 +5383,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) '@vitest/pretty-format@2.0.5': dependencies: @@ -5613,7 +5691,7 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001777 electron-to-chromium: 1.5.286 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -5646,7 +5724,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001769: {} + caniuse-lite@1.0.30001777: {} chai@5.3.3: dependencies: @@ -5819,6 +5897,9 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@2.1.2: + optional: true + diff@8.0.3: {} dir-glob@3.0.1: @@ -6038,22 +6119,22 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.2): + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-react-hooks@7.0.1(eslint@9.39.2): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.0 - eslint: 9.39.2 + eslint: 9.39.2(jiti@2.6.1) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.2): + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -6061,7 +6142,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 9.39.2 + eslint: 9.39.2(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -6084,9 +6165,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.2: + eslint@9.39.2(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 @@ -6120,6 +6201,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -6573,6 +6656,9 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jiti@2.6.1: + optional: true + jju@1.4.0: {} joycon@3.1.1: {} @@ -6670,6 +6756,56 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + optional: true + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -6974,10 +7110,11 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(postcss@8.5.6)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: + jiti: 2.6.1 postcss: 8.5.6 yaml: 2.8.2 @@ -7237,13 +7374,15 @@ snapshots: signal-exit@4.1.0: {} - size-limit@12.0.0: + size-limit@12.0.0(jiti@2.6.1): dependencies: bytes-iec: 3.1.1 lilconfig: 3.1.3 nanospinner: 1.2.2 picocolors: 1.1.1 tinyglobby: 0.2.15 + optionalDependencies: + jiti: 2.6.1 slash@3.0.0: {} @@ -7471,7 +7610,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(@microsoft/api-extractor@7.56.3(@types/node@25.2.2))(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(@microsoft/api-extractor@7.56.3(@types/node@25.2.2))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -7482,7 +7621,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.6)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.2) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -7564,13 +7703,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.54.0(eslint@9.39.2)(typescript@5.9.3): + typescript-eslint@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -7625,7 +7764,7 @@ snapshots: uuid@9.0.1: {} - vite-plugin-dts@4.5.4(@types/node@25.2.2)(rollup@4.57.1)(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2)): + vite-plugin-dts@4.5.4(@types/node@25.2.2)(rollup@4.57.1)(typescript@5.9.3)(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)): dependencies: '@microsoft/api-extractor': 7.56.3(@types/node@25.2.2) '@rollup/pluginutils': 5.3.0(rollup@4.57.1) @@ -7638,13 +7777,13 @@ snapshots: magic-string: 0.30.21 typescript: 5.9.3 optionalDependencies: - vite: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2): + vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -7655,12 +7794,14 @@ snapshots: optionalDependencies: '@types/node': 25.2.2 fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 yaml: 2.8.2 - vitest@4.0.18(@types/node@25.2.2)(jsdom@28.0.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.2.2)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.31.1)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@25.2.2)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -7677,7 +7818,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@25.2.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.2.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 18ec407..ce5f6c3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - 'packages/*' + - '!packages/homepage'