Redesign landing page with interactive mockups and new sections

This commit is contained in:
Mohamed Boudra
2026-03-28 21:39:37 +07:00
parent a3aed9b8b6
commit 2e9f935dbc
13 changed files with 1418 additions and 245 deletions
@@ -0,0 +1,737 @@
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";
// ── 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 ────────────────────────────────────────
type 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" };
};
type 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 ─────────────────────────────────────────────
type 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 },
{ name: "Review", provider: "claude", done: true },
];
// ── Diff data ──────────────────────────────────────────
type DiffLineType = "add" | "remove" | "context" | "header";
type 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-variable" }, { text: "h1", cls: "text-syn-tag" }, { text: ">", 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" }, { text: "main", cls: "text-syn-tag" }, { 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 ──────────────────────
const IMPLEMENT_CHAT: ChatItem[] = [
{ type: "text", text: "Starting implementation of the returns dashboard.", bold: true },
{ type: "tool", label: "Edit file", summary: "src/pages/dashboard.tsx", status: "done" },
{ type: "tool", label: "Edit file", summary: "src/components/return-table.tsx", status: "done" },
{ type: "tool", label: "Edit file", summary: "src/components/filter-bar.tsx", status: "done" },
{ type: "tool", label: "Edit file", summary: "src/components/status-chart.tsx", status: "done" },
{ type: "tool", label: "Run command", summary: "npm run typecheck", status: "done" },
{ type: "text", text: "Typecheck passes. Adding API route and data fetching." },
{ type: "tool", label: "Edit file", summary: "src/api/returns.ts", status: "done" },
{ type: "tool", label: "Edit file", summary: "src/hooks/use-returns.ts", status: "done" },
{ type: "tool", label: "Run command", summary: "npm run typecheck", status: "running" },
];
// ── Terminal log lines ─────────────────────────────────
const TERMINAL_LINES = [
{ text: "$ npm run dev", cls: "text-mock-fg" },
{ text: "", cls: "" },
{ text: "> acme-returns@0.1.0 dev", cls: "text-mock-fg-muted" },
{ text: "> next dev --turbopack", cls: "text-mock-fg-muted" },
{ text: "", cls: "" },
{ text: " ▲ Next.js 15.3.1 (Turbopack)", cls: "text-mock-fg" },
{ text: " - Local: http://localhost:3000", cls: "text-mock-fg-muted" },
{ text: "", cls: "" },
{ text: " ✓ Starting...", cls: "text-mock-green" },
{ text: " ✓ Ready in 1.2s", cls: "text-mock-green" },
{ text: " ○ Compiling /dashboard ...", cls: "text-mock-fg-muted" },
{ text: " ✓ Compiled /dashboard in 340ms", cls: "text-mock-green" },
{ text: " ○ Compiling /api/returns ...", cls: "text-mock-fg-muted" },
{ text: " ✓ Compiled /api/returns in 120ms", cls: "text-mock-green" },
];
// ── Sub-components ──────────────────────────────────────
function TrafficLights() {
return (
<div className="flex items-center gap-[6px]">
<div className="w-[11px] h-[11px] rounded-full bg-[#ff5f57]" />
<div className="w-[11px] h-[11px] rounded-full bg-[#febc2e]" />
<div className="w-[11px] h-[11px] rounded-full bg-[#28c840]" />
</div>
);
}
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 <SquareTerminal size={13} className={cls} />;
return provider === "claude"
? <ClaudeIcon size={13} className={cls} />
: <CodexIcon size={13} className={cls} />;
}
function TabBarAction({ icon: Icon }: { icon: React.ComponentType<{ size?: number; className?: string; strokeWidth?: number }> }) {
return (
<div className="w-4 h-5 flex items-center justify-center flex-shrink-0">
<Icon size={12} strokeWidth={1.5} className="text-mock-fg-muted" />
</div>
);
}
function PaneTabBar({ tabs, focused = false }: { tabs: TabDef[]; focused?: boolean }) {
return (
<div className="flex items-stretch h-7 bg-mock-surface0 border-b border-mock-border flex-shrink-0">
{tabs.map((tab) => (
<div key={tab.name} className="flex items-center gap-1.5 px-2 border-r border-mock-border relative min-w-0">
{tab.active && (
<div className={`absolute top-0 left-0 right-0 h-0.5 ${focused ? "bg-mock-accent" : "bg-mock-border-accent"}`} />
)}
<div className="relative flex-shrink-0">
<ProviderIcon provider={tab.provider} muted={!tab.active} />
</div>
<span className={`text-[11px] truncate ${tab.active ? "text-mock-fg" : "text-mock-fg-muted"}`}>
{tab.name}
</span>
<X size={11} className="text-mock-zinc500 flex-shrink-0 ml-auto" />
</div>
))}
<div className="ml-auto flex items-center px-1.5">
<TabBarAction icon={SquarePen} />
<TabBarAction icon={SquareTerminal} />
<TabBarAction icon={Columns2} />
<TabBarAction icon={Rows2} />
</div>
</div>
);
}
function Composer({ provider, model, focused = false }: { provider: "claude" | "codex"; model: string; focused?: boolean }) {
const Icon = provider === "claude" ? ClaudeIcon : CodexIcon;
return (
<div className="px-3 pb-3 flex-shrink-0">
<div className="bg-mock-surface1 border border-mock-border-accent rounded-2xl px-3 py-2 flex flex-col gap-[10px]">
<div className="flex items-center">
{focused && (
<span className="inline-block w-[1px] h-[13px] bg-mock-fg animate-pulse mr-[1px] flex-shrink-0" />
)}
<span className="text-[11px] text-mock-zinc500 leading-[1.4] select-none whitespace-nowrap overflow-hidden text-ellipsis">
Message the agent, tag @files, or use /commands and /skills
</span>
</div>
<div className="flex items-end justify-between">
<div className="flex items-center gap-0.5">
<div className="w-[22px] h-[22px] rounded-full flex items-center justify-center flex-shrink-0">
<Paperclip size={12} className="text-mock-fg" />
</div>
<div className="flex items-center gap-[4px] h-[22px] px-[6px] rounded-full">
<Icon size={12} className="text-mock-fg-muted" />
<span className="text-[10px] text-mock-fg-muted leading-none">{model}</span>
<ChevronDown size={10} className="text-mock-fg-muted" />
</div>
</div>
<div className="flex items-center gap-[6px]">
<div className="w-[22px] h-[22px] rounded-full flex items-center justify-center flex-shrink-0">
<Mic size={12} className="text-mock-fg" />
</div>
<div className="w-[22px] h-[22px] rounded-full bg-mock-accent flex items-center justify-center flex-shrink-0">
<ArrowUp size={12} className="text-white" />
</div>
</div>
</div>
</div>
</div>
);
}
function ChatMessages({ items }: { items: ChatItem[] }) {
return (
<div className="flex-1 overflow-hidden px-2 py-2">
{items.map((item, i) => {
if (item.type === "user") {
return (
<div key={i} className="flex justify-end py-1">
<div
className="bg-mock-surface2 px-2.5 py-1.5 max-w-[85%] leading-none"
style={{ borderRadius: "16px 2px 16px 16px" }}
>
<span className="text-[11px] text-mock-fg leading-none">{item.text}</span>
</div>
</div>
);
}
if (item.type === "tool") {
const isDone = item.status === "done";
return (
<div
key={i}
className="flex items-center rounded-lg px-2 py-[1px] -mx-[6px] border border-transparent"
>
<div className="w-[22px] h-[22px] rounded-full flex items-center justify-center flex-shrink-0 mr-1">
<Bot size={12} className="text-mock-fg-muted" />
</div>
<span className="text-[11px] text-mock-fg-muted truncate flex-shrink-0">
{item.label}
</span>
{item.summary && (
<span className="text-[11px] text-mock-zinc500 truncate flex-1 ml-2">
{item.summary}
</span>
)}
<div className="ml-auto flex-shrink-0 pl-2">
{isDone ? (
<Check size={11} className="text-mock-zinc500" />
) : (
<Loader size={11} className="text-mock-fg-muted animate-spin" />
)}
</div>
</div>
);
}
return (
<p
key={i}
className={`text-[11px] px-2 py-[1px] text-mock-fg leading-[1.5] ${item.bold ? "font-medium" : ""}`}
>
{item.text}
</p>
);
})}
</div>
);
}
function ChatArea() {
return (
<div className="flex flex-col flex-1 min-w-0 bg-mock-surface0">
<ChatMessages items={CHAT} />
<Composer provider="claude" model="Opus 4.6" focused />
</div>
);
}
function ImplementPane() {
return (
<div className="flex flex-col flex-1 min-w-0 min-h-0 bg-mock-surface0">
<ChatMessages items={IMPLEMENT_CHAT} />
<Composer provider="codex" model="gpt-5.4" />
</div>
);
}
function TerminalPane() {
return (
<div className="flex flex-col flex-1 min-w-0 min-h-0 bg-mock-surface0">
<div className="flex-1 overflow-hidden px-3 py-2">
{TERMINAL_LINES.map((line, i) => (
<div key={i} className="leading-[1.3]">
<code className={`text-[10px] font-mono ${line.cls} whitespace-pre`}>{line.text}</code>
</div>
))}
</div>
</div>
);
}
function ExplorerSidebar() {
return (
<div className="flex flex-col bg-mock-sidebar border-l border-mock-border w-[28%] flex-shrink-0 overflow-hidden">
{/* Explorer header — h-12, Changes/Files tabs */}
<div className="flex items-center h-10 px-2 border-b border-mock-border flex-shrink-0">
<div className="flex items-center gap-1">
<div className="flex items-center px-3 py-1.5 rounded-md bg-mock-surface1">
<span className="text-[11px] text-mock-fg font-medium">Changes</span>
</div>
<div className="flex items-center px-3 py-1.5 rounded-md">
<span className="text-[11px] text-mock-fg-muted">Files</span>
</div>
</div>
</div>
{/* Secondary header — h-9, Uncommitted dropdown + filter icons */}
<div className="flex items-center justify-between h-9 border-b border-mock-border flex-shrink-0">
<div className="flex items-center gap-1 ml-2 px-1 h-6 rounded">
<span className="text-[11px] text-mock-fg-muted">Uncommitted</span>
<ChevronDown size={11} className="text-mock-fg-muted" />
</div>
<div className="flex items-center pr-2">
<div className="flex items-center justify-center w-6 h-6 rounded">
<ListChevronsUpDown size={14} className="text-mock-fg-muted" />
</div>
</div>
</div>
{/* File header — expanded */}
<div className="flex items-center justify-between pl-2 pr-2 py-1.5 border-b border-mock-border flex-shrink-0">
<div className="flex items-center gap-1 flex-1 min-w-0 overflow-hidden">
<ChevronDown size={10} className="text-mock-fg-muted flex-shrink-0" />
<span className="text-[11px] text-mock-fg flex-shrink-0">dashboard.tsx</span>
<span className="text-[11px] text-mock-fg-muted truncate min-w-0"> src/pages</span>
<span
className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0"
style={{ backgroundColor: "rgba(46, 160, 67, 0.2)" }}
>New</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-1">
<span className="text-[11px] text-mock-green-400">+16</span>
<span className="text-[11px] text-mock-red">-0</span>
</div>
</div>
{/* Diff lines */}
<div className="overflow-hidden bg-mock-surface1">
{DIFF_LINES.map((line, i) => {
const isAdd = line.type === "add";
const isRemove = line.type === "remove";
const lineBg = isAdd ? "bg-mock-diff-add" : isRemove ? "bg-mock-diff-remove" : "bg-mock-surface1";
const lineNumCls = isAdd ? "text-mock-green-400" : isRemove ? "text-mock-red" : "text-mock-fg-muted";
return (
<div key={i} className={`flex items-stretch ${lineBg}`}>
<div className="w-8 border-r border-mock-border flex-shrink-0 flex items-center justify-end">
<code className={`text-[10px] font-mono ${lineNumCls} select-none pr-2 py-[1px]`}>{line.ln ?? ""}</code>
</div>
<code className="text-[10px] font-mono text-mock-fg pl-3 pr-3 py-[1px] whitespace-pre flex-1 min-w-0">
{line.tokens?.map((tok, j) => (
<span key={j} className={tok.cls}>{tok.text}</span>
))}
</code>
</div>
);
})}
</div>
{/* 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) => (
<div key={file.name} className="flex items-center justify-between pl-2 pr-2 py-1.5 border-b border-mock-border">
<div className="flex items-center gap-1 flex-1 min-w-0 overflow-hidden">
<ChevronRight size={10} className="text-mock-fg-muted flex-shrink-0" />
<span className="text-[11px] text-mock-fg flex-shrink-0">{file.name}</span>
<span className="text-[11px] text-mock-fg-muted truncate min-w-0"> {file.dir}</span>
{file.isNew && (
<span
className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0"
style={{ backgroundColor: "rgba(46, 160, 67, 0.2)" }}
>New</span>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-1">
<span className="text-[11px] text-mock-green-400">+{file.added}</span>
<span className="text-[11px] text-mock-red">-{file.removed}</span>
</div>
</div>
))}
<div className="flex-1" />
</div>
);
}
// 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 STEP_MS = SYNCED_LOADER_DURATION_MS / DOT_COUNT; // 158.333…ms per step
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;
return (
<div style={{ width: size, height: size, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
<div style={{ position: "relative", width: gridW, height: gridH }}>
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => {
const col = dotIndex % 2;
const row = Math.floor(dotIndex / 2);
// The snake sequence index for this dot: when is it the "head"?
const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]);
// Negative delay syncs the animation so this dot is the head at t=0 when sequenceIndex=0.
// At t=0, headIndex should be 0, meaning sequenceIndex=0 dot is the head.
// Each dot is shifted by sequenceIndex steps back in time.
const delayMs = -(sequenceIndex * STEP_MS);
return (
<div
key={dotIndex}
style={{
position: "absolute",
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`,
}}
/>
);
})}
</div>
</div>
);
}
function WorkspaceStatusDot({ status }: { status: SidebarWorkspace["status"] }) {
if (status === "running") return <div className="w-[5px] h-[5px] rounded-full bg-mock-amber" />;
if (status === "done") return <div className="w-[5px] h-[5px] rounded-full bg-mock-green" />;
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 (
<div className="flex flex-col bg-mock-sidebar border-r border-mock-border w-[200px] flex-shrink-0">
{/* Traffic lights */}
<div className="flex items-center pl-3 pr-2 pt-3 pb-2">
<TrafficLights />
</div>
{/* Sessions header */}
<div className="flex items-center gap-2 pl-3 pr-2 py-2 border-b border-mock-border">
<MessagesSquare size={16} className="text-mock-fg-muted flex-shrink-0" />
<span className="text-sm text-mock-fg-muted">Sessions</span>
</div>
{/* Project list */}
<div className="flex-1 overflow-hidden pt-1 pb-4 min-h-0">
{SIDEBAR_PROJECTS.map((project) => (
<div key={project.name} className="mb-2">
{/* Project row — icon aligns with traffic lights / sessions */}
<div className="flex items-center gap-2 min-h-[32px] py-1.5 pl-3 pr-2">
<div className="w-4 h-4 rounded-sm border border-mock-border flex items-center justify-center flex-shrink-0">
<span className="text-[9px] text-mock-fg-muted leading-none">{project.initial}</span>
</div>
<span className="text-[13px] text-mock-fg font-normal truncate flex-1 min-w-0 leading-5">
{project.name}
</span>
</div>
{/* Workspace rows — indented one level */}
{project.workspaces.map((workspace) => (
<div key={workspace.name} className={`mb-1 mx-1.5 rounded-lg ${workspace.selected ? "bg-mock-surface1" : ""}`}>
<div className="flex items-center gap-2 min-h-[28px] py-1 pl-[22px] pr-1">
<div className="relative w-[14px] h-4 flex-shrink-0 flex items-center justify-center">
{workspace.status === "syncing" ? (
<SyncedLoader size={11} />
) : (
<>
{workspace.kind === "worktree"
? <FolderGit2 size={14} className="text-mock-fg-muted" />
: <Monitor size={14} className="text-mock-fg-muted" />
}
{workspace.status !== "idle" && (
<div className="absolute bottom-0 right-0">
<WorkspaceStatusDot status={workspace.status} />
</div>
)}
</>
)}
</div>
<span className="text-[12px] text-mock-fg/[0.76] font-normal truncate flex-1 min-w-0 leading-[1.4]">
{workspace.name}
</span>
{workspace.diffStat && (
<div className="flex items-center gap-1 flex-shrink-0">
<span className="text-[10px] text-mock-green-400 font-normal leading-none">+{workspace.diffStat.additions}</span>
<span className="text-[10px] text-mock-red font-normal leading-none">-{workspace.diffStat.deletions}</span>
</div>
)}
</div>
{workspace.pr && (
<div className="flex items-center gap-1 pl-[42px] pr-2 pb-0.5">
<GitPullRequest size={11} className="text-mock-fg-muted" />
<span className="text-[10px] text-mock-fg-muted leading-none truncate">
#{workspace.pr.number} · {workspace.pr.state === "open" ? "Open" : workspace.pr.state === "merged" ? "Merged" : "Closed"}
</span>
</div>
)}
</div>
))}
</div>
))}
</div>
{/* Footer — dot aligns with traffic lights / icons */}
<div className="flex items-center justify-between pl-3 pr-2 py-3 border-t border-mock-border">
<div className="flex items-center gap-2 min-w-0 flex-shrink">
<div className="w-2 h-2 rounded-full bg-mock-green flex-shrink-0" />
<span className="text-[13px] text-mock-fg-muted truncate">MacBook Pro</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<div className="w-6 h-6 flex items-center justify-center">
<Plus size={18} className="text-mock-fg-muted" />
</div>
<div className="w-6 h-6 flex items-center justify-center">
<Settings size={18} className="text-mock-fg-muted" />
</div>
</div>
</div>
</div>
);
}
// ── Desktop Mockup ──────────────────────────────────────
function DesktopMockup() {
const containerRef = React.useRef<HTMLDivElement>(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);
}, []);
return (
<div ref={containerRef} className="w-full overflow-hidden" style={{ height: `${1200 * (9 / 16) * scale}px` }}>
<div
className="mx-auto rounded-xl overflow-hidden border border-mock-border bg-mock-surface0 shadow-[6px_6px_0_rgba(0,0,0,0.4)] origin-top-left"
style={{ width: 1200, transform: `scale(${scale})` }}
>
{/* Top-level: left sidebar | center column | explorer sidebar — all full height */}
<div className="flex aspect-video">
{/* Left sidebar — full height */}
<motion.div {...fade(D.sidebar)} className="contents">
<Sidebar />
</motion.div>
{/* Center column: title bar + split panes */}
<div className="flex flex-col flex-1 min-w-0 min-h-0">
{/* Title bar — belongs to center column only */}
<motion.div {...fade(D.titleBar)} className="flex items-center h-10 px-2 bg-mock-surface0 border-b border-mock-border flex-shrink-0">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
<PanelLeft size={14} className="text-mock-fg-muted" />
</div>
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="text-[13px] font-light text-mock-fg truncate flex-shrink-0">main</span>
<span className="text-[13px] text-mock-fg-muted truncate flex-shrink min-w-0">acme/returns-app</span>
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
<Ellipsis size={14} className="text-mock-fg-muted" />
</div>
</div>
</div>
<div className="flex items-center gap-2 ml-auto flex-shrink-0">
<div className="flex items-stretch rounded-md border border-mock-border-accent overflow-hidden">
<div className="flex items-center gap-2 px-3 py-1">
<GitCommitHorizontal size={12} className="text-mock-fg-muted flex-shrink-0" />
<span className="text-[11px] text-mock-fg font-normal">Commit</span>
</div>
<div className="flex items-center justify-center w-7 border-l border-mock-border-accent">
<ChevronDown size={12} className="text-mock-fg-muted" />
</div>
</div>
<div className="flex items-center gap-2 px-3 py-1 rounded-md">
<SourceControlIcon size={14} className="text-mock-fg-muted" />
<span className="text-[11px] font-normal text-mock-green-400">+247</span>
<span className="text-[11px] font-normal text-mock-red">-15</span>
</div>
</div>
</motion.div>
{/* Split panes */}
<div className="flex flex-1 min-h-0">
{/* Left pane: all agent tabs + chat */}
<div className="flex flex-col flex-1 min-w-0 min-h-0">
<motion.div {...fade(D.tabs)}>
<PaneTabBar tabs={TABS} focused />
</motion.div>
<motion.div {...fade(D.chat)} className="flex-1 flex min-h-0">
<ChatArea />
</motion.div>
</div>
{/* Resize handle */}
<div className="w-px bg-mock-border flex-shrink-0" />
{/* Right pane: terminal only */}
<motion.div {...fade(D.chat)} className="flex flex-col flex-1 min-w-0 min-h-0">
<PaneTabBar tabs={[{ name: "npm run dev", provider: "terminal", done: false, active: true }]} />
<TerminalPane />
</motion.div>
</div>
</div>
{/* Explorer sidebar — full height, pushes center column */}
<motion.div {...fade(D.diffPanel)} className="contents">
<ExplorerSidebar />
</motion.div>
</div>
</div>
</div>
);
}
// ── Export ───────────────────────────────────────────────
export function HeroMockup() {
return <DesktopMockup />;
}