From fa031f0e5d596842323fea65bfdbc7049861a04c Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 15:25:40 -0800 Subject: [PATCH 01/11] App hero --- .../components/HeroSection/HeroSection.tsx | 4 +- .../components/AppMockup/AppMockup.tsx | 637 ++++++++++++++++++ .../HeroSection/components/AppMockup/index.ts | 1 + 3 files changed, 640 insertions(+), 2 deletions(-) create mode 100644 apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx create mode 100644 apps/marketing/src/app/components/HeroSection/components/AppMockup/index.ts diff --git a/apps/marketing/src/app/components/HeroSection/HeroSection.tsx b/apps/marketing/src/app/components/HeroSection/HeroSection.tsx index 6de8a1010b0..c163bae3dbb 100644 --- a/apps/marketing/src/app/components/HeroSection/HeroSection.tsx +++ b/apps/marketing/src/app/components/HeroSection/HeroSection.tsx @@ -6,8 +6,8 @@ import { useState } from "react"; import { FaGithub } from "react-icons/fa"; import { DownloadButton } from "../DownloadButton"; import { WaitlistModal } from "../WaitlistModal"; +import { AppMockup } from "./components/AppMockup"; import { GridBackground } from "./components/GridBackground"; -import { ProductDemo } from "./components/ProductDemo"; import { TypewriterText } from "./components/TypewriterText"; export function HeroSection() { @@ -64,7 +64,7 @@ export function HeroSection() { animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.5, delay: 0.2 }} > - + diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx new file mode 100644 index 00000000000..a8066def862 --- /dev/null +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -0,0 +1,637 @@ +"use client"; + +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; +import { + LuChevronDown, + LuFile, + LuFilePlus, + LuFolder, + LuFolderGit2, + LuGitBranch, + LuGitPullRequest, + LuLayers, + LuPanelLeft, + LuPencil, + LuPlus, + LuRefreshCw, + LuX, +} from "react-icons/lu"; + +const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + +function AsciiSpinner({ className }: { className?: string }) { + const [frameIndex, setFrameIndex] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setFrameIndex((prev) => (prev + 1) % SPINNER_FRAMES.length); + }, 80); + return () => clearInterval(interval); + }, []); + + return ( + + {SPINNER_FRAMES[frameIndex]} + + ); +} + +function StatusIndicator({ + status, +}: { + status: "permission" | "working" | "review"; +}) { + const config = { + permission: { ping: "bg-red-400", dot: "bg-red-500", pulse: true }, + working: { ping: "bg-amber-400", dot: "bg-amber-500", pulse: true }, + review: { ping: "", dot: "bg-green-500", pulse: false }, + }[status]; + + return ( + + {config.pulse && ( + + )} + + + ); +} + +const WORKSPACES = [ + { name: "main", branch: "main", isMain: true }, + { + name: "autoupdate toast", + branch: "autoupdate-toast", + add: 46, + del: 1, + pr: "#733", + }, + { + name: "brew install", + branch: "brew-install", + add: 193, + del: 0, + pr: "#815", + }, + { + name: "fix option key", + branch: "fix-option-key", + add: 394, + del: 23, + pr: "#884", + status: "review" as const, + }, + { + name: "ui blocking threads", + branch: "ui-blocking-threads", + add: 33, + del: 0, + pr: "#816", + }, + { + name: "cloud ws", + branch: "cloud-ws", + add: 5, + del: 0, + pr: "#827", + isActive: true, + status: "working" as const, + }, + { + name: "terminal bugs 2", + branch: "terminal-bugs-2", + add: 233, + del: 188, + pr: "#905", + status: "permission" as const, + }, + { name: "feature parity", branch: "feature-parity", add: 207, del: 1 }, + { + name: "allow binding hotkeys t...", + branch: "allow-binding-hotkeys-to-preset", + add: 461, + del: 1, + }, + { name: "hero component", branch: "hero-component", add: 299, del: 2 }, + { name: "terminal bug 3", branch: "terminal-bug-3", add: 37, del: 0 }, +]; + +const FILE_CHANGES = [ + { path: "bun.lock", add: 38, del: 25, indent: 0, type: "edit" }, + { + path: "apps/api/src/app/api/electric/[...path]", + add: 1, + del: 0, + indent: 0, + type: "folder", + }, + { path: "utils.ts", add: 21, del: 4, indent: 1, type: "edit" }, + { path: "route.ts", add: 1, del: 1, indent: 1, type: "edit" }, + { + path: "apps/desktop/src/lib/trpc/routers", + add: 0, + del: 0, + indent: 0, + type: "folder", + }, + { path: "index.ts", add: 2, del: 0, indent: 1, type: "add" }, + { path: "cloud-terminal", add: 0, del: 0, indent: 0, type: "folder" }, + { path: "index.ts", add: 178, del: 0, indent: 1, type: "add" }, + { path: "ssh-terminal", add: 0, del: 0, indent: 0, type: "folder" }, + { path: "index.ts", add: 7, del: 0, indent: 1, type: "add" }, + { path: "ssh-manager.ts", add: 277, del: 0, indent: 1, type: "add" }, + { path: "NewCloudWorkspaceModal", add: 0, del: 0, indent: 0, type: "folder" }, + { path: "index.ts", add: 4, del: 0, indent: 1, type: "add" }, + { + path: "NewCloudWorkspaceModal...", + add: 239, + del: 0, + indent: 1, + type: "add", + }, + { + path: "useCloudWorkspaceMutati...", + add: 121, + del: 0, + indent: 1, + type: "add", + }, + { path: "useCloudWorkspaces.ts", add: 84, del: 0, indent: 1, type: "add" }, + { path: "layout.tsx", add: 2, del: 0, indent: 1, type: "edit" }, + { path: "collections.ts", add: 51, del: 17, indent: 1, type: "edit" }, + { path: "WorkspaceSidebar.tsx", add: 14, del: 0, indent: 1, type: "edit" }, +]; + +const PORTS = [ + { workspace: "hero component", ports: ["3002"] }, + { + workspace: "terminal bug 3", + ports: ["3000", "3001", "5678", "5927", "31416"], + }, +]; + +function WorkspaceItem({ + name, + branch, + add, + del, + pr, + isActive, + isMain, + status, +}: { + name: string; + branch: string; + add?: number; + del?: number; + pr?: string; + isActive?: boolean; + isMain?: boolean; + status?: "permission" | "working" | "review"; +}) { + return ( +
+ {isActive && ( +
+ )} +
+ {status === "working" ? ( + + ) : isMain ? ( + + ) : ( + + )} + {status && status !== "working" && ( + + + + )} +
+
+
+ + {name} + + {(add !== undefined || pr) && ( +
+ {add !== undefined && ( + + +{add} + {del !== undefined && del > 0 && ( + -{del} + )} + + )} +
+ )} +
+
+ + {branch} + + {pr && ( + + + {pr} + + )} +
+
+
+ ); +} + +function FileChangeItem({ + path, + add, + del, + indent, + type, +}: { + path: string; + add: number; + del: number; + indent: number; + type: string; +}) { + const Icon = + type === "folder" + ? LuFolder + : type === "add" + ? LuFilePlus + : type === "edit" + ? LuPencil + : LuFile; + const iconColor = + type === "add" + ? "text-emerald-400" + : type === "edit" + ? "text-amber-400" + : "text-muted-foreground/50"; + + return ( +
+
+ + {path} +
+ {type !== "folder" && (add > 0 || del > 0) && ( + + {add > 0 && +{add}} + {del > 0 && -{del}} + + )} +
+ ); +} + +export function AppMockup() { + return ( + + {/* Window chrome */} +
+
+
+
+
+
+
+ + + /cloud-ws + + Open + + KO + Kiet's Org +
+
+ Base: + main + (default) + +
+
+ +
+ {/* Left sidebar */} +
+ {/* Workspaces header */} +
+
+ + +
+
+ + {/* New Workspace button */} +
+ +
+ + {/* Repository section */} +
+
+ ⟨:⟩ + superset + (11) +
+
+ + +
+
+ + {/* Workspace list */} +
+ {WORKSPACES.map((ws) => ( + + ))} +
+ + {/* Ports section */} +
+
+
+ + Ports +
+ 6 +
+ {PORTS.map((port) => ( +
+
+ + {port.workspace} + + +
+
+ {port.ports.map((p) => ( + + {p} + + ))} +
+
+ ))} +
+ + {/* Add repository */} +
+ +
+
+ + {/* Main content area */} +
+ {/* Tab bar */} +
+
+ claude + +
+
+ Terminal 1 + +
+
+ mcp + +
+
+ + +
+
+ + {/* Terminal header */} +
+ + + Terminal + +
+ + +
+ + {/* Terminal content */} +
+ {/* Claude ASCII art header */} +
+
+ {` * ▐▛███▜▌ * + * ▝▜█████▛▘ * + * ▘▘ ▝▝ *`} +
+
+
+ + Claude Code + {" "} + v2.0.74 +
+
Opus 4.5 · Claude Max
+
+ ~/.superset/worktrees/superset/cloud-ws +
+
+
+ + {/* Command prompt */} +
+ {" "} + /mcp +
+ + {/* MCP output */} +
+
+ + Manage MCP servers + +
+
1 server
+ +
+ + 1. + morph-mcp + ✓ connected + + · Enter to view details + +
+ +
+
MCP Config locations (by scope):
+
+ • User config (available in all your projects): +
+
+ · /Users/kietho/.claude.json +
+
+ • Project config (shared via .mcp.json): +
+
+ · + /Users/kietho/.superset/worktrees/superset/cloud-ws/.mcp.json +
+
+ • Local config (private to you in this project): +
+
+ · /Users/kietho/.claude.json [project: ...] +
+
+ +
+
+ Tip: Use /mcp enable or /mcp disable to quickly toggle all + servers +
+
+ +
+ For help configuring MCP servers, see:{" "} + + https://code.claude.com/docs/en/mcp + +
+ +
+ Enter to confirm · Esc to cancel +
+
+
+
+ + {/* Right sidebar */} +
+ {/* Header icons */} +
+
+ + + +
+
+ + #827 +
+
+ + {/* Commit message section */} +
+
+ Commit message +
+
+
+ + {/* Push button */} +
+ +
+ + {/* Against main */} +
+
+
+ + Against main +
+ 46 +
+
+ + {/* Root Path */} +
+ + Root Path + +
+ + {/* File changes list */} +
+ {FILE_CHANGES.map((file, i) => ( + + ))} +
+
+
+ + ); +} diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/index.ts b/apps/marketing/src/app/components/HeroSection/components/AppMockup/index.ts new file mode 100644 index 00000000000..54c3b5c9be1 --- /dev/null +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/index.ts @@ -0,0 +1 @@ +export { AppMockup } from "./AppMockup"; From 9ad09aa77b4b74f050fd38464ac1fb455fae2644 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 15:33:15 -0800 Subject: [PATCH 02/11] feat(marketing): replace hero videos with app mockup and add hover interactions Replace video-based demos with static AppMockup component and change selector pills to trigger on hover instead of click, preparing for animated demo states. --- .../components/HeroSection/HeroSection.tsx | 4 +- .../components/AppMockup/AppMockup.tsx | 106 ++++++++---------- .../HeroSection/components/AppMockup/index.ts | 2 +- .../components/ProductDemo/ProductDemo.tsx | 30 +---- .../components/SelectorPill/SelectorPill.tsx | 6 +- 5 files changed, 59 insertions(+), 89 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/HeroSection.tsx b/apps/marketing/src/app/components/HeroSection/HeroSection.tsx index c163bae3dbb..6de8a1010b0 100644 --- a/apps/marketing/src/app/components/HeroSection/HeroSection.tsx +++ b/apps/marketing/src/app/components/HeroSection/HeroSection.tsx @@ -6,8 +6,8 @@ import { useState } from "react"; import { FaGithub } from "react-icons/fa"; import { DownloadButton } from "../DownloadButton"; import { WaitlistModal } from "../WaitlistModal"; -import { AppMockup } from "./components/AppMockup"; import { GridBackground } from "./components/GridBackground"; +import { ProductDemo } from "./components/ProductDemo"; import { TypewriterText } from "./components/TypewriterText"; export function HeroSection() { @@ -64,7 +64,7 @@ export function HeroSection() { animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.5, delay: 0.2 }} > - +
diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index a8066def862..a7a188ee919 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -122,49 +122,17 @@ const WORKSPACES = [ ]; const FILE_CHANGES = [ - { path: "bun.lock", add: 38, del: 25, indent: 0, type: "edit" }, - { - path: "apps/api/src/app/api/electric/[...path]", - add: 1, - del: 0, - indent: 0, - type: "folder", - }, - { path: "utils.ts", add: 21, del: 4, indent: 1, type: "edit" }, - { path: "route.ts", add: 1, del: 1, indent: 1, type: "edit" }, - { - path: "apps/desktop/src/lib/trpc/routers", - add: 0, - del: 0, - indent: 0, - type: "folder", - }, - { path: "index.ts", add: 2, del: 0, indent: 1, type: "add" }, - { path: "cloud-terminal", add: 0, del: 0, indent: 0, type: "folder" }, - { path: "index.ts", add: 178, del: 0, indent: 1, type: "add" }, - { path: "ssh-terminal", add: 0, del: 0, indent: 0, type: "folder" }, - { path: "index.ts", add: 7, del: 0, indent: 1, type: "add" }, - { path: "ssh-manager.ts", add: 277, del: 0, indent: 1, type: "add" }, - { path: "NewCloudWorkspaceModal", add: 0, del: 0, indent: 0, type: "folder" }, - { path: "index.ts", add: 4, del: 0, indent: 1, type: "add" }, - { - path: "NewCloudWorkspaceModal...", - add: 239, - del: 0, - indent: 1, - type: "add", - }, - { - path: "useCloudWorkspaceMutati...", - add: 121, - del: 0, - indent: 1, - type: "add", - }, - { path: "useCloudWorkspaces.ts", add: 84, del: 0, indent: 1, type: "add" }, - { path: "layout.tsx", add: 2, del: 0, indent: 1, type: "edit" }, - { path: "collections.ts", add: 51, del: 17, indent: 1, type: "edit" }, - { path: "WorkspaceSidebar.tsx", add: 14, del: 0, indent: 1, type: "edit" }, + { path: "bun.lock", add: 38, del: 25, type: "edit" }, + { path: "packages/db/src/schema", type: "folder" }, + { path: "cloud-workspace.ts", add: 119, del: 0, type: "add", indent: 1 }, + { path: "enums.ts", add: 21, del: 0, type: "edit", indent: 1 }, + { path: "apps/desktop/src/renderer", type: "folder" }, + { path: "CloudTerminal.tsx", add: 169, del: 0, type: "add", indent: 1 }, + { path: "useCloudWorkspaces.ts", add: 84, del: 0, type: "add", indent: 1 }, + { path: "WorkspaceSidebar.tsx", add: 14, del: 0, type: "edit", indent: 1 }, + { path: "apps/api/src/trpc/routers", type: "folder" }, + { path: "ssh-manager.ts", add: 277, del: 0, type: "add", indent: 1 }, + { path: "index.ts", add: 7, del: 0, type: "edit", indent: 1 }, ]; const PORTS = [ @@ -253,15 +221,15 @@ function WorkspaceItem({ function FileChangeItem({ path, - add, - del, - indent, + add = 0, + del = 0, + indent = 0, type, }: { path: string; - add: number; - del: number; - indent: number; + add?: number; + del?: number; + indent?: number; type: string; }) { const Icon = @@ -279,26 +247,48 @@ function FileChangeItem({ ? "text-amber-400" : "text-muted-foreground/50"; + const isFolder = type === "folder"; + return (
-
- - {path} +
+ + + {path} +
- {type !== "folder" && (add > 0 || del > 0) && ( - + {!isFolder && (add > 0 || del > 0) && ( + {add > 0 && +{add}} - {del > 0 && -{del}} + {del > 0 && -{del}} )}
); } -export function AppMockup() { +export type ActiveDemo = + | "Use Any Agents" + | "Create Parallel Branches" + | "See Changes" + | "Open in Any IDE"; + +interface AppMockupProps { + activeDemo?: ActiveDemo; +} + +export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { + // Animation states based on activeDemo + // - "Use Any Agents": Highlight terminal area with agent output + // - "Create Parallel Branches": Highlight workspace sidebar with branches + // - "See Changes": Highlight file changes panel on right + // - "Open in Any IDE": Highlight top bar / open actions + return ( ( - DEMO_OPTIONS[0]?.label ?? "", - ); + const [activeOption, setActiveOption] = useState("Use Any Agents"); return (
@@ -32,26 +30,8 @@ export function ProductDemo() { {/* Content wrapper */}
- {/* Video container with border */} -
- {DEMO_OPTIONS.map((option) => ( - - - - ))} -
+ {/* App mockup container */} + {/* Selector pills */}
@@ -60,7 +40,7 @@ export function ProductDemo() { key={option.label} label={option.label} active={activeOption === option.label} - onClick={() => setActiveOption(option.label)} + onHover={() => setActiveOption(option.label as ActiveDemo)} /> ))}
diff --git a/apps/marketing/src/app/components/HeroSection/components/ProductDemo/components/SelectorPill/SelectorPill.tsx b/apps/marketing/src/app/components/HeroSection/components/ProductDemo/components/SelectorPill/SelectorPill.tsx index 3f4c783350b..401ab94348c 100644 --- a/apps/marketing/src/app/components/HeroSection/components/ProductDemo/components/SelectorPill/SelectorPill.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/ProductDemo/components/SelectorPill/SelectorPill.tsx @@ -5,18 +5,18 @@ import { motion } from "framer-motion"; interface SelectorPillProps { label: string; active?: boolean; - onClick?: () => void; + onHover?: () => void; } export function SelectorPill({ label, active = false, - onClick, + onHover, }: SelectorPillProps) { return ( Date: Thu, 22 Jan 2026 15:59:15 -0800 Subject: [PATCH 03/11] Animate --- .../components/AppMockup/AppMockup.tsx | 220 +++++++++--------- 1 file changed, 116 insertions(+), 104 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index a7a188ee919..8a852ab25d8 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -1,6 +1,7 @@ "use client"; import { motion } from "framer-motion"; +import Image from "next/image"; import { useEffect, useState } from "react"; import { LuChevronDown, @@ -10,8 +11,6 @@ import { LuFolderGit2, LuGitBranch, LuGitPullRequest, - LuLayers, - LuPanelLeft, LuPencil, LuPlus, LuRefreshCw, @@ -63,62 +62,45 @@ function StatusIndicator({ } const WORKSPACES = [ - { name: "main", branch: "main", isMain: true }, { - name: "autoupdate toast", - branch: "autoupdate-toast", + name: "use any agents", + branch: "use-any-agents", add: 46, del: 1, pr: "#733", + isActive: true, + status: "working" as const, }, { - name: "brew install", - branch: "brew-install", + name: "create parallel branches", + branch: "create-parallel-branches", add: 193, del: 0, pr: "#815", + status: "review" as const, }, { - name: "fix option key", - branch: "fix-option-key", + name: "see changes", + branch: "see-changes", add: 394, del: 23, pr: "#884", - status: "review" as const, }, { - name: "ui blocking threads", - branch: "ui-blocking-threads", + name: "open in any IDE", + branch: "open-in-any-ide", add: 33, del: 0, pr: "#816", - }, - { - name: "cloud ws", - branch: "cloud-ws", - add: 5, - del: 0, - pr: "#827", - isActive: true, - status: "working" as const, - }, - { - name: "terminal bugs 2", - branch: "terminal-bugs-2", - add: 233, - del: 188, - pr: "#905", status: "permission" as const, }, - { name: "feature parity", branch: "feature-parity", add: 207, del: 1 }, { - name: "allow binding hotkeys t...", - branch: "allow-binding-hotkeys-to-preset", - add: 461, - del: 1, + name: "forward ports", + branch: "forward-ports", + add: 127, + del: 8, + pr: "#902", }, - { name: "hero component", branch: "hero-component", add: 299, del: 2 }, - { name: "terminal bug 3", branch: "terminal-bug-3", add: 37, del: 0 }, ]; const FILE_CHANGES = [ @@ -136,10 +118,10 @@ const FILE_CHANGES = [ ]; const PORTS = [ - { workspace: "hero component", ports: ["3002"] }, + { workspace: "use any agents", ports: ["3002"] }, { - workspace: "terminal bug 3", - ports: ["3000", "3001", "5678", "5927", "31416"], + workspace: "see changes", + ports: ["3000", "3001", "5678"], }, ]; @@ -150,7 +132,6 @@ function WorkspaceItem({ del, pr, isActive, - isMain, status, }: { name: string; @@ -159,7 +140,6 @@ function WorkspaceItem({ del?: number; pr?: string; isActive?: boolean; - isMain?: boolean; status?: "permission" | "working" | "review"; }) { return ( @@ -172,8 +152,6 @@ function WorkspaceItem({
{status === "working" ? ( - ) : isMain ? ( - ) : ( )} @@ -311,8 +289,6 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { Open - KO - Kiet's Org
Base: @@ -325,25 +301,6 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Left sidebar */}
- {/* Workspaces header */} -
-
- - -
-
- {/* New Workspace button */}
{PORTS.map((port) => (
@@ -432,18 +387,38 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Tab bar */}
-
+ {/* Claude tab - always visible, active */} +
+ Claude claude
-
- Terminal 1 - -
-
- mcp - -
+ {/* Other agent tabs - shown when "Use Any Agents" is active */} + +
+ Codex + codex + +
+
+ Gemini + gemini + +
+
+ Cursor + cursor + +
+
@@ -568,16 +543,11 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
- {/* Commit message section */} -
-
- Commit message + {/* Commit & Push section */} +
+
+ Commit message...
-
-
- - {/* Push button */} -
- {/* Against main */} -
-
-
- - Against main -
- 46 -
-
- - {/* Root Path */} -
- - Root Path - -
- {/* File changes list */}
{FILE_CHANGES.map((file, i) => ( @@ -622,6 +573,67 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
+ + {/* External IDE Popup - shown when "Open in Any IDE" is active */} + + {/* IDE window chrome */} +
+
+
+
+
+
+ External IDE +
+
+ +
+ {/* File tree */} +
+
+ + src +
+
+
+ + index.ts +
+
+ + utils.ts +
+
+ + types.ts +
+
+
+ + {/* Code editor */} +
+
+
import {"{"} Agent {"}"} from "ai"
+
import {"{"} tools {"}"} from "./utils"
+
+
const agent = new Agent({"{"}
+
model: "claude-4",
+
tools: [tools.read, tools.write]
+
{"}"})
+
+
+
+ ); } From 8e2991dd33685a62a6534c2ce87264369f749caa Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 16:00:18 -0800 Subject: [PATCH 04/11] Animate --- .../components/AppMockup/AppMockup.tsx | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index 8a852ab25d8..8f83dbe180b 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -395,29 +395,43 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Other agent tabs - shown when "Use Any Agents" is active */} -
- Codex - codex - -
-
- Gemini - gemini - -
-
- Cursor - cursor - -
+ Codex + codex + +
+ + Gemini + gemini + + + + Cursor + cursor +
From 6a2ba57c3be27d724c74c56b26b03d3621a2eab2 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 16:03:32 -0800 Subject: [PATCH 05/11] Changes --- .../components/AppMockup/AppMockup.tsx | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index 8f83dbe180b..7d6d234c3aa 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -9,11 +9,9 @@ import { LuFilePlus, LuFolder, LuFolderGit2, - LuGitBranch, LuGitPullRequest, LuPencil, LuPlus, - LuRefreshCw, LuX, } from "react-icons/lu"; @@ -282,20 +280,8 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
-
- - - /cloud-ws - - Open - -
-
- Base: - main - (default) - -
+ superset +
@@ -371,16 +357,6 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { ))}
- {/* Add repository */} -
- -
{/* Main content area */} @@ -544,13 +520,9 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {/* Right sidebar */}
- {/* Header icons */} + {/* Header */}
-
- - - -
+ Review Changes
#827 From a1e26f78ff0f53d3ea304be74b356742e360db4c Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 16:11:45 -0800 Subject: [PATCH 06/11] Parallel --- .../components/AppMockup/AppMockup.tsx | 110 +++++++++++++++--- 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index 7d6d234c3aa..32e6298168e 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -12,6 +12,7 @@ import { LuGitPullRequest, LuPencil, LuPlus, + LuTerminal, LuX, } from "react-icons/lu"; @@ -312,18 +313,46 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {/* Workspace list */}
- {WORKSPACES.map((ws) => ( - - ))} + {/* New workspace - shown when "Create Parallel Branches" is active */} + +
+
+ +
+
+
+ new workspace +
+ + creating... + +
+
+
+ {WORKSPACES.map((ws) => { + const isFirstItem = ws.name === "use any agents"; + const shouldHideActiveState = isFirstItem && activeDemo === "Create Parallel Branches"; + return ( + + ); + })}
{/* Ports section */} @@ -365,8 +394,17 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Claude tab - always visible, active */}
- Claude - claude + {activeDemo === "Create Parallel Branches" ? ( + <> + + setup + + ) : ( + <> + Claude + claude + + )}
{/* Other agent tabs - shown when "Use Any Agents" is active */} @@ -427,7 +465,7 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Terminal content */} -
+
{/* Claude ASCII art header */}
@@ -515,6 +553,37 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { Enter to confirm · Esc to cancel
+ + {/* Create Parallel Branches overlay */} + +
+ {" "} + superset new +
+
+
+ + Setting up new parallel environment... +
+
+ → Creating worktree from main +
+
+ → Installing dependencies +
+
+ → Configuring environment +
+
+
@@ -545,7 +614,14 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* File changes list */} -
+ {FILE_CHANGES.map((file, i) => ( ))} -
+
From 9f618136d49b4edb162e72ed302c5530319e9956 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 16:12:28 -0800 Subject: [PATCH 07/11] Padding --- .../components/AppMockup/AppMockup.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index 32e6298168e..e4df10a0496 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -409,11 +409,13 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Other agent tabs - shown when "Use Any Agents" is active */} @@ -422,11 +424,13 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { @@ -435,11 +439,13 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { From da0d7bacbb428ca335e8ba1e000bb8d73ea0dea7 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 16:29:15 -0800 Subject: [PATCH 08/11] feat(marketing): add See Changes diff review animation to hero mockup Expand right sidebar and show GitHub-style PR diff review when See Changes demo option is hovered. --- .../components/AppMockup/AppMockup.tsx | 162 ++++++++++++++---- 1 file changed, 126 insertions(+), 36 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index e4df10a0496..6cf6f24be35 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -594,52 +594,142 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Right sidebar */} -
- {/* Header */} -
- Review Changes -
- - #827 + + {/* Default view - Header, Commit & Push, File changes */} + + {/* Header */} +
+ Review Changes +
+ + #827 +
-
- {/* Commit & Push section */} -
-
- Commit message... + {/* Commit & Push section */} +
+
+ Commit message... +
+
- -
+ {FILE_CHANGES.map((file, i) => ( + + ))} + + - {/* File changes list */} + {/* Diff review view - shown when "See Changes" is active */} - {FILE_CHANGES.map((file, i) => ( - - ))} + {/* PR Header */} +
+
+ + Review PR #827 +
+ Open +
+ + {/* File tabs */} +
+ cloud-workspace.ts + enums.ts + +4 more +
+ + {/* Diff content */} +
+
+
@@ -1,4 +1,6 @@
+
+ 1 + import {"{"} db {"}"} from "../db" +
+
+ + + import {"{"} CloudWorkspace {"}"} from "./types" +
+
+ + + import {"{"} createSSHConnection {"}"} from "./ssh" +
+
+ 2 + +
+
+ - + export const getWorkspaces = () ={">"} {"{"} +
+
+ + + export const getWorkspaces = async () ={">"} {"{"} +
+
+ 4 + {" "}return db.query.workspaces +
+
+
+ + {/* Review actions */} +
+ + +
-
+
{/* External IDE Popup - shown when "Open in Any IDE" is active */} From 6d2f0a05645d719ba86e28a21959337f14c9b62a Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 16:43:44 -0800 Subject: [PATCH 09/11] Make it glass --- .../components/AppMockup/AppMockup.tsx | 133 ++++++++++-------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index 6cf6f24be35..93dc9062016 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -268,14 +268,14 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { return ( {/* Window chrome */} -
+
@@ -287,12 +287,12 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Left sidebar */} -
+
{/* New Workspace button */} -
+
{/* Repository section */} -
+
superset (5) @@ -356,27 +356,27 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Ports section */} -
+
-
+
Ports
- 4 + 4
{PORTS.map((port) => (
- + {port.workspace} - +
{port.ports.map((p) => ( {p} @@ -391,9 +391,9 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {/* Main content area */}
{/* Tab bar */} -
+
{/* Claude tab - always visible, active */} -
+
{activeDemo === "Create Parallel Branches" ? ( <> @@ -460,18 +460,26 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Terminal header */} -
- - +
+ + Terminal
- - + +
{/* Terminal content */} -
+
+ {/* Default terminal content */} + {/* Claude ASCII art header */}
@@ -500,7 +508,7 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* MCP output */} -
+
Manage MCP servers @@ -559,10 +567,11 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { Enter to confirm · Esc to cancel
+ {/* Create Parallel Branches overlay */} {/* Header */} -
- Review Changes +
+ Review Changes
- - #827 + + #827
{/* Commit & Push section */} -
-
+
+
Commit message...
@@ -660,7 +669,7 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {/* Diff review view - shown when "See Changes" is active */} {/* PR Header */} -
+
- - Review PR #827 + + Review PR #827
- Open + Open
{/* File tabs */} -
- cloud-workspace.ts - enums.ts - +4 more +
+ cloud-workspace.ts + enums.ts + +4 more
{/* Diff content */}
-
@@ -1,4 +1,6 @@
+
@@ -1,4 +1,6 @@
- 1 - import {"{"} db {"}"} from "../db" + 1 + import {"{"} db {"}"} from "../db"
-
- + - import {"{"} CloudWorkspace {"}"} from "./types" +
+ + + import {"{"} CloudWorkspace {"}"} from "./types"
-
- + - import {"{"} createSSHConnection {"}"} from "./ssh" +
+ + + import {"{"} createSSHConnection {"}"} from "./ssh"
- 2 - + 2 +
-
- - - export const getWorkspaces = () ={">"} {"{"} +
+ - + export const getWorkspaces = () ={">"} {"{"}
-
- + - export const getWorkspaces = async () ={">"} {"{"} +
+ + + export const getWorkspaces = async () ={">"} {"{"}
- 4 - {" "}return db.query.workspaces + 4 + {" "}return db.query.workspaces
{/* Review actions */} -
- -
From 6ca88fea045f39713fab75731487f2d56e3bb935 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 21:04:43 -0800 Subject: [PATCH 10/11] Glassy --- .../components/AppMockup/AppMockup.tsx | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index 93dc9062016..714bfdc8334 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -571,7 +571,7 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {/* Create Parallel Branches overlay */} ❯{" "} superset new
-
+
- + Setting up new parallel environment...
-
+
→ Creating worktree from main
-
+
→ Installing dependencies
-
+
→ Configuring environment
@@ -743,7 +743,7 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {/* External IDE Popup - shown when "Open in Any IDE" is active */} {/* IDE window chrome */} -
+
-
-
-
+
+
+
- External IDE + External IDE
{/* File tree */} -
-
+
+
src
-
+
index.ts
-
+
utils.ts
-
+
types.ts
@@ -788,14 +788,14 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Code editor */} -
+
-
import {"{"} Agent {"}"} from "ai"
-
import {"{"} tools {"}"} from "./utils"
-
-
const agent = new Agent({"{"}
-
model: "claude-4",
-
tools: [tools.read, tools.write]
+
import {"{"} Agent {"}"} from "ai"
+
import {"{"} tools {"}"} from "./utils"
+
+
const agent = new Agent({"{"}
+
model: "claude-4",
+
tools: [tools.read, tools.write]
{"}"})
From 62bb8d9c20f69087b8ee09a698bb161311456185 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 21:34:29 -0800 Subject: [PATCH 11/11] Subtle wave bg --- .../components/HeroSection/HeroSection.tsx | 7 +- apps/marketing/src/components/ui/index.ts | 2 + .../src/components/ui/shader-animation.tsx | 172 ++++++++++ .../src/components/ui/wave-background.tsx | 314 ++++++++++++++++++ 4 files changed, 492 insertions(+), 3 deletions(-) create mode 100644 apps/marketing/src/components/ui/index.ts create mode 100644 apps/marketing/src/components/ui/shader-animation.tsx create mode 100644 apps/marketing/src/components/ui/wave-background.tsx diff --git a/apps/marketing/src/app/components/HeroSection/HeroSection.tsx b/apps/marketing/src/app/components/HeroSection/HeroSection.tsx index 6de8a1010b0..d570912b3f5 100644 --- a/apps/marketing/src/app/components/HeroSection/HeroSection.tsx +++ b/apps/marketing/src/app/components/HeroSection/HeroSection.tsx @@ -4,9 +4,9 @@ import { COMPANY } from "@superset/shared/constants"; import { motion } from "framer-motion"; import { useState } from "react"; import { FaGithub } from "react-icons/fa"; +import { ShaderAnimation } from "../../../components/ui/shader-animation"; import { DownloadButton } from "../DownloadButton"; import { WaitlistModal } from "../WaitlistModal"; -import { GridBackground } from "./components/GridBackground"; import { ProductDemo } from "./components/ProductDemo"; import { TypewriterText } from "./components/TypewriterText"; @@ -16,7 +16,7 @@ export function HeroSection() { return (
- +
@@ -38,7 +38,8 @@ export function HeroSection() { />

- Run dozens of Claude Code, Codex, or any other in parallel. + Orchestrate a team of Claude Code, Codex, or any other coding + agents

diff --git a/apps/marketing/src/components/ui/index.ts b/apps/marketing/src/components/ui/index.ts new file mode 100644 index 00000000000..d146457b106 --- /dev/null +++ b/apps/marketing/src/components/ui/index.ts @@ -0,0 +1,2 @@ +export { ShaderAnimation } from "./shader-animation" +export { Waves } from "./wave-background" diff --git a/apps/marketing/src/components/ui/shader-animation.tsx b/apps/marketing/src/components/ui/shader-animation.tsx new file mode 100644 index 00000000000..5ee37a60d9d --- /dev/null +++ b/apps/marketing/src/components/ui/shader-animation.tsx @@ -0,0 +1,172 @@ +"use client" + +import { useEffect, useRef } from "react" +import * as THREE from "three" + +interface ShaderAnimationProps { + className?: string + opacity?: number + speed?: number + intensity?: number +} + +export function ShaderAnimation({ + className = "", + opacity = 0.15, + speed = 0.008, + intensity = 0.0003, +}: ShaderAnimationProps) { + const containerRef = useRef(null) + const sceneRef = useRef<{ + camera: THREE.Camera + scene: THREE.Scene + renderer: THREE.WebGLRenderer + uniforms: { + time: { type: string; value: number } + resolution: { type: string; value: THREE.Vector2 } + intensity: { type: string; value: number } + } + animationId: number + startTime: number + } | null>(null) + + useEffect(() => { + if (!containerRef.current) return + + const container = containerRef.current + + const vertexShader = ` + void main() { + gl_Position = vec4(position, 1.0); + } + ` + + const fragmentShader = ` + #define TWO_PI 6.2831853072 + #define PI 3.14159265359 + + precision highp float; + uniform vec2 resolution; + uniform float time; + uniform float intensity; + + void main(void) { + vec2 uv = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y) / 6.0; + float t = time * 0.008; + float lineWidth = intensity; + + vec3 color = vec3(0.0); + for(int j = 0; j < 3; j++){ + for(int i = 0; i < 5; i++){ + color[j] += lineWidth * float(i * i) / abs(fract(t - 0.01 * float(j) + float(i) * 0.01) * 5.0 - length(uv) + mod(uv.x + uv.y, 0.2)); + } + } + + gl_FragColor = vec4(color[0], color[1], color[2], 1.0); + } + ` + + const camera = new THREE.Camera() + camera.position.z = 1 + + const scene = new THREE.Scene() + const geometry = new THREE.PlaneGeometry(2, 2) + + const uniforms = { + time: { type: "f", value: 1.0 }, + resolution: { type: "v2", value: new THREE.Vector2() }, + intensity: { type: "f", value: intensity }, + } + + const material = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: vertexShader, + fragmentShader: fragmentShader, + }) + + const mesh = new THREE.Mesh(geometry, material) + scene.add(mesh) + + const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true }) + renderer.setPixelRatio(1) + renderer.setClearColor(0x000000, 0) + + container.appendChild(renderer.domElement) + + const onWindowResize = () => { + const width = container.clientWidth + const height = container.clientHeight + renderer.setSize(width, height) + uniforms.resolution.value.x = renderer.domElement.width + uniforms.resolution.value.y = renderer.domElement.height + } + + onWindowResize() + window.addEventListener("resize", onWindowResize, false) + + const startTime = performance.now() + let lastRenderTime = 0 + const targetFPS = 10 + const frameInterval = 1000 / targetFPS + + const animate = (currentTime: number) => { + const animationId = requestAnimationFrame(animate) + + if (currentTime - lastRenderTime < frameInterval) { + if (sceneRef.current) { + sceneRef.current.animationId = animationId + } + return + } + lastRenderTime = currentTime + + const elapsed = (performance.now() - startTime) * 0.001 + const oscillation = Math.sin(elapsed * speed) * 6 + uniforms.time.value = oscillation + + renderer.render(scene, camera) + + if (sceneRef.current) { + sceneRef.current.animationId = animationId + } + } + + sceneRef.current = { + camera, + scene, + renderer, + uniforms, + animationId: 0, + startTime, + } + + requestAnimationFrame(animate) + + return () => { + window.removeEventListener("resize", onWindowResize) + + if (sceneRef.current) { + cancelAnimationFrame(sceneRef.current.animationId) + + if (container && sceneRef.current.renderer.domElement) { + container.removeChild(sceneRef.current.renderer.domElement) + } + + sceneRef.current.renderer.dispose() + geometry.dispose() + material.dispose() + } + } + }, [speed, intensity]) + + return ( +
+ ) +} diff --git a/apps/marketing/src/components/ui/wave-background.tsx b/apps/marketing/src/components/ui/wave-background.tsx new file mode 100644 index 00000000000..88e3fc35981 --- /dev/null +++ b/apps/marketing/src/components/ui/wave-background.tsx @@ -0,0 +1,314 @@ +'use client' +import * as React from 'react' +import { useEffect, useRef } from 'react' +import { createNoise2D } from 'simplex-noise' + +interface Point { + x: number + y: number + wave: { x: number; y: number } + cursor: { + x: number + y: number + vx: number + vy: number + } +} + +interface WavesProps { + className?: string + strokeColor?: string + backgroundColor?: string + pointerSize?: number +} + +export function Waves({ + className = "", + strokeColor = "#ffffff", // White lines + backgroundColor = "#000000", // Black background + pointerSize = 0.5 +}: WavesProps) { + const containerRef = useRef(null) + const svgRef = useRef(null) + const mouseRef = useRef({ + x: -10, + y: 0, + lx: 0, + ly: 0, + sx: 0, + sy: 0, + v: 0, + vs: 0, + a: 0, + set: false, + }) + const pathsRef = useRef([]) + const linesRef = useRef([]) + const noiseRef = useRef<((x: number, y: number) => number) | null>(null) + const rafRef = useRef(null) + const boundingRef = useRef(null) + + useEffect(() => { + if (!containerRef.current || !svgRef.current) return + + noiseRef.current = createNoise2D() + + setSize() + setLines() + + window.addEventListener('resize', onResize) + window.addEventListener('mousemove', onMouseMove) + containerRef.current.addEventListener('touchmove', onTouchMove, { passive: false }) + + rafRef.current = requestAnimationFrame(tick) + + return () => { + if (rafRef.current) cancelAnimationFrame(rafRef.current) + window.removeEventListener('resize', onResize) + window.removeEventListener('mousemove', onMouseMove) + containerRef.current?.removeEventListener('touchmove', onTouchMove) + } + }, []) + + const setSize = () => { + if (!containerRef.current || !svgRef.current) return + + boundingRef.current = containerRef.current.getBoundingClientRect() + const { width, height } = boundingRef.current + + svgRef.current.style.width = `${width}px` + svgRef.current.style.height = `${height}px` + } + + const setLines = () => { + if (!svgRef.current || !boundingRef.current) return + + const { width, height } = boundingRef.current + linesRef.current = [] + + pathsRef.current.forEach(path => { + path.remove() + }) + pathsRef.current = [] + + const xGap = 8 + const yGap = 8 + + const oWidth = width + 200 + const oHeight = height + 30 + + const totalLines = Math.ceil(oWidth / xGap) + const totalPoints = Math.ceil(oHeight / yGap) + + const xStart = (width - xGap * totalLines) / 2 + const yStart = (height - yGap * totalPoints) / 2 + + for (let i = 0; i < totalLines; i++) { + const points: Point[] = [] + + for (let j = 0; j < totalPoints; j++) { + const point: Point = { + x: xStart + xGap * i, + y: yStart + yGap * j, + wave: { x: 0, y: 0 }, + cursor: { x: 0, y: 0, vx: 0, vy: 0 }, + } + + points.push(point) + } + + const path = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path' + ) + path.classList.add('a__line') + path.classList.add('js-line') + path.setAttribute('fill', 'none') + path.setAttribute('stroke', strokeColor) + path.setAttribute('stroke-width', '1') + + svgRef.current.appendChild(path) + pathsRef.current.push(path) + + linesRef.current.push(points) + } + } + + const onResize = () => { + setSize() + setLines() + } + + const onMouseMove = (e: MouseEvent) => { + updateMousePosition(e.pageX, e.pageY) + } + + const onTouchMove = (e: TouchEvent) => { + e.preventDefault() + const touch = e.touches[0] + if (touch) { + updateMousePosition(touch.clientX, touch.clientY) + } + } + + const updateMousePosition = (x: number, y: number) => { + if (!boundingRef.current) return + + const mouse = mouseRef.current + mouse.x = x - boundingRef.current.left + mouse.y = y - boundingRef.current.top + window.scrollY + + if (!mouse.set) { + mouse.sx = mouse.x + mouse.sy = mouse.y + mouse.lx = mouse.x + mouse.ly = mouse.y + + mouse.set = true + } + + if (containerRef.current) { + containerRef.current.style.setProperty('--x', `${mouse.sx}px`) + containerRef.current.style.setProperty('--y', `${mouse.sy}px`) + } + } + + const movePoints = (time: number) => { + const { current: lines } = linesRef + const { current: mouse } = mouseRef + const { current: noise } = noiseRef + + if (!noise) return + + lines.forEach((points) => { + points.forEach((p: Point) => { + const move = noise( + (p.x + time * 0.002) * 0.002, + (p.y + time * 0.001) * 0.001 + ) * 3 + + p.wave.x = Math.cos(move) * 3 + p.wave.y = Math.sin(move) * 1.5 + + const dx = p.x - mouse.sx + const dy = p.y - mouse.sy + const d = Math.hypot(dx, dy) + const l = 100 + + let targetX = 0 + let targetY = 0 + + if (d < l && d > 0) { + const s = (1 - d / l) * (1 - d / l) + targetX = (dx / d) * s * 10 + targetY = (dy / d) * s * 10 + } + + p.cursor.x += (targetX - p.cursor.x) * 0.3 + p.cursor.y += (targetY - p.cursor.y) * 0.3 + }) + }) + } + + const moved = (point: Point, withCursorForce = true) => { + const coords = { + x: point.x + point.wave.x + (withCursorForce ? point.cursor.x : 0), + y: point.y + point.wave.y + (withCursorForce ? point.cursor.y : 0), + } + + return coords + } + + const drawLines = () => { + const { current: lines } = linesRef + const { current: paths } = pathsRef + + lines.forEach((points, lIndex) => { + const path = paths[lIndex] + const first = points[0] + if (points.length < 2 || !path || !first) return; + + const firstPoint = moved(first, false) + let d = `M ${firstPoint.x} ${firstPoint.y}` + + for (let i = 1; i < points.length; i++) { + const point = points[i] + if (!point) continue + const current = moved(point) + d += `L ${current.x} ${current.y}` + } + + path.setAttribute('d', d) + }) + } + + const tick = (time: number) => { + const { current: mouse } = mouseRef + + mouse.sx += (mouse.x - mouse.sx) * 0.1 + mouse.sy += (mouse.y - mouse.sy) * 0.1 + + const dx = mouse.x - mouse.lx + const dy = mouse.y - mouse.ly + const d = Math.hypot(dx, dy) + + mouse.v = d + mouse.vs += (d - mouse.vs) * 0.1 + mouse.vs = Math.min(100, mouse.vs) + + mouse.lx = mouse.x + mouse.ly = mouse.y + + mouse.a = Math.atan2(dy, dx) + + if (containerRef.current) { + containerRef.current.style.setProperty('--x', `${mouse.sx}px`) + containerRef.current.style.setProperty('--y', `${mouse.sy}px`) + } + + movePoints(time) + drawLines() + + rafRef.current = requestAnimationFrame(tick) + } + + return ( +
+ +
+
+ ) +}