diff --git a/packages/website/public/homepage-hero.png b/packages/website/public/homepage-hero.png new file mode 100644 index 000000000..6ab4ada9f Binary files /dev/null and b/packages/website/public/homepage-hero.png differ diff --git a/packages/website/src/components/hero-mockup.tsx b/packages/website/src/components/hero-mockup.tsx index 9545b3d57..f1a0733d3 100644 --- a/packages/website/src/components/hero-mockup.tsx +++ b/packages/website/src/components/hero-mockup.tsx @@ -1,1005 +1,15 @@ -import * as React from "react"; -import { motion } from "framer-motion"; -import { - FolderGit2, - Paperclip, - ArrowUp, - X, - MessagesSquare, - Plus, - Settings, - Monitor, - GitCommitHorizontal, - ChevronDown, - ChevronRight, - ListChevronsUpDown, - Bot, - Check, - Loader, - SquareTerminal, - PanelLeft, - Ellipsis, - Mic, - GitPullRequest, - SquarePen, - Columns2, - Rows2, -} from "lucide-react"; -import { ClaudeIcon, CodexIcon, SourceControlIcon } from "~/components/mockup"; - -const USER_BUBBLE_STYLE = { borderRadius: "16px 2px 16px 16px" }; -const NEW_BADGE_STYLE = { backgroundColor: "rgba(46, 160, 67, 0.2)" }; - -// ── Stagger timing ────────────────────────────────────── - -const D = { - shell: 0, - sidebar: 0.1, - titleBar: 0.15, - tabs: 0.25, - chat: 0.35, - diffPanel: 0.55, -}; - -function fade(delay: number) { - return { - initial: { opacity: 0, y: 8 } as const, - animate: { opacity: 1, y: 0 } as const, - transition: { duration: 0.5, delay, ease: "easeOut" as const }, - }; -} - -// ── Data ──────────────────────────────────────────────── - -type ChatItem = - | { type: "user"; text: string } - | { type: "text"; text: string; bold?: boolean } - | { type: "tool"; label: string; summary?: string; status: "running" | "done" }; - -const CHAT: ChatItem[] = [ - { type: "user", text: "Build me an internal dashboard for tracking customer returns" }, - { type: "text", text: "I'll break this down into planning and implementation.", bold: true }, - { type: "tool", label: "Run plan-technical", summary: "codex · plan-technical", status: "done" }, - { type: "tool", label: "Run plan-design", summary: "claude · plan-design", status: "done" }, - { - type: "tool", - label: "Wait for agents", - summary: "plan-technical plan-design", - status: "done", - }, - { type: "text", text: "Got the plans. Spinning up Codex for implementation.", bold: true }, - { type: "tool", label: "Run implement", summary: "codex · 12 files changed", status: "done" }, - { type: "text", text: "Implementation done. Requesting review from Claude." }, - { type: "tool", label: "Run review", summary: "claude · no issues found", status: "running" }, - { type: "text", text: "All tasks complete. Dashboard is ready.", bold: true }, -]; - -// ── Sidebar data ──────────────────────────────────────── - -interface SidebarWorkspace { - name: string; - kind: "checkout" | "worktree"; - status: "running" | "done" | "idle" | "syncing"; - selected?: boolean; - diffStat?: { additions: number; deletions: number }; - pr?: { number: number; state: "open" | "merged" | "closed" }; -} - -interface SidebarProject { - initial: string; - name: string; - workspaces: SidebarWorkspace[]; -} - -const SIDEBAR_PROJECTS: SidebarProject[] = [ - { - initial: "A", - name: "acme/returns-app", - workspaces: [ - { - name: "main", - kind: "checkout", - status: "syncing", - selected: true, - diffStat: { additions: 247, deletions: 15 }, - }, - { - name: "feat/dashboard", - kind: "worktree", - status: "done", - pr: { number: 142, state: "open" }, - }, - ], - }, - { - initial: "P", - name: "acme/payments", - workspaces: [ - { name: "main", kind: "checkout", status: "idle" }, - { - name: "fix/stripe-webhook", - kind: "worktree", - status: "done", - diffStat: { additions: 38, deletions: 4 }, - pr: { number: 89, state: "merged" }, - }, - ], - }, - { - initial: "I", - name: "acme/infra", - workspaces: [ - { name: "main", kind: "checkout", status: "idle" }, - { - name: "feat/k8s-autoscale", - kind: "worktree", - status: "syncing", - diffStat: { additions: 91, deletions: 3 }, - }, - ], - }, - { - initial: "D", - name: "acme/design-system", - workspaces: [{ name: "main", kind: "checkout", status: "idle" }], - }, -]; - -// ── Tab data ───────────────────────────────────────────── - -interface TabDef { - name: string; - provider: "claude" | "codex" | "terminal"; - done: boolean; - active?: boolean; -} - -const TABS: TabDef[] = [ - { name: "Orchestrator", provider: "claude", done: false, active: true }, - { name: "Implement", provider: "codex", done: true }, -]; - -// ── Diff data ────────────────────────────────────────── - -type DiffLineType = "add" | "remove" | "context" | "header"; -interface DiffLine { - type: DiffLineType; - ln: string | null; - content?: string; - tokens?: Array<{ text: string; cls: string }>; -} - -const DIFF_LINES: DiffLine[] = [ - { - type: "add", - ln: "1", - tokens: [ - { text: "import", cls: "text-syn-keyword" }, - { text: " { ", cls: "text-syn-punctuation" }, - { text: "useState", cls: "text-syn-variable" }, - { text: " } ", cls: "text-syn-punctuation" }, - { text: "from", cls: "text-syn-keyword" }, - { text: ' "react"', cls: "text-syn-string" }, - ], - }, - { - type: "add", - ln: "2", - tokens: [ - { text: "import", cls: "text-syn-keyword" }, - { text: " { ", cls: "text-syn-punctuation" }, - { text: "ReturnTable", cls: "text-syn-variable" }, - { text: " } ", cls: "text-syn-punctuation" }, - { text: "from", cls: "text-syn-keyword" }, - { text: ' "./components"', cls: "text-syn-string" }, - ], - }, - { type: "add", ln: "3", tokens: [] }, - { - type: "add", - ln: "4", - tokens: [ - { text: "export", cls: "text-syn-keyword" }, - { text: " function", cls: "text-syn-keyword" }, - { text: " Dashboard", cls: "text-syn-function" }, - { text: "() {", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "5", - tokens: [ - { text: " const", cls: "text-syn-keyword" }, - { text: " [returns, setReturns]", cls: "text-syn-variable" }, - { text: " = ", cls: "text-syn-operator" }, - { text: "useState", cls: "text-syn-function" }, - { text: "([])", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "6", - tokens: [ - { text: " const", cls: "text-syn-keyword" }, - { text: " [filter, setFilter]", cls: "text-syn-variable" }, - { text: " = ", cls: "text-syn-operator" }, - { text: "useState", cls: "text-syn-function" }, - { text: '("all")', cls: "text-syn-string" }, - ], - }, - { type: "add", ln: "7", tokens: [] }, - { - type: "add", - ln: "8", - tokens: [ - { text: " ", cls: "text-syn-punctuation" }, - { text: "return", cls: "text-syn-keyword" }, - { text: " (", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "9", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "main", cls: "text-syn-tag" }, - { text: " className", cls: "text-syn-property" }, - { text: '="', cls: "text-syn-punctuation" }, - { text: "min-h-screen p-8", cls: "text-syn-string" }, - { text: '">', cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "10", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "h1", cls: "text-syn-tag" }, - { text: ">Customer Returns", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "11", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "FilterBar", cls: "text-syn-tag" }, - { text: " value", cls: "text-syn-property" }, - { text: "={", cls: "text-syn-punctuation" }, - { text: "filter", cls: "text-syn-variable" }, - { text: "}", cls: "text-syn-punctuation" }, - { text: " onChange", cls: "text-syn-property" }, - { text: "={", cls: "text-syn-punctuation" }, - { text: "setFilter", cls: "text-syn-variable" }, - { text: "} />", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "12", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "ReturnTable", cls: "text-syn-tag" }, - { text: " data", cls: "text-syn-property" }, - { text: "={", cls: "text-syn-punctuation" }, - { text: "returns", cls: "text-syn-variable" }, - { text: "} />", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "13", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "StatusChart", cls: "text-syn-tag" }, - { text: " data", cls: "text-syn-property" }, - { text: "={", cls: "text-syn-punctuation" }, - { text: "returns", cls: "text-syn-variable" }, - { text: "} />", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "14", - tokens: [ - { text: " ", cls: "text-syn-punctuation" }, - ], - }, - { type: "add", ln: "15", tokens: [{ text: " )", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "16", tokens: [{ text: "}", cls: "text-syn-punctuation" }] }, -]; - -// ── Implementation agent chat data ────────────────────── - -// ── Terminal log lines ───────────────────────────────── - -const TERMINAL_LINES = [ - { id: "cmd", text: "$ npm run dev", cls: "text-mock-fg" }, - { id: "blank1", text: "", cls: "" }, - { id: "pkg", text: "> acme-returns@0.1.0 dev", cls: "text-mock-fg-muted" }, - { id: "pkg-cmd", text: "> next dev --turbopack", cls: "text-mock-fg-muted" }, - { id: "blank2", text: "", cls: "" }, - { id: "next-ver", text: " ▲ Next.js 15.3.1 (Turbopack)", cls: "text-mock-fg" }, - { id: "next-local", text: " - Local: http://localhost:3000", cls: "text-mock-fg-muted" }, - { id: "blank3", text: "", cls: "" }, - { id: "starting", text: " ✓ Starting...", cls: "text-mock-green" }, - { id: "ready", text: " ✓ Ready in 1.2s", cls: "text-mock-green" }, - { id: "compiling-dashboard", text: " ○ Compiling /dashboard ...", cls: "text-mock-fg-muted" }, - { id: "compiled-dashboard", text: " ✓ Compiled /dashboard in 340ms", cls: "text-mock-green" }, - { id: "compiling-api", text: " ○ Compiling /api/returns ...", cls: "text-mock-fg-muted" }, - { id: "compiled-api", text: " ✓ Compiled /api/returns in 120ms", cls: "text-mock-green" }, -]; - -// ── Sub-components ────────────────────────────────────── - -function TrafficLights() { - return ( -
-
-
-
-
- ); -} - -function ProviderIcon({ - provider, - muted = false, -}: { - provider: "claude" | "codex" | "terminal"; - muted?: boolean; -}) { - const cls = muted ? "text-mock-fg-muted" : "text-mock-fg"; - if (provider === "terminal") return ; - return provider === "claude" ? ( - - ) : ( - - ); -} - -function TabBarAction({ - icon: Icon, -}: { - icon: React.ComponentType<{ size?: number; className?: string; strokeWidth?: number }>; -}) { - return ( -
- -
- ); -} - -function PaneTabBar({ tabs, focused = false }: { tabs: TabDef[]; focused?: boolean }) { - return ( -
- {tabs.map((tab) => ( -
- {tab.active && ( -
- )} - -
- -
- - - {tab.name} - - - -
- ))} -
- - - - -
-
- ); -} - -function Composer({ - provider, - model, - focused = false, -}: { - provider: "claude" | "codex"; - model: string; - focused?: boolean; -}) { - const Icon = provider === "claude" ? ClaudeIcon : CodexIcon; - return ( -
-
-
- {focused && ( - - )} - - Message the agent, tag @files, or use /commands and /skills - -
-
-
-
- -
-
- - {model} - -
-
-
-
- -
-
- -
-
-
-
-
- ); -} - -function prStateLabel(state: "open" | "merged" | "closed"): string { - if (state === "open") return "Open"; - if (state === "merged") return "Merged"; - return "Closed"; -} - -function WorkspaceIcon({ workspace }: { workspace: SidebarWorkspace }) { - if (workspace.status === "syncing") return ; - const icon = - workspace.kind === "worktree" ? ( - - ) : ( - - ); - return ( - <> - {icon} - {workspace.status !== "idle" && ( -
- -
- )} - - ); -} - -function SidebarWorkspaceRow({ workspace }: { workspace: SidebarWorkspace }) { - return ( -
-
-
- -
- - {workspace.name} - - {workspace.diffStat && ( -
- - +{workspace.diffStat.additions} - - - -{workspace.diffStat.deletions} - -
- )} -
- {workspace.pr && ( -
- - - #{workspace.pr.number} · {prStateLabel(workspace.pr.state)} - -
- )} -
- ); -} - -function diffLineBg(type: DiffLineType): string { - if (type === "add") return "bg-mock-diff-add"; - if (type === "remove") return "bg-mock-diff-remove"; - return "bg-mock-surface1"; -} - -function diffLineNumCls(type: DiffLineType): string { - if (type === "add") return "text-mock-green-400"; - if (type === "remove") return "text-mock-red"; - return "text-mock-fg-muted"; -} - -function DiffLineRow({ line }: { line: DiffLine }) { - const lineBg = diffLineBg(line.type); - const lineNumCls = diffLineNumCls(line.type); - return ( -
-
- - {line.ln ?? ""} - -
- - {line.tokens?.map((tok) => ( - - {tok.text} - - ))} - -
- ); -} - -function chatItemKey(item: ChatItem): string { - if (item.type === "user") return `user-${item.text}`; - if (item.type === "tool") return `tool-${item.label}-${item.summary ?? ""}`; - return `text-${item.text}`; -} - -function ChatMessages({ items }: { items: ChatItem[] }) { - return ( -
- {items.map((item) => { - const key = chatItemKey(item); - if (item.type === "user") { - return ( -
-
- {item.text} -
-
- ); - } - if (item.type === "tool") { - const isDone = item.status === "done"; - return ( -
-
- -
- - {item.label} - - {item.summary && ( - - {item.summary} - - )} -
- {isDone ? ( - - ) : ( - - )} -
-
- ); - } - return ( -

- {item.text} -

- ); - })} -
- ); -} - -function ChatArea() { - return ( -
- - -
- ); -} - -function TerminalPane() { - return ( -
-
- {TERMINAL_LINES.map((line) => ( -
- {line.text} -
- ))} -
-
- ); -} - -function ExplorerSidebar() { - return ( -
- {/* Explorer header — h-12, Changes/Files tabs */} -
-
-
- Changes -
-
- Files -
-
-
- - {/* Secondary header — h-9, Uncommitted dropdown + filter icons */} -
-
- Uncommitted - -
-
-
- -
-
-
- - {/* File header — expanded */} -
-
- - dashboard.tsx - src/pages - - New - -
-
- +16 - -0 -
-
- - {/* Diff lines */} -
- {DIFF_LINES.map((line) => ( - - ))} -
- - {/* Collapsed file rows */} - {[ - { name: "return-table.tsx", dir: "src/components", added: 42, removed: 8 }, - { name: "filter-bar.tsx", dir: "src/components", added: 28, removed: 3 }, - { name: "status-chart.tsx", dir: "src/components", added: 19, removed: 0, isNew: true }, - { name: "returns.ts", dir: "src/api", added: 12, removed: 5 }, - { name: "index.tsx", dir: "src/pages", added: 6, removed: 2 }, - ].map((file) => ( -
-
- - {file.name} - {file.dir} - {file.isNew && ( - - New - - )} -
-
- +{file.added} - -{file.removed} -
-
- ))} - -
-
- ); -} - -// Snake traversal order matching the real app: [0, 1, 3, 5, 4, 2] (2-col × 3-row grid) -// DOT_SEQUENCE[sequenceIndex] = dotIndex, so sequenceIndex = indexOf(dotIndex) -const DOT_SEQUENCE = [0, 1, 3, 5, 4, 2] as const; -const SYNCED_LOADER_DURATION_MS = 950; -const DOT_COUNT = 6; -const DOT_INDICES = Array.from({ length: DOT_COUNT }, (_, i) => i); -const STEP_MS = SYNCED_LOADER_DURATION_MS / DOT_COUNT; // 158.333…ms per step - -interface SyncedLoaderDotProps { - dotIndex: number; - dotSize: number; - gap: number; -} - -function SyncedLoaderDot({ dotIndex, dotSize, gap }: SyncedLoaderDotProps) { - const col = dotIndex % 2; - const row = Math.floor(dotIndex / 2); - const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]); - const delayMs = -(sequenceIndex * STEP_MS); - const style = React.useMemo( - () => ({ - position: "absolute" as const, - left: col * (dotSize + gap), - top: row * (dotSize + gap), - width: dotSize, - height: dotSize, - borderRadius: "50%", - backgroundColor: "#f59e0b", - animationName: "synced-snake-dot", - animationDuration: `${SYNCED_LOADER_DURATION_MS}ms`, - animationTimingFunction: "linear", - animationIterationCount: "infinite", - animationDelay: `${delayMs}ms`, - }), - [col, row, dotSize, gap, delayMs], - ); - return
; -} - -function SyncedLoader({ size = 11 }: { size?: number }) { - const gap = Math.max(1, Math.round(size * 0.12)); - const dotSize = Math.max(2, Math.floor((size - gap * 2) / 3)); - const gridW = dotSize * 2 + gap; - const gridH = dotSize * 3 + gap * 2; - - const outerStyle = React.useMemo( - () => ({ - width: size, - height: size, - display: "flex", - alignItems: "center", - justifyContent: "center", - flexShrink: 0, - }), - [size], - ); - - const innerStyle = React.useMemo( - () => ({ position: "relative" as const, width: gridW, height: gridH }), - [gridW, gridH], - ); - - return ( -
-
- {DOT_INDICES.map((dotIndex) => ( - - ))} -
-
- ); -} - -function WorkspaceStatusDot({ status }: { status: SidebarWorkspace["status"] }) { - if (status === "running") return
; - if (status === "done") return
; - return null; -} - -function Sidebar() { - // Shared left edge: pl-3 (12px) for traffic lights, icons, footer dot - // Workspace indent: pl-7 (28px) - // PR badge aligns with workspace text: pl-[42px] - return ( -
- {/* Traffic lights */} -
- -
- - {/* Sessions header */} -
- - Sessions -
- - {/* Project list */} -
- {SIDEBAR_PROJECTS.map((project) => ( -
- {/* Project row — icon aligns with traffic lights / sessions */} -
-
- - {project.initial} - -
- - {project.name} - -
- - {/* Workspace rows — indented one level */} - {project.workspaces.map((workspace) => ( - - ))} -
- ))} -
- - {/* Footer — dot aligns with traffic lights / icons */} -
-
-
- MacBook Pro -
-
-
- -
-
- -
-
-
-
- ); -} - -const TERMINAL_TABS: TabDef[] = [ - { name: "npm run dev", provider: "terminal", done: false, active: true }, -]; - -function TitleBarLeft() { - return ( -
-
- -
-
- main - - acme/returns-app - -
- -
-
-
- ); -} - -function TitleBarRight() { - return ( -
-
-
- - Commit -
-
- -
-
-
- - +247 - -15 -
-
- ); -} - -function DesktopSplitPanes() { - return ( -
- {/* Left pane: all agent tabs + chat */} -
- - - - - - -
- - {/* Resize handle */} -
- - {/* Right pane: terminal only */} - - - - -
- ); -} - -// ── Desktop Mockup ────────────────────────────────────── - -function DesktopMockup() { - const containerRef = React.useRef(null); - const [scale, setScale] = React.useState(1); - - React.useEffect(() => { - function updateScale() { - if (!containerRef.current) return; - const parentWidth = containerRef.current.parentElement?.clientWidth ?? 1200; - const designWidth = 1200; - setScale(Math.min(1, parentWidth / designWidth)); - } - updateScale(); - window.addEventListener("resize", updateScale); - return () => window.removeEventListener("resize", updateScale); - }, []); - - const containerStyle = React.useMemo(() => ({ height: `${1200 * (9 / 16) * scale}px` }), [scale]); - const innerStyle = React.useMemo(() => ({ width: 1200, transform: `scale(${scale})` }), [scale]); - - return ( -
-
- {/* Top-level: left sidebar | center column | explorer sidebar — all full height */} -
- {/* Left sidebar — full height */} - - - - - {/* Center column: title bar + split panes */} -
- {/* Title bar — belongs to center column only */} - - - - - - -
- - {/* Explorer sidebar — full height, pushes center column */} - - - -
-
-
- ); -} - -// ── Export ─────────────────────────────────────────────── - export function HeroMockup() { - return ; + return ( +
+ Paseo desktop app with coding agents, a conversation, and a code diff open side by side +
+ ); }