diff --git a/.github/hooks/workmux-status/hooks.json b/.github/hooks/workmux-status/hooks.json new file mode 100644 index 000000000..3e69b55c9 --- /dev/null +++ b/.github/hooks/workmux-status/hooks.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "hooks": { + "userPromptSubmitted": [ + { + "type": "command", + "bash": "workmux set-window-status working" + } + ], + "postToolUse": [ + { + "type": "command", + "bash": "workmux set-window-status working" + } + ], + "agentStop": [ + { + "type": "command", + "bash": "workmux set-window-status done" + } + ] + } +} diff --git a/.webmux.yaml b/.webmux.yaml new file mode 100644 index 000000000..21dfa5dff --- /dev/null +++ b/.webmux.yaml @@ -0,0 +1,39 @@ +# Project display name in the dashboard +name: windmilldocs + +# Each service defines a port env var that webmux injects into .env.local +# when creating a worktree. Ports are auto-assigned: base + (slot × step). +# Use `source .env.local` in your .workmux.yaml pane commands to pick them up. +services: + - name: app + portEnv: PORT + portStart: 3000 # Port for the main branch (slot 0) + portStep: 10 # Increment per worktree (3010, 3020, ...) + +# Agent profiles determine how AI agents run in worktrees +profiles: + default: + name: default + + # --- Sandbox profile (uncomment to enable) --- + # Runs agents in Docker containers for full isolation. + # Requires: docker + a built image. + # sandbox: + # name: sandbox + # image: my-project-sandbox + # envPassthrough: # Env vars forwarded into the container + # - DATABASE_URL + # systemPrompt: > + # You are running inside a sandboxed container. + # Start the dev server with: npm run dev + +# --- Linked repos (uncomment to enable) --- +# Monitor PRs from related repos in the dashboard. +# linkedRepos: +# - repo: org/other-repo +# alias: other + +# --- Startup environment variables --- +# These will appear as configurable fields in the UI when creating a worktree. +# startupEnvs: +# NODE_ENV: development diff --git a/.workmux.yaml b/.workmux.yaml new file mode 100644 index 000000000..069ffcaa4 --- /dev/null +++ b/.workmux.yaml @@ -0,0 +1,21 @@ +main_branch: main + +panes: + # Agent pane — runs the AI coding assistant + - command: >- + claude --append-system-prompt + "You are running inside a git worktree managed by workmux.\n + Pane layout (current window):\n + - Pane 0: this pane (claude agent)\n + - Pane 1: dev server\n\n + To check dev server logs: tmux capture-pane -t .1 -p -S -50\n + To restart dev server: tmux send-keys -t .1 C-c 'source .env.local && PORT=\$PORT npm run start' Enter" + focus: true + + # Dev server — waits for .env.local (written by webmux) then starts + - command: >- + npm install && + until [ -f .env.local ]; do sleep 0.2; done; + source .env.local; + PORT=$PORT npm run start + split: horizontal diff --git a/docusaurus.config.js b/docusaurus.config.js index 0b6f91054..acf0c31cd 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -60,6 +60,54 @@ const config = { { to: '/terms/2025-12-01', from: '/terms' + }, + { + to: '/platform/script-editor', + from: '/product/script-editor' + }, + { + to: '/platform/flow-editor', + from: '/product/flow-editor' + }, + { + to: '/platform/app-builder', + from: '/product/app-builder' + }, + { + to: '/platform/triggers', + from: '/product/triggers' + }, + { + to: '/platform/datatables', + from: '/product/datatables' + }, + { + to: '/platform/deployment-versioning', + from: '/product/deployment-versioning' + }, + { + to: '/platform/local-dev', + from: '/product/local-dev' + }, + { + to: '/platform/workers', + from: '/product/workers' + }, + { + to: '/platform/sandboxes', + from: '/product/sandboxes' + }, + { + to: '/platform/observability', + from: '/product/observability' + }, + { + to: '/platform/rbac', + from: '/product/rbac' + }, + { + to: '/platform/self-host', + from: '/product/self-host' } ] } @@ -138,6 +186,25 @@ const config = { href: '/' }, items: [ + { + type: 'dropdown', + label: 'Platform', + position: 'left', + items: [ + { label: 'Script editor', href: '/platform/script-editor' }, + { label: 'Flow editor', href: '/platform/flow-editor' }, + { label: 'App builder', href: '/platform/app-builder' }, + { label: 'Triggers', href: '/platform/triggers' }, + { label: 'Data tables', href: '/platform/datatables' }, + { label: 'Deployment & versioning', href: '/platform/deployment-versioning' }, + { label: 'Local dev', href: '/platform/local-dev' }, + { label: 'Workers', href: '/platform/workers' }, + { label: 'AI sandboxes', href: '/platform/sandboxes' }, + { label: 'Observability', href: '/platform/observability' }, + { label: 'RBAC', href: '/platform/rbac' }, + { label: 'No-ops self-host', href: '/platform/self-host' }, + ] + }, { href: '/pricing', position: 'left', diff --git a/frontend-builder-full.png b/frontend-builder-full.png new file mode 100644 index 000000000..780ecf20f Binary files /dev/null and b/frontend-builder-full.png differ diff --git a/src/components/products/DatatablesHeroAnimation.jsx b/src/components/products/DatatablesHeroAnimation.jsx new file mode 100644 index 000000000..a987d227b --- /dev/null +++ b/src/components/products/DatatablesHeroAnimation.jsx @@ -0,0 +1,246 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { ArrowDown, Play, CheckCircle, Loader2, Database } from 'lucide-react'; + +const insertedRows = [ + { id: 1, customer: 'Alice Martin', amount: '$1,250.00', status: 'active' }, + { id: 2, customer: 'Bob Chen', amount: '$890.00', status: 'pending' }, + { id: 3, customer: 'Carol Smith', amount: '$2,100.00', status: 'active' }, + { id: 4, customer: 'Dan Wilson', amount: '$450.00', status: 'active' }, +]; + +const scriptLines = [ + { tokens: [{ text: 'import ', color: 'text-purple-400' }, { text: '* as wmill ', color: 'text-blue-300' }, { text: 'from ', color: 'text-purple-400' }, { text: "'windmill-client'", color: 'text-amber-300' }] }, + { tokens: [] }, + { tokens: [{ text: 'export async function ', color: 'text-purple-400' }, { text: 'main', color: 'text-blue-300' }, { text: '(', color: 'text-gray-400' }] }, + { tokens: [{ text: ' name', color: 'text-orange-300' }, { text: ': string, ', color: 'text-gray-400' }, { text: 'amount', color: 'text-orange-300' }, { text: ': number', color: 'text-gray-400' }] }, + { tokens: [{ text: ') {', color: 'text-gray-400' }] }, + { tokens: [{ text: ' const ', color: 'text-purple-400' }, { text: 'db ', color: 'text-blue-300' }, { text: '= ', color: 'text-gray-400' }, { text: 'wmill', color: 'text-blue-300' }, { text: '.getResource(', color: 'text-gray-400' }, { text: "'pg'", color: 'text-amber-300' }, { text: ')', color: 'text-gray-400' }] }, + { tokens: [{ text: ' await ', color: 'text-purple-400' }, { text: 'db', color: 'text-blue-300' }, { text: '.query(', color: 'text-gray-400' }] }, + { tokens: [{ text: " `INSERT INTO orders (name, amount)", color: 'text-amber-300' }] }, + { tokens: [{ text: " VALUES ($1, $2)`", color: 'text-amber-300' }, { text: ', [name, amount]', color: 'text-gray-400' }] }, + { tokens: [{ text: ' )', color: 'text-gray-400' }] }, + { tokens: [{ text: '}', color: 'text-gray-400' }] }, +]; + +export default function DatatablesHeroAnimation() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + + // Phases: + // 1 - panels visible + code appears + run 1 starts immediately + // 2 - run 1: done → row 1 appears + // 3 - run 2: running + // 4 - run 2: done → row 2 appears + // 5 - run 3: running + // 6 - run 3: done → row 3 appears + // 7 - run 4: running + // 8 - run 4: done → row 4 appears + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 200), + setTimeout(() => setPhase(2), 1000), + setTimeout(() => setPhase(3), 1700), + setTimeout(() => setPhase(4), 2500), + setTimeout(() => setPhase(5), 3200), + setTimeout(() => setPhase(6), 4000), + setTimeout(() => setPhase(7), 4700), + setTimeout(() => setPhase(8), 5500), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView]); + + // Odd phases = running, even phases >= 2 = done + const isRunning = phase >= 1 && phase % 2 === 1; + const isDone = phase >= 2 && phase % 2 === 0; + const runState = isRunning ? 'running' : isDone ? 'done' : 'idle'; + + // Each even phase >= 2 adds a row + const visibleRowCount = phase < 2 ? 0 : Math.floor(phase / 2); + + const studioColumns = ['id', 'customer', 'amount', 'status']; + + return ( +
+
+
+ {/* Script card */} + = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + > +
+ {/* Title bar */} +
+
+ + insert_order.ts +
+ + {runState === 'done' ? ( + <> + + Done + + ) : runState === 'running' ? ( + <> + + + + Running + + ) : ( + <> + + Run + + )} + +
+ + {/* Code */} +
+ {scriptLines.map((line, i) => ( + = 1 ? { opacity: 1 } : {}} + transition={{ duration: 0.3, delay: i * 0.05 }} + > + {i + 1} + {line.tokens.length === 0 ? ( +   + ) : ( + line.tokens.map((token, j) => ( + {token.text} + )) + )} + + ))} +
+
+
+ + {/* Down arrow */} + = 1 ? { opacity: 1 } : {}} + transition={{ duration: 0.4 }} + > + + + + {/* Database Studio panel (visible from start, empty) */} + = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5, delay: 0.1 }} + > +
+ {/* Title bar */} +
+ Windmill + Database Studio + / orders +
+ +
+ {/* Header */} +
+ {studioColumns.map((col) => ( +
+ {col} +
+ ))} +
+ + {/* 4 row slots — blank initially, filled as executions complete */} + {insertedRows.map((row, i) => { + const isFilled = i < visibleRowCount; + const isLatest = isFilled && i === visibleRowCount - 1; + return ( +
+ {isFilled ? ( + <> + {row.id} + {row.customer} + {row.amount} + + + {row.status} + + + + ) : ( + <> +
 
+
 
+
 
+
 
+ + )} +
+ ); + })} +
+
+
+
+
+
+ ); +} diff --git a/src/components/products/HeroCTAButtons.tsx b/src/components/products/HeroCTAButtons.tsx new file mode 100644 index 000000000..a256a622d --- /dev/null +++ b/src/components/products/HeroCTAButtons.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; + +export default function HeroCTAButtons() { + return ( +
+ (window as any).plausible?.('try-cloud')} + data-analytics='"try-cloud"' + className="rounded-md transition-all bg-blue-500 px-4 py-2 text-base font-semibold leading-7 text-white hover:bg-blue-800 hover:!text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 !no-underline" + rel="nofollow" + > + Try Windmill cloud + + (window as any).plausible?.('self-host')} + className="text-base font-semibold leading-7 text-gray-900 dark:text-gray-200 !no-underline" + > + Self-host in 3 mins + +
+ ); +} diff --git a/src/components/products/LocalDevHeroAnimation.jsx b/src/components/products/LocalDevHeroAnimation.jsx new file mode 100644 index 000000000..5f96266da --- /dev/null +++ b/src/components/products/LocalDevHeroAnimation.jsx @@ -0,0 +1,236 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView, AnimatePresence } from 'framer-motion'; +import { Check, RefreshCcw } from 'lucide-react'; + +// Code snippet shown in both Windmill and Local cards +const codeLines = [ + { num: '1', tokens: [{ text: 'import ', color: 'text-purple-400' }, { text: '* as wmill ', color: 'text-blue-300' }, { text: 'from ', color: 'text-purple-400' }, { text: "'windmill'", color: 'text-amber-300' }] }, + { num: '2', tokens: [] }, + { num: '3', tokens: [{ text: 'export async function ', color: 'text-purple-400' }, { text: 'main', color: 'text-blue-300' }, { text: '(limit) {', color: 'text-gray-400' }] }, + { num: '4', tokens: [{ text: ' const rows = db.query(q, [limit]);', color: 'text-gray-400' }] }, + { num: '5', tokens: [{ text: ' return rows;', color: 'text-gray-400' }] }, + { num: '6', tokens: [{ text: '}', color: 'text-gray-400' }] }, +]; + +function CodeSnippet({ dark = false, showDiff = false, staggerDelay = 0 }) { + const bg = dark ? 'text-gray-500' : 'text-gray-500'; + const numColor = dark ? 'text-gray-600' : 'text-gray-400'; + return ( +
+ {codeLines.map((line, i) => { + // Line 4 (index 3) gets the diff treatment + if (i === 3 && showDiff) { + return ( + +
+ {line.num} + {' '}const rows = db.query(q, [limit]); +
+
+ {line.num} + {' '}const rows = db.query(q, [limit, offset]); +
+
+ ); + } + return ( + + {line.num} + {line.tokens.length === 0 ? ( +   + ) : ( + line.tokens.map((token, j) => ( + {token.text} + )) + )} + + ); + })} +
+ ); +} + +export default function LocalDevHeroAnimation() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + + // Phases: + // 1 - Cards fade in. Windmill shows code snippet. Local is empty. + // 2 - Sync pull (spinner in Local card). + // 3 - Code appears in Local card. + // 4 - Diff appears in Local card (edited code). + // 5 - Push sync (spinner in Windmill card). + // 6 - Windmill card updates with new code + "new version" badge. + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 400), + setTimeout(() => setPhase(2), 1200), + setTimeout(() => setPhase(3), 2400), + setTimeout(() => setPhase(4), 4000), + setTimeout(() => setPhase(5), 5600), + setTimeout(() => setPhase(6), 7000), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView]); + + return ( +
+
+ = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + className="w-full" + > +
+ {/* Left card: Windmill workspace */} +
+
+
+
+ Windmill + Windmill UI + {phase >= 6 && ( + + new version + + )} +
+
+ process_orders.ts +
+
+
+ {phase === 5 ? ( +
+ + + + + + Syncing... +
+ ) : ( +
+ {phase >= 6 ? ( + + ) : ( + + )} +
+ )} +
+
+
+ + {/* Right card: Local (dark terminal style) */} +
+
+
+
+ Cursor + Local dev + {phase >= 4 && phase < 6 && ( + + modified + + )} + {phase >= 6 && ( + + pushed + + )} +
+ {phase >= 3 ? ( +
+ process_orders.ts +
+ ) : ( +
+ no file +
+ )} +
+
+ {phase < 2 ? ( +
+ empty +
+ ) : phase === 2 ? ( +
+ + + + + + Syncing... +
+ ) : ( +
+ = 4} staggerDelay={phase === 3 ? 0 : 0} /> +
+ )} +
+
+
+
+ + {phase >= 6 && ( + + + Workspaces in sync + + )} +
+
+
+ ); +} diff --git a/src/components/products/ProductCTA.tsx b/src/components/products/ProductCTA.tsx new file mode 100644 index 000000000..4375a264f --- /dev/null +++ b/src/components/products/ProductCTA.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { ArrowRight } from 'lucide-react'; +import BookDemoModal from '../BookDemoModal'; + +const fadeIn = { + initial: { opacity: 0, y: 30 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true }, + transition: { duration: 0.5 }, +}; + +export default function ProductCTA() { + const [bookDemoOpen, setBookDemoOpen] = useState(false); + + return ( +
+ +

+ Build your internal platform on Windmill +

+

+ Scripts, flows, apps, and infrastructure in one place. +

+
+ (window as any).plausible?.('try-cloud')} + data-analytics='"try-cloud"' + className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 transition-colors !no-underline" + rel="nofollow" + > + Get started for free + + + +
+
+ +
+ ); +} diff --git a/src/components/products/ProductPageLayout.tsx b/src/components/products/ProductPageLayout.tsx new file mode 100644 index 000000000..0c48db7a8 --- /dev/null +++ b/src/components/products/ProductPageLayout.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import Footer from '../../landing/Footer'; +import LandingHeader from '../../landing/LandingHeader'; +import Head from '@docusaurus/Head'; +import SeoHead from '../SeoHead'; +import RadialBlur from '../../landing/RadialBlur'; +import LayoutProvider from '@theme/Layout/Provider'; + +interface ProductData { + slug: string; + name: string; + headline: string; + description: string; + link: string; +} + +interface ProductPageLayoutProps { + Content: React.ComponentType<{ title: string; description: string }>; + frontMatter: { + title: string; + description: string; + }; + productData?: ProductData; +} + +export default function ProductPageLayout({ Content, frontMatter, productData }: ProductPageLayoutProps) { + const pageUrl = productData + ? `https://www.windmill.dev${productData.link}` + : 'https://www.windmill.dev'; + + const pageSchema = productData + ? { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: `Windmill ${productData.name}`, + headline: productData.headline, + description: productData.description, + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Web', + publisher: { + '@type': 'Organization', + name: 'Windmill', + logo: { + '@type': 'ImageObject', + url: 'https://www.windmill.dev/img/windmill.svg' + } + }, + url: pageUrl + } + : null; + + return ( + +
+ + + {pageSchema && ( + + + + )} + <> + + + +
+
+ ); +} diff --git a/src/components/products/RbacHeroAnimation.jsx b/src/components/products/RbacHeroAnimation.jsx new file mode 100644 index 000000000..e8e08caa4 --- /dev/null +++ b/src/components/products/RbacHeroAnimation.jsx @@ -0,0 +1,115 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView, AnimatePresence } from 'framer-motion'; +import { Shield, CheckCircle } from 'lucide-react'; + +// Ordered newest-first (anti-chronological) +const auditRows = [ + { time: '09:42:35', userName: 'Sam Admin', action: 'permission.change', actionColor: 'bg-amber-50 text-amber-700 border-amber-200', resource: 'f/staging/ +engineering:writer' }, + { time: '09:42:18', userName: 'Alice Lee', action: 'create', actionColor: 'bg-blue-50 text-blue-700 border-blue-200', resource: 'f/staging/new_flow.yaml' }, + { time: '09:42:01', userName: 'Tom Kim', action: 'execute', actionColor: 'bg-green-50 text-green-700 border-green-200', resource: 'f/production/deploy.ts' }, + { time: '09:41:44', userName: 'Mike Jones', action: 'secret.access', actionColor: 'bg-red-50 text-red-700 border-red-200', resource: 'DB_PASSWORD → injected' }, + { time: '09:41:28', userName: 'Sam Admin', action: 'permission.change', actionColor: 'bg-amber-50 text-amber-700 border-amber-200', resource: 'f/production/ +ops:viewer' }, + { time: '09:41:15', userName: 'Sam Admin', action: 'deploy', actionColor: 'bg-blue-50 text-blue-700 border-blue-200', resource: 'f/production/deploy.ts' }, + { time: '09:41:02', userName: 'Alice Lee', action: 'login', actionColor: 'bg-green-50 text-green-700 border-green-200', resource: 'SSO via Okta' }, +]; + +// Number of rows visible initially (the 3 oldest) +const INITIAL_COUNT = 3; + +export default function RbacHeroAnimation() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + + // Phase 1: card + header + 3 oldest rows + // Phase 2..5: one newer row prepended each + // Phase 6: summary + const totalNewRows = auditRows.length - INITIAL_COUNT; // 4 + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 300), + ...Array.from({ length: totalNewRows }, (_, i) => + setTimeout(() => setPhase(i + 2), 1000 + i * 500) + ), + setTimeout(() => setPhase(totalNewRows + 2), 1000 + totalNewRows * 500 + 600), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView]); + + // How many rows from the top are visible + // Phase 1: last INITIAL_COUNT rows (indices 4,5,6) + // Phase 2: index 3 added, Phase 3: index 2, Phase 4: index 1, Phase 5: index 0 + const visibleFromIndex = phase < 1 + ? auditRows.length + : Math.max(0, auditRows.length - INITIAL_COUNT - (phase - 1)); + + const visibleRows = auditRows.slice(visibleFromIndex); + const summaryPhase = totalNewRows + 2; + + return ( +
+
+ = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + style={{ width: 540 }} + > +
+ {/* Header with column labels */} +
+ Time + User + Action + Resource +
+ +
+ {/* Log rows */} +
+ + {visibleRows.map((row) => ( + + {row.time} +
+ {row.userName} +
+
+ + {row.action} + +
+ {row.resource} +
+ ))} +
+
+ + {/* Summary */} + {phase >= summaryPhase && ( + + + + SOC 2 Type II · Every action recorded + + )} +
+
+
+
+
+ ); +} diff --git a/src/components/products/SandboxesHeroAnimation.jsx b/src/components/products/SandboxesHeroAnimation.jsx new file mode 100644 index 000000000..c6351a054 --- /dev/null +++ b/src/components/products/SandboxesHeroAnimation.jsx @@ -0,0 +1,250 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView, AnimatePresence } from 'framer-motion'; +import { Shield, CheckCircle, CheckCircle2, Loader2, GitPullRequest } from 'lucide-react'; +import { SiTypescript } from 'react-icons/si'; + +const DAG_CX = 130; +const DAG_NH = 40; +const DAG_W = 230; +const DAG_GAP = 70; + +export default function SandboxesHeroAnimation() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + + const linearY = 20; + const sandboxY = linearY + DAG_NH + DAG_GAP; + const resultY = sandboxY + DAG_NH + DAG_GAP; + + const ballPositions = { + 2: linearY + DAG_NH / 2, + 3: sandboxY + DAG_NH / 2, + 4: sandboxY + DAG_NH / 2, + 5: resultY + DAG_NH / 2, + }; + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 300), + setTimeout(() => setPhase(2), 900), + setTimeout(() => setPhase(3), 2200), + setTimeout(() => setPhase(4), 2900), + setTimeout(() => setPhase(5), 6400), + setTimeout(() => setPhase(6), 7200), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView]); + + const linearActive = phase >= 2 && phase < 3; + const sandboxActive = phase >= 3 && phase < 5; + const resultActive = phase >= 5 && phase <= 6; + + return ( +
+
+
+ + {/* SVG edges + ball */} + {phase >= 1 && ( + + + + + {/* Animated ball */} + {phase >= 2 && ( + = 6 ? 0 : 1 }} + transition={{ duration: 0.7, ease: 'linear' }} + /> + )} + + )} + + {/* Linear node */} + {phase >= 1 && ( + +
+ {linearActive && ( + + )} +
+ + Fetch Linear issues +
+ + {/* Issues log card */} + {phase >= 2 && ( +
+ +
+
+ + 3 issues found +
+
+
+
+ )} +
+
+ )} + + {/* Sandbox node */} + {phase >= 1 && ( + +
+ {sandboxActive && ( + + )} +
+ + Fix issues and open PR +
+ + {/* NSJAIL panel: full when active, collapsed after */} + {phase >= 4 && ( +
+ + {phase < 5 ? ( + +
+
+ + + NSJAIL · Isolated sandbox + +
+
+
+ + Fix WM-142: auth timeout + + + 💭 Analyzing the issue... + + + + Read + src/auth.ts + + + + Edit + src/auth.ts:42 + + + + Bash + npm test -- auth + + + ✓ 4 passed + + + + Bash + git push origin fix/wm-142 + + + + Bash + gh pr create + + + + + + +
+
+
+
+ ) : ( + +
+
+ + Issues fixed and PR opened +
+
+
+ )} +
+
+ )} +
+
+ )} + + {/* Result node */} + {phase >= 1 && ( + +
+ {resultActive && ( + + )} +
+ + Result +
+ + {/* PR output tooltip next to Result */} + {phase >= 6 && ( +
+ +
+
+ + #284 Fix linear issues +
+
+
+
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/src/components/products/SelfHostHeroAnimationA.jsx b/src/components/products/SelfHostHeroAnimationA.jsx new file mode 100644 index 000000000..e9e6ca94f --- /dev/null +++ b/src/components/products/SelfHostHeroAnimationA.jsx @@ -0,0 +1,186 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { Plane, Monitor, CheckCircle } from 'lucide-react'; +import { + SiDocker, + SiKubernetes, + SiAmazonaws, + SiGooglecloud, + SiMicrosoftazure, + SiHetzner, + SiRender, + SiDigitalocean, +} from 'react-icons/si'; + +const targets = [ + { id: 'local', label: 'Local', Icon: Monitor, color: '#6B7280' }, + { id: 'docker', label: 'Docker', Icon: SiDocker, color: '#2496ED' }, + { id: 'kubernetes', label: 'Kubernetes', Icon: SiKubernetes, color: '#326CE5' }, + { id: 'aws', label: 'AWS', Icon: SiAmazonaws, color: '#FF9900' }, + { id: 'gcp', label: 'GCP', Icon: SiGooglecloud, color: '#4285F4' }, + { id: 'azure', label: 'Azure', Icon: SiMicrosoftazure, color: '#0078D4' }, + { id: 'hetzner', label: 'Hetzner', Icon: SiHetzner, color: '#D50C2D' }, + { id: 'flyio', label: 'Fly.io', Icon: Plane, color: '#7B3FE4' }, + { id: 'render', label: 'Render', Icon: SiRender, color: '#46E3B7' }, + { id: 'digitalocean', label: 'DigitalOcean', Icon: SiDigitalocean, color: '#0080FF' }, +]; + +function TargetCard({ target, index, isSelected }) { + const TargetIcon = target.Icon; + return ( + + + + {target.label} + + + ); +} + +export default function SelfHostHeroAnimationA() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + const [cycle, setCycle] = useState(0); + + // Phase 0: nothing + // Phase 1: card with header + grid of logos, Deploy button disabled + // Phase 2: Docker gets selected, Deploy button becomes enabled + // Phase 3: Deploy button press animation + // Phase 4: Grid replaced by spinning Windmill logo + success message + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 400), + setTimeout(() => setPhase(2), 2000), + setTimeout(() => setPhase(3), 3400), + setTimeout(() => setPhase(4), 3800), + setTimeout(() => { + setPhase(0); + setCycle(c => c + 1); + }, 7500), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView, cycle]); + + const CARD_HEIGHT = 340; + const deployEnabled = phase >= 2; + const deployPressed = phase === 3; + + return ( +
+
+ = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + style={{ maxWidth: 520 }} + className="w-full mx-auto" + > +
+ {/* Header */} +
+ Self host + {phase < 4 && ( + +
+ Deploy +
+
+ )} +
+ + {/* Content */} +
+ {/* Logo grid */} + {phase >= 1 && phase < 4 && ( +
+
+ Deploy on +
+
+ {targets.map((target, i) => ( + = 2 && target.id === 'docker'} + /> + ))} +
+
+ )} + + {/* Success state */} + {phase >= 4 && ( + + + + + + Deployed successfully + + + + )} +
+
+
+
+
+ ); +} diff --git a/src/components/products/SelfHostHeroAnimationA2.jsx b/src/components/products/SelfHostHeroAnimationA2.jsx new file mode 100644 index 000000000..5b9072647 --- /dev/null +++ b/src/components/products/SelfHostHeroAnimationA2.jsx @@ -0,0 +1,180 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { Plane, Monitor, CloudCog } from 'lucide-react'; +import { + SiDocker, + SiKubernetes, + SiHelm, + SiTerraform, + SiAmazonecs, + SiWindows, + SiAmazonaws, + SiGooglecloud, + SiMicrosoftazure, + SiHetzner, + SiRender, + SiDigitalocean, +} from 'react-icons/si'; + +const deployMethods = [ + { id: 'docker', label: 'Docker', Icon: SiDocker, color: '#2496ED' }, + { id: 'kubernetes', label: 'Kubernetes', Icon: SiKubernetes, color: '#326CE5' }, + { id: 'helm', label: 'Helm', Icon: SiHelm, color: '#0F1689' }, + { id: 'ecs', label: 'AWS ECS', Icon: SiAmazonecs, color: '#FF9900' }, + { id: 'cloudformation', label: 'CloudFormation', Icon: CloudCog, color: '#FF4F8B' }, + { id: 'terraform', label: 'Terraform', Icon: SiTerraform, color: '#7B42BC' }, + { id: 'windows', label: 'Windows', Icon: SiWindows, color: '#0078D6' }, +]; + +const infraProviders = [ + { id: 'local', label: 'Local', Icon: Monitor, color: '#6B7280' }, + { id: 'aws', label: 'AWS', Icon: SiAmazonaws, color: '#FF9900' }, + { id: 'gcp', label: 'GCP', Icon: SiGooglecloud, color: '#4285F4' }, + { id: 'azure', label: 'Azure', Icon: SiMicrosoftazure, color: '#0078D4' }, + { id: 'hetzner', label: 'Hetzner', Icon: SiHetzner, color: '#D50C2D' }, + { id: 'flyio', label: 'Fly.io', Icon: Plane, color: '#7B3FE4' }, + { id: 'render', label: 'Render', Icon: SiRender, color: '#46E3B7' }, + { id: 'digitalocean', label: 'DigitalOcean', Icon: SiDigitalocean, color: '#0080FF' }, +]; + +const selectedIds = new Set(['docker', 'aws']); + +function ProviderCard({ provider, index, showSelected }) { + const ProviderIcon = provider.Icon; + const isSelected = showSelected && selectedIds.has(provider.id); + return ( + + + + {provider.label} + + + ); +} + +export default function SelfHostHeroAnimationA2() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 400), + setTimeout(() => setPhase(2), 1800), + setTimeout(() => setPhase(3), 2200), + setTimeout(() => setPhase(4), 4000), + setTimeout(() => setPhase(5), 5400), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView]); + + const CARD_HEIGHT = 380; + + return ( +
+
+ = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + style={{ maxWidth: 520 }} + className="w-full mx-auto" + > +
+
+ {/* Deploy button centered */} + {phase >= 1 && phase < 3 && ( + +
+ Deploy +
+
+ )} + + {/* Logos grid */} + {phase >= 3 && ( + + {/* Deploy with (methods) */} +
+
+ Deploy with +
+
+ {deployMethods.map((method, i) => ( + = 4} + /> + ))} +
+
+ + {/* On (infrastructure providers) */} +
+
+ On +
+
+ {infraProviders.map((provider, i) => ( + = 4} + /> + ))} +
+
+
+ )} +
+
+
+
+
+ ); +} diff --git a/src/components/products/SelfHostHeroAnimationB.jsx b/src/components/products/SelfHostHeroAnimationB.jsx new file mode 100644 index 000000000..470fabe47 --- /dev/null +++ b/src/components/products/SelfHostHeroAnimationB.jsx @@ -0,0 +1,244 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { Shield, Lock, Database, Server, Cpu, Globe, Webhook, Calendar, CheckCircle } from 'lucide-react'; + +const vpcNodes = [ + { id: 'server', label: 'Server', Icon: Server, x: 30, y: 60 }, + { id: 'workers', label: 'Workers', Icon: Cpu, x: 30, y: 130 }, + { id: 'pg', label: 'PostgreSQL', Icon: Database, LockIcon: Lock, x: 30, y: 200 }, +]; + +const externalSources = [ + { id: 'webhook', label: 'Webhook', Icon: Webhook }, + { id: 'api', label: 'API', Icon: Globe }, + { id: 'cron', label: 'Cron', Icon: Calendar }, +]; + +export default function SelfHostHeroAnimationB() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + const [cycle, setCycle] = useState(0); + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 400), + setTimeout(() => setPhase(2), 1200), + setTimeout(() => setPhase(3), 2200), + setTimeout(() => setPhase(4), 3200), + setTimeout(() => setPhase(5), 4600), + setTimeout(() => setPhase(6), 6000), + setTimeout(() => { + setPhase(0); + setCycle(c => c + 1); + }, 7500), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView, cycle]); + + return ( +
+
+ = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + className="w-full" + > +
+ {/* Header */} +
+ + Data sovereignty +
+ + {/* Content */} +
+ {/* Left: VPC region */} +
+ {/* VPC dashed border */} + {phase >= 1 && ( + +
+ + + Your VPC + +
+
+ )} + + {/* VPC nodes */} + {phase >= 2 && ( +
+ {/* SVG connector lines */} + + + + + {/* Animated data dot traveling down */} + {phase >= 4 && phase < 6 && ( + = 5 ? 200 : 130, + opacity: 1, + }} + transition={{ duration: 1.2, ease: 'easeInOut' }} + /> + )} + + + {vpcNodes.map((node, i) => { + const NodeIcon = node.Icon; + const isActive = phase >= 4 && ( + (node.id === 'server' && phase >= 4) || + (node.id === 'workers' && phase >= 5) || + (node.id === 'pg' && phase >= 5) + ); + return ( + +
+ + {node.label} + {node.LockIcon && ( + + )} +
+ + {/* DB insert label */} + {node.id === 'pg' && phase >= 5 && ( + + 3 rows inserted + + )} +
+ ); + })} +
+ )} + + {/* Shield overlay */} + {phase >= 6 && ( + +
+ +
+ + +
+
+ + Your data never leaves your infrastructure + +
+
+ )} +
+ + {/* Right: External sources */} +
+
+ External sources +
+ + {phase >= 3 && ( +
+ {externalSources.map((source, i) => { + const SourceIcon = source.Icon; + const isDimmed = phase >= 6; + return ( + = 4 && !isDimmed ? 'border-blue-200 bg-blue-50/30' : 'border-gray-200 bg-gray-50'}`} + initial={{ opacity: 0, x: 10 }} + animate={{ opacity: isDimmed ? 0.3 : 1, x: 0 }} + transition={{ duration: 0.3, delay: i * 0.15 }} + > + + + {source.label} + + + {/* Animated arrow */} + {phase >= 4 && phase < 6 && ( + + ← + + )} + + {phase >= 6 && ( + + )} + + ); + })} +
+ )} +
+
+
+
+
+
+ ); +} diff --git a/src/components/products/SelfHostHeroAnimationC.jsx b/src/components/products/SelfHostHeroAnimationC.jsx new file mode 100644 index 000000000..e9d505487 --- /dev/null +++ b/src/components/products/SelfHostHeroAnimationC.jsx @@ -0,0 +1,200 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { Database, Server, Cpu, HardDrive, CheckCircle } from 'lucide-react'; +import { SiAmazonaws, SiGooglecloud, SiMicrosoftazure } from 'react-icons/si'; + +const providers = [ + { id: 'aws', label: 'AWS', Icon: SiAmazonaws, tint: 'border-orange-300', bg: 'bg-orange-50/40', text: 'text-orange-600', deployLabel: 'helm install' }, + { id: 'gcp', label: 'GCP', Icon: SiGooglecloud, tint: 'border-blue-300', bg: 'bg-blue-50/40', text: 'text-blue-600', deployLabel: 'helm install' }, + { id: 'azure', label: 'Azure', Icon: SiMicrosoftazure, tint: 'border-cyan-300', bg: 'bg-cyan-50/40', text: 'text-cyan-600', deployLabel: 'helm install' }, + { id: 'bare', label: 'Bare metal', Icon: HardDrive, tint: 'border-slate-400', bg: 'bg-slate-50/40', text: 'text-slate-600', deployLabel: 'docker compose' }, +]; + +const archNodes = [ + { id: 'pg', label: 'PostgreSQL', Icon: Database }, + { id: 'server', label: 'Server', Icon: Server }, + { id: 'workers', label: 'Workers', Icon: Cpu }, +]; + +export default function SelfHostHeroAnimationC() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + const [cycle, setCycle] = useState(0); + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 400), + setTimeout(() => setPhase(2), 1000), + setTimeout(() => setPhase(3), 1800), + setTimeout(() => setPhase(4), 3000), + setTimeout(() => setPhase(5), 4200), + setTimeout(() => setPhase(6), 5400), + setTimeout(() => setPhase(7), 6600), + setTimeout(() => { + setPhase(0); + setCycle(c => c + 1); + }, 7800), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView, cycle]); + + // Which provider is active (phases 3-6 map to providers 0-3) + const activeProvider = phase >= 3 && phase <= 6 ? phase - 3 : -1; + // Which providers have checkmarks (all completed providers) + const checkedProviders = phase >= 3 ? Math.min(phase - 2, 4) : 0; + const allDone = phase >= 7; + + // Get border color for architecture nodes + const getArchBorder = () => { + if (allDone) return 'border-gray-200'; + if (activeProvider >= 0) return providers[activeProvider].tint; + return 'border-gray-200'; + }; + + const getArchBg = () => { + if (allDone) return 'bg-gray-50'; + if (activeProvider >= 0) return providers[activeProvider].bg; + return 'bg-gray-50'; + }; + + return ( +
+
+ = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + style={{ maxWidth: 520 }} + className="w-full mx-auto" + > +
+ {/* Header */} +
+ + Cloud agnostic + {allDone && ( + + Same stack, any infrastructure + + )} +
+ +
+ {/* Provider pills */} + {phase >= 2 && ( + + {providers.map((provider, i) => { + const ProviderIcon = provider.Icon; + const isActive = activeProvider === i; + const isChecked = i < checkedProviders; + + return ( + + + + {provider.label} + + {isChecked && ( + + + + )} + + ); + })} + + )} + + {/* Deploy label */} + {activeProvider >= 0 && ( + + + {providers[activeProvider].deployLabel} + + + )} + + {/* Architecture nodes */} + {phase >= 1 && ( +
+ {/* SVG connector lines */} + + + + + +
+ {archNodes.map((node, i) => { + const NodeIcon = node.Icon; + return ( + + = 0 ? providers[activeProvider].text.replace('text-', 'text-') : 'text-gray-400' + }`} /> + {node.label} + + ); + })} +
+
+ )} +
+
+
+
+
+ ); +} diff --git a/src/components/products/TriggerHeroAnimation.jsx b/src/components/products/TriggerHeroAnimation.jsx new file mode 100644 index 000000000..76cf2d466 --- /dev/null +++ b/src/components/products/TriggerHeroAnimation.jsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { FileCode, Zap, Mail, Database, Calendar, Webhook, Radio, Rss, Globe, Cloud, MessageSquare, Waypoints, Route, MonitorPlay, Plug, RefreshCw } from 'lucide-react'; +import { SiTypescript } from 'react-icons/si'; + +const triggerTypes = [ + // Column 1 + { id: 'schedule', label: 'Schedule', icon: Calendar, color: 'text-blue-500' }, + { id: 'webhook', label: 'Webhook', icon: Webhook, color: 'text-orange-500' }, + { id: 'http', label: 'HTTP routes', icon: Route, color: 'text-cyan-500' }, + { id: 'kafka', label: 'Kafka', icon: Waypoints, color: 'text-emerald-500' }, + { id: 'postgres', label: 'Postgres CDC', icon: Database, color: 'text-violet-500' }, + { id: 'websocket', label: 'WebSocket', icon: Radio, color: 'text-pink-500' }, + { id: 'email', label: 'Email', icon: Mail, color: 'text-amber-500' }, + { id: 'nats', label: 'NATS', icon: MessageSquare, color: 'text-green-500' }, + // Column 2 + { id: 'sqs', label: 'SQS', icon: Cloud, color: 'text-orange-400' }, + { id: 'mqtt', label: 'MQTT', icon: Rss, color: 'text-teal-500' }, + { id: 'gcppubsub', label: 'GCP Pub/Sub', icon: Cloud, color: 'text-blue-400' }, + { id: 'native', label: 'Native triggers', icon: Globe, color: 'text-indigo-500' }, + { id: 'ui', label: 'Auto-generated UIs',icon: MonitorPlay, color: 'text-purple-500' }, + { id: 'cli', label: 'CLI & API', icon: Zap, color: 'text-gray-500' }, + { id: 'mcp', label: 'MCP', icon: Plug, color: 'text-rose-500' }, + { id: 'polls', label: 'Scheduled polls', icon: RefreshCw, color: 'text-sky-500' }, +]; + +function TriggerRow({ t, i, phase }) { + const activeOrder = ['schedule', 'kafka']; + const activeIdx = activeOrder.indexOf(t.id); + const isOn = activeIdx >= 0 && phase >= 3 + activeIdx; + return ( + = 2 ? { opacity: 1, x: 0 } : {}} + transition={{ duration: 0.3, delay: 0.05 + i * 0.04 }} + > +
+ + {t.label} +
+
+ +
+
+ ); +} + +export default function TriggerHeroAnimation() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + + useEffect(() => { + if (!isInView) return; + const timers = [ + setTimeout(() => setPhase(1), 400), + setTimeout(() => setPhase(2), 1000), + setTimeout(() => setPhase(3), 1800), + setTimeout(() => setPhase(4), 2400), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView]); + + const col1 = triggerTypes.slice(0, 8); + const col2 = triggerTypes.slice(8); + + return ( +
+
+ = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + style={{ width: 460 }} + > +
+
+
+ +
+
process_order.ts
+
+ + = 2 ? { opacity: 1, height: 'auto' } : {}} + transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }} + style={{ overflow: 'hidden' }} + > +
+
+ + Triggers +
+ +
+
+ {col1.map((t, i) => )} +
+
+ {col2.map((t, i) => )} +
+
+
+
+
+
+
+
+ ); +} diff --git a/src/components/products/WorkersHeroAnimation.jsx b/src/components/products/WorkersHeroAnimation.jsx new file mode 100644 index 000000000..f3371afb8 --- /dev/null +++ b/src/components/products/WorkersHeroAnimation.jsx @@ -0,0 +1,241 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, useInView, AnimatePresence } from 'framer-motion'; +import { Database, Server, CheckCircle, Loader2 } from 'lucide-react'; +import { SiTypescript, SiGo, SiGnubash, SiPhp, SiRust } from 'react-icons/si'; + +function PythonColored({ className }) { + return ( + + + + + + + + + + + + + + + ); +} + +const jobQueue = [ + { name: 'process_orders.ts', Icon: SiTypescript, iconColor: 'text-[#3178C6]' }, + { name: 'train_model.py', Icon: PythonColored, iconColor: '' }, + { name: 'sync_inventory.ts', Icon: SiTypescript, iconColor: 'text-[#3178C6]' }, + { name: 'pg_migrate.go', Icon: SiGo, iconColor: 'text-[#00ADD8]' }, + { name: 'send_invoices.py', Icon: PythonColored, iconColor: '' }, + { name: 'resize_images.rs', Icon: SiRust, iconColor: 'text-[#DEA584]' }, + { name: 'aggregate_logs.sh', Icon: SiGnubash, iconColor: 'text-[#4EAA25]' }, + { name: 'daily_report.php', Icon: SiPhp, iconColor: 'text-[#777BB4]' }, +]; + +const workerTags = { + default: { label: 'default', classes: 'bg-blue-100 text-blue-600' }, + dedicated: { label: 'dedicated', classes: 'bg-purple-100 text-purple-600' }, + native: { label: 'native', classes: 'bg-green-100 text-green-600' }, +}; + +export default function WorkersHeroAnimation() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState(0); + const [cycle, setCycle] = useState(0); + + useEffect(() => { + if (!isInView) return; + const CYCLE_DURATION = 8600; + const timers = [ + setTimeout(() => setPhase(1), 400), + setTimeout(() => setPhase(2), 1000), + setTimeout(() => setPhase(3), 2000), + setTimeout(() => setPhase(4), 3200), + setTimeout(() => setPhase(5), 4800), + setTimeout(() => setPhase(6), 6200), + setTimeout(() => setPhase(7), 7400), + setTimeout(() => { + setPhase(0); + setCycle(c => c + 1); + }, CYCLE_DURATION), + ]; + return () => timers.forEach(clearTimeout); + }, [isInView, cycle]); + + const workerStates = (() => { + if (phase < 1) return []; + const w = [ + { id: 'W1', tag: 'default', status: 'idle', job: null, result: null }, + { id: 'W2', tag: 'dedicated', status: 'idle', job: null, result: null }, + ]; + + if (phase >= 3) { + w[0] = { id: 'W1', tag: 'default', status: 'processing', job: 0, result: null }; + } + if (phase >= 4) { + w[0] = { id: 'W1', tag: 'default', status: 'processing', job: 2, result: null }; + w[1] = { id: 'W2', tag: 'dedicated', status: 'processing', job: 1, result: null }; + } + if (phase >= 5) { + w[0] = { id: 'W1', tag: 'default', status: 'processing', job: 4, result: null }; + w[1] = { id: 'W2', tag: 'dedicated', status: 'processing', job: 3, result: null }; + w.push({ id: 'W3', tag: 'native', status: 'processing', job: 5, result: null }); + } + if (phase >= 6) { + w[0] = { id: 'W1', tag: 'default', status: 'processing', job: 7, result: null }; + w[1] = { id: 'W2', tag: 'dedicated', status: 'processing', job: 6, result: null }; + w[2] = { id: 'W3', tag: 'native', status: 'done', job: 5, result: '87ms' }; + } + if (phase >= 7) { + w[0] = { id: 'W1', tag: 'default', status: 'done', job: null, result: '98ms' }; + w[1] = { id: 'W2', tag: 'dedicated', status: 'done', job: null, result: '112ms' }; + w[2] = { id: 'W3', tag: 'native', status: 'done', job: null, result: '91ms' }; + } + return w; + })(); + + const grabbedJobs = new Set(); + if (phase >= 3) grabbedJobs.add(0); + if (phase >= 4) { grabbedJobs.add(1); grabbedJobs.add(2); } + if (phase >= 5) { grabbedJobs.add(3); grabbedJobs.add(4); grabbedJobs.add(5); } + if (phase >= 6) { grabbedJobs.add(6); grabbedJobs.add(7); } + + const visibleJobs = jobQueue.map((j, i) => ({ ...j, index: i })).filter(j => !grabbedJobs.has(j.index)); + + return ( +
+
+ = 1 ? { opacity: 1, y: 0 } : {}} + transition={{ duration: 0.5 }} + className="w-full" + > +
+ {/* Header */} +
+ + Job execution + {phase >= 7 && ( + + 8 jobs · 97ms avg + + )} +
+ + {/* Content: Queue | Workers */} +
+ {/* Left: Queue */} +
+
+ + Queue +
+
+ + {phase >= 2 && visibleJobs.map((job, i) => { + const LangIcon = job.Icon; + return ( + + + {job.name} + + ); + })} + + {phase >= 7 && visibleJobs.length === 0 && ( + + empty + + )} +
+
+ + {/* Right: Workers */} +
+
+ + Workers +
+
+ + {workerStates.map((w, i) => ( + + + {w.id} + + {workerTags[w.tag].label} + +
+ {w.status === 'processing' && ( + <> + + + + {w.job !== null && ( + {jobQueue[w.job]?.name} + )} + + )} + {w.status === 'done' && ( + + + {w.result && {w.result}} + + )} + {w.status === 'idle' && ( + idle + )} +
+
+ ))} +
+
+
+
+
+
+
+
+ ); +} diff --git a/src/components/use-cases/TriggerGrid.js b/src/components/use-cases/TriggerGrid.js new file mode 100644 index 000000000..1210753bb --- /dev/null +++ b/src/components/use-cases/TriggerGrid.js @@ -0,0 +1,86 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import { motion } from 'framer-motion'; +import { + Calendar, + Webhook, + Mail, + Route, + Radio, + Database, + Waypoints, + MessageSquare, + Cloud, + Rss, + Globe, + MonitorPlay, + Zap, + Plug, + RefreshCw, +} from 'lucide-react'; + +const fadeIn = { + initial: { opacity: 0, y: 30 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true }, + transition: { duration: 0.5 }, +}; + +const triggers = [ + { icon: Calendar, label: 'Schedules', desc: 'Cron and visual cron builder', link: '/docs/core_concepts/scheduling' }, + { icon: Webhook, label: 'Webhooks', desc: 'Sync and async HTTP triggers', link: '/docs/core_concepts/webhooks' }, + { icon: Mail, label: 'Emails', desc: 'Trigger on incoming emails', link: '/docs/getting_started/triggers#emails' }, + { icon: Route, label: 'HTTP routes', desc: 'Custom REST endpoints', link: '/docs/core_concepts/http_routing' }, + { icon: Radio, label: 'WebSockets', desc: 'Real-time bidirectional events', link: '/docs/core_concepts/websocket_triggers' }, + { icon: Database, label: 'Postgres', desc: 'CDC and row-level triggers', link: '/docs/core_concepts/postgres_triggers' }, + { icon: Waypoints, label: 'Kafka', desc: 'Consume from Kafka topics', link: '/docs/core_concepts/kafka_triggers' }, + { icon: MessageSquare, label: 'NATS', desc: 'Subscribe to NATS subjects', link: '/docs/core_concepts/nats_triggers' }, + { icon: Cloud, label: 'SQS', desc: 'AWS SQS queue consumer', link: '/docs/core_concepts/sqs_triggers' }, + { icon: Rss, label: 'MQTT', desc: 'IoT and MQTT broker events', link: '/docs/core_concepts/mqtt_triggers' }, + { icon: Cloud, label: 'GCP Pub/Sub', desc: 'Google Cloud Pub/Sub events', link: '/docs/core_concepts/gcp_triggers' }, + { icon: Globe, label: 'Native triggers', desc: 'Nextcloud, Google Drive, Calendar', link: '/docs/core_concepts/native_triggers' }, + { icon: MonitorPlay, label: 'Auto-generated UIs', desc: 'One-click run from the UI', link: '/docs/getting_started/triggers#auto-generated-uis' }, + { icon: Zap, label: 'CLI & API', desc: 'Trigger from scripts or CI/CD', link: '/docs/getting_started/triggers#trigger-from-api' }, + { icon: Plug, label: 'MCP', desc: 'Trigger from LLM clients', link: '/docs/core_concepts/mcp' }, + { icon: RefreshCw, label: 'Scheduled polls', desc: 'Poll APIs on an interval', link: '/docs/core_concepts/scheduling#scheduled-polls' }, +]; + +export default function TriggerGrid({ title, subtitle }) { + return ( +
+ {title !== null && ( + +

+ {title || 'Every way to trigger a script or flow'} +

+ {subtitle && ( +

{subtitle}

+ )} +
+ )} + +
+ {triggers.map((trigger) => { + const Icon = trigger.icon; + return ( + +
+ + + {trigger.label} + +
+ + {trigger.desc} + + + ); + })} +
+
+ ); +} diff --git a/src/components/use-cases/TriggerSection.js b/src/components/use-cases/TriggerSection.js new file mode 100644 index 000000000..788f1218a --- /dev/null +++ b/src/components/use-cases/TriggerSection.js @@ -0,0 +1,38 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import { motion } from 'framer-motion'; +import { BookOpen } from 'lucide-react'; +import TriggerGrid from './TriggerGrid'; + +const fadeIn = { + initial: { opacity: 0, y: 30 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true }, + transition: { duration: 0.5 } +}; + +export default function TriggerSection() { + return ( +
+ +

Triggers

+

+ Any script or workflow can be triggered by{' '} + schedules,{' '} + webhooks, + {' '}message queues, database changes, emails, and{' '} + more. +

+ + Read the docs + +
+
+ +
+
+ ); +} diff --git a/src/components/use-cases/UseCaseCarousel.js b/src/components/use-cases/UseCaseCarousel.js index 6e0932033..cc7a74418 100644 --- a/src/components/use-cases/UseCaseCarousel.js +++ b/src/components/use-cases/UseCaseCarousel.js @@ -18,7 +18,7 @@ const allUseCases = [ { label: 'Triggers', subtitle: 'Trigger scripts and flows from schedules, webhooks, Kafka, Postgres, websockets, emails and more.', to: '/use-cases/triggers', cover: '/img/money-pages/cron-schedules-card.webp' }, ]; -export default function UseCaseCarousel({ current, subtitle }) { +export default function UseCaseCarousel({ current, title, subtitle }) { const carouselRef = useRef(null); const useCases = allUseCases.filter((uc) => uc.to !== `/use-cases/${current}`); @@ -33,7 +33,7 @@ export default function UseCaseCarousel({ current, subtitle }) {

- More you can build on Windmill + {title || 'More you can build on Windmill'}

{subtitle && (

{subtitle}

diff --git a/src/data/products/app-builder.js b/src/data/products/app-builder.js new file mode 100644 index 000000000..3201e5bb4 --- /dev/null +++ b/src/data/products/app-builder.js @@ -0,0 +1,7 @@ +export const appBuilderProduct = { + slug: 'app-builder', + name: 'App builder', + headline: 'Full-code frontends connected to your backend', + description: 'Build custom UIs in React or Svelte with an auto-generated, type-safe API to backend runnables in 20+ languages.', + link: '/platform/app-builder', +}; diff --git a/src/data/products/datatables.js b/src/data/products/datatables.js new file mode 100644 index 000000000..62194b780 --- /dev/null +++ b/src/data/products/datatables.js @@ -0,0 +1,7 @@ +export const datatablesProduct = { + slug: 'datatables', + name: 'Data tables', + headline: 'SQL storage with near-zero setup', + description: 'Store and query relational data from scripts, flows, and apps. Windmill manages the database, credentials, and schema so you just write SQL.', + link: '/platform/datatables', +}; diff --git a/src/data/products/index.js b/src/data/products/index.js new file mode 100644 index 000000000..67554810a --- /dev/null +++ b/src/data/products/index.js @@ -0,0 +1,15 @@ +// Build +export { scriptEditorProduct } from './script-editor'; +export { workflowEditorProduct } from './flow-editor'; +export { appBuilderProduct } from './app-builder'; +export { datatablesProduct } from './datatables'; +export { triggersProduct } from './triggers'; +export { versioningProduct } from './versioning'; +export { localDevProduct } from './local-dev'; + +// Run +export { observabilityProduct } from './observability'; +export { rbacProduct } from './rbac'; +export { sandboxesProduct } from './sandboxes'; +export { workersProduct } from './workers'; +export { selfHostProduct } from './self-host'; diff --git a/src/data/products/local-dev.js b/src/data/products/local-dev.js new file mode 100644 index 000000000..8d49a4b5e --- /dev/null +++ b/src/data/products/local-dev.js @@ -0,0 +1,7 @@ +export const localDevProduct = { + slug: 'local-dev', + name: 'Local dev', + headline: 'Develop locally, deploy to Windmill', + description: 'Use the CLI, VS Code extension, and AI coding assistants to build scripts and flows from your own environment.', + link: '/platform/local-dev', +}; diff --git a/src/data/products/observability.js b/src/data/products/observability.js new file mode 100644 index 000000000..e525fbd97 --- /dev/null +++ b/src/data/products/observability.js @@ -0,0 +1,7 @@ +export const observabilityProduct = { + slug: 'observability', + name: 'Observability', + headline: 'Every run, logged and searchable', + description: 'Real-time logs, job metrics, run history, and alerting across all your scripts and flows.', + link: '/platform/observability', +}; diff --git a/src/data/products/rbac.js b/src/data/products/rbac.js new file mode 100644 index 000000000..97276333d --- /dev/null +++ b/src/data/products/rbac.js @@ -0,0 +1,7 @@ +export const rbacProduct = { + slug: 'rbac', + name: 'RBAC', + headline: 'Role-based access controls', + description: 'Enforce role-based access, audit logs, secret management and compliance controls.', + link: '/platform/rbac', +}; diff --git a/src/data/products/sandboxes.js b/src/data/products/sandboxes.js new file mode 100644 index 000000000..1d1ced22f --- /dev/null +++ b/src/data/products/sandboxes.js @@ -0,0 +1,7 @@ +export const sandboxesProduct = { + slug: 'sandboxes', + name: 'AI sandboxes', + headline: 'Isolated environments for AI agents', + description: 'How do I run AI agents safely in Windmill? Isolated sandboxes for Claude Code, Codex, or custom agents with persistent storage and pre-configured tools.', + link: '/platform/sandboxes', +}; diff --git a/src/data/products/script-editor.js b/src/data/products/script-editor.js new file mode 100644 index 000000000..92222965d --- /dev/null +++ b/src/data/products/script-editor.js @@ -0,0 +1,7 @@ +export const scriptEditorProduct = { + slug: 'script-editor', + name: 'Script editor', + headline: 'Code editor with built-in infrastructure', + description: 'Write a function in TypeScript, Python, Go, Bash, or SQL. Windmill gives you a UI, an API endpoint, triggers, and resource access automatically.', + link: '/platform/script-editor', +}; diff --git a/src/data/products/self-host.js b/src/data/products/self-host.js new file mode 100644 index 000000000..9bcabb37e --- /dev/null +++ b/src/data/products/self-host.js @@ -0,0 +1,7 @@ +export const selfHostProduct = { + slug: 'self-host', + name: 'No-ops self-host', + headline: 'Your infrastructure, zero maintenance', + description: 'Deploy Windmill on your own infrastructure with zero maintenance and no ops overhead.', + link: '/platform/self-host', +}; diff --git a/src/data/products/triggers.js b/src/data/products/triggers.js new file mode 100644 index 000000000..9c25dd517 --- /dev/null +++ b/src/data/products/triggers.js @@ -0,0 +1,7 @@ +export const triggersProduct = { + slug: 'triggers', + name: 'Triggers', + headline: 'Every way to start a script or flow', + description: 'How do I trigger scripts and flows in Windmill? Schedules, webhooks, Kafka, Postgres CDC, WebSockets, emails, and more.', + link: '/platform/triggers', +}; diff --git a/src/data/products/versioning.js b/src/data/products/versioning.js new file mode 100644 index 000000000..2485dddc9 --- /dev/null +++ b/src/data/products/versioning.js @@ -0,0 +1,7 @@ +export const versioningProduct = { + slug: 'deployment-versioning', + name: 'Deployment & versioning', + headline: 'Deploy in one click. Version with Git.', + description: 'Deploy in one click with built-in versioning, or connect Git for source control, local development, and CI/CD pipelines.', + link: '/platform/deployment-versioning', +}; diff --git a/src/data/products/workers.js b/src/data/products/workers.js new file mode 100644 index 000000000..956235430 --- /dev/null +++ b/src/data/products/workers.js @@ -0,0 +1,7 @@ +export const workersProduct = { + slug: 'workers', + name: 'Workers', + headline: 'Isolated workers that scale with your workload', + description: 'How do Windmill workers work? Route workloads by tag, scale horizontally, and deploy workers anywhere.', + link: '/platform/workers', +}; diff --git a/src/data/products/workflow-editor.js b/src/data/products/workflow-editor.js new file mode 100644 index 000000000..34a6ac451 --- /dev/null +++ b/src/data/products/workflow-editor.js @@ -0,0 +1,7 @@ +export const workflowEditorProduct = { + slug: 'flow-editor', + name: 'flow editor', + headline: 'Visual workflow engine with full code flexibility', + description: 'Orchestrate scripts into DAG-based flows with branching, loops, retries, approval steps, and full observability.', + link: '/platform/flow-editor', +}; diff --git a/src/landing/Hero.jsx b/src/landing/Hero.jsx index cedfe0673..596321e7f 100644 --- a/src/landing/Hero.jsx +++ b/src/landing/Hero.jsx @@ -4,6 +4,7 @@ import RadialBlur from './RadialBlur'; import HomescreenSvg from '../../static/homescreen.svg'; import Link from '@docusaurus/Link'; import { Copy, Check } from 'lucide-react'; +import HeroCTAButtons from '../components/products/HeroCTAButtons'; function CliSnippet() { const cmd = 'npm install -g windmill-cli'; @@ -73,24 +74,8 @@ export default function Hero() {
-
- window.plausible('try-cloud')} - data-analytics='"try-cloud"' - className="rounded-md transition-all bg-blue-500 px-4 py-2 text-base font-semibold leading-7 text-white hover:bg-blue-800 hover:!text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 !no-underline" - rel="nofollow" - > - Try Windmill cloud - - - window.plausible?.('self-host')} - className="text-base font-semibold leading-7 text-gray-900 dark:text-gray-200 text !no-underline" - > - Self-host in 3 mins - +
+
{/* */}
diff --git a/src/landing/LandingHeader.jsx b/src/landing/LandingHeader.jsx index cdeec0bab..37491ca74 100644 --- a/src/landing/LandingHeader.jsx +++ b/src/landing/LandingHeader.jsx @@ -23,26 +23,97 @@ import { useColorMode } from '@docusaurus/theme-common'; import { SiDiscord, SiGithub } from 'react-icons/si'; import { motion } from 'framer-motion'; import ThemeToggleButton from './ThemeToggleButton'; -import { Bot, Landmark, HeartPulse } from 'lucide-react'; +import { + Bot, Landmark, HeartPulse, + Code, GitFork, LayoutDashboard, Database, GitBranch, Terminal, + Activity, ShieldCheck, Container, Cpu, Server, Zap, +} from 'lucide-react'; import Banner from './Banner'; import Link from '@docusaurus/Link'; -const products = [ +const productCategories = [ { - name: 'Scripts', - description: 'Code to production in minutes.', - href: '/scripts' + title: 'Build', + items: [ + { + name: 'Script editor', + description: 'Write scripts in TypeScript, Python, Go, Bash or SQL.', + href: '/platform/script-editor', + icon: Code, + }, + { + name: 'Flow editor', + description: 'Connect scripts into flows with no glue code.', + href: '/platform/flow-editor', + icon: GitFork, + }, + { + name: 'App builder', + description: 'Connect backend logic to React & Svelte frontends.', + href: '/platform/app-builder', + icon: LayoutDashboard, + }, + { + name: 'Triggers', + description: 'Schedules, webhooks, Kafka, Postgres CDC and more.', + href: '/platform/triggers', + icon: Zap, + }, + { + name: 'Data tables', + description: 'Store and query relational data with managed SQL.', + href: '/platform/datatables', + icon: Database, + }, + { + name: 'Deployment & versioning', + description: 'Sync with Git, stage workspaces and deploy via CI/CD.', + href: '/platform/deployment-versioning', + icon: GitBranch, + }, + ], }, { - name: 'Flows', - description: 'Build complex flows without complexity.', - href: '/flows' + title: 'Run', + items: [ + { + name: 'Local dev', + description: 'Develop and test locally with the Windmill CLI.', + href: '/platform/local-dev', + icon: Terminal, + }, + { + name: 'Workers', + description: 'Isolated workers that pull from a shared queue.', + href: '/platform/workers', + icon: Cpu, + }, + { + name: 'AI sandboxes', + description: 'Run Claude Code, Codex, or custom agents in isolated environments.', + href: '/platform/sandboxes', + icon: Container, + }, + { + name: 'Observability', + description: 'Monitor logs, metrics and alerts in real time.', + href: '/platform/observability', + icon: Activity, + }, + { + name: 'RBAC', + description: 'Enforce role-based access, audit logs and secrets.', + href: '/platform/rbac', + icon: ShieldCheck, + }, + { + name: 'No-ops self-host', + description: 'Deploy Windmill on your infra with zero maintenance.', + href: '/platform/self-host', + icon: Server, + }, + ], }, - { - name: 'Apps', - description: 'Build super fast and powerful apps using drag-and-drop.', - href: '/apps' - } ]; const solutionsByUseCase = [ @@ -70,12 +141,6 @@ const solutionsByUseCase = [ href: '/use-cases/data-pipelines', icon: CircleStackIcon, }, - { - name: 'Triggers', - description: 'Schedules, webhooks, Kafka, Postgres and more.', - href: '/use-cases/triggers', - icon: ClockIcon, - }, ]; // const solutionsByRole = [ @@ -100,6 +165,12 @@ const solutionsByUseCase = [ // ]; const resources = [ + { + name: 'Hub', + description: 'Browse community scripts, flows, and apps.', + href: 'https://hub.windmill.dev', + newtab: true + }, { name: 'OpenAPI', description: 'Explore our API specs.', @@ -170,6 +241,69 @@ export default function LandingHeader() { + + {({ open }) => ( + <> + + Platform + + + + +
+
+
+ {productCategories.map((category) => ( +
+
+ {category.items.map((item) => ( + + +
+

+ {item.name} +

+

+ {item.description} +

+
+ + ))} +
+
+ ))} +
+
+
+
+
+ + )} +
{({ open }) => ( <> @@ -204,9 +338,9 @@ export default function LandingHeader() {
{/* Use cases */}
-

+ {/*

By use case -

+

*/}
{solutionsByUseCase.map((item) => ( - - Hub - - ))}
+
+ +
@@ -321,8 +451,6 @@ export default function LandingHeader() {
- - - - Hub - - + {/* Products section in mobile menu */} + + {/* Solutions section in mobile menu */}
{[ - { title: 'By use case', items: solutionsByUseCase }, + { title: '' /* 'By use case' */, items: solutionsByUseCase }, // { title: 'By role', items: solutionsByRole }, // { title: 'By industry', items: solutionsByIndustry }, // { title: 'Compare', items: solutionsCompare }, diff --git a/src/pages/index.js b/src/pages/index.js index 94d161f5e..ff460bc41 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,8 +1,7 @@ import React from 'react'; import Hero from '../landing/Hero'; import Footer from '../landing/Footer'; -import LandingSection from '../landing/LandingSection'; -import CallToAction from '../landing/CallToAction'; +import ProductCTA from '../components/products/ProductCTA'; import SeoHead from '../components/SeoHead'; import HeroExample from '../landing/HeroExample'; import LandingHeader from '../landing/LandingHeader'; @@ -88,9 +87,7 @@ function HomepageHeader() { - - - +