From 489f9f09e1d36b19ec1711b244ac7fc3e4dc2795 Mon Sep 17 00:00:00 2001 From: tyulyukov Date: Wed, 29 Apr 2026 20:09:01 +0300 Subject: [PATCH] feat(landing): add interactive app preview to hero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inspired by https://github.com/pingdotgg/t3code/pull/2349 by @maria-rcks. Replaces the static hero (logo + headline + CTA) with an additional interactive preview underneath: a faithful, MarCode-branded recreation of the actual app shell — sidebar with projects/threads, chat timeline with user/assistant/tool turns, composer with model + access + branch controls, and a checkout bar at the bottom. Real interactivity (no static screenshots, no animation hacks): - Click any thread in the sidebar → header, timeline, composer placeholder, branch, worktree, access mode all swap together. - Click any project header → expands/collapses its thread group with a rotating chevron. - Working/Completed status pills track the active thread. The upstream PR was an Astro + 920 lines of vanilla JS + 2300 lines of CSS implementation for T3 Code's Astro marketing app. MarCode's landing is Next.js + React + Tailwind v4, so this is a from-scratch port that takes the *idea* (interactive shell preview replacing a screenshot) and rebuilds it against the existing brand tokens (fresh-syntax, curious-sky, dream-shift, sunbyte, rebel-mint, neo-pine, noir, mindspace) and Klaster Sans + Inter typography. Implementation: - `apps/landing/src/components/AppPreview/data.ts` (~270 lines): PreviewThread / PreviewProject / PreviewTurn shapes + small fixture set covering 4 projects, 8 threads, and turn scripts that reflect recent MarCode work (this app preview, the chat minimap, the rate limit meter, etc.). - `apps/landing/src/components/AppPreview/AppPreview.tsx` (~470 lines): the interactive component. Pure React state — `useState` for the active thread, active model, and the collapsed-projects set. - `apps/landing/src/components/Hero.tsx`: drops `min-h-[85vh]`, mounts the preview in a `max-w-5xl` container below the existing CTA stack. Verified with Playwright at 1280×1100: - Initial render shows the marcode-landing thread active with full user/assistant/tool turn timeline. - Clicking "Wire the chat minimap rail into the timeline" swaps the entire content (header, timeline, composer placeholder, branch, Auto-accept edits access mode) instantly. - Clicking the lawn project header collapses its threads with rotated chevron. - Build passes (`next build`, 23.2 kB additional first-load JS). - Typecheck passes. --- .../src/components/AppPreview/AppPreview.tsx | 483 ++++++++++++++++++ .../landing/src/components/AppPreview/data.ts | 353 +++++++++++++ apps/landing/src/components/Hero.tsx | 8 +- 3 files changed, 843 insertions(+), 1 deletion(-) create mode 100644 apps/landing/src/components/AppPreview/AppPreview.tsx create mode 100644 apps/landing/src/components/AppPreview/data.ts diff --git a/apps/landing/src/components/AppPreview/AppPreview.tsx b/apps/landing/src/components/AppPreview/AppPreview.tsx new file mode 100644 index 00000000000..a0b646d6c6e --- /dev/null +++ b/apps/landing/src/components/AppPreview/AppPreview.tsx @@ -0,0 +1,483 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { + ArrowDownIcon, + ArrowDownUpIcon, + ChevronDownIcon, + ChevronRightIcon, + CornerDownLeftIcon, + EyeIcon, + FolderIcon, + GitBranchIcon, + GlobeIcon, + ImageIcon, + LockIcon, + PencilIcon, + PlusIcon, + SearchIcon, + ShieldIcon, + TerminalIcon, + WrenchIcon, + ZapIcon, +} from "lucide-react"; +import { + ACCESS_LABELS, + MODELS, + previewProjects, + previewThreads, + previewTurns, + type PreviewAccessMode, + type PreviewTurn, +} from "./data"; + +type ToolCallKind = "command" | "read" | "edit" | "search" | "fetch"; + +const TOOL_ICONS: Record = { + command: TerminalIcon, + read: EyeIcon, + edit: PencilIcon, + search: SearchIcon, + fetch: GlobeIcon, +}; + +const ACCESS_ICONS: Record = { + "approval-required": ShieldIcon, + "auto-accept-edits": LockIcon, + "full-access": ZapIcon, +}; + +const ACCESS_ACCENTS: Record = { + "approval-required": "text-sunbyte", + "auto-accept-edits": "text-curious-sky", + "full-access": "text-rebel-mint", +}; + +const MODEL_DOT_BG: Record<"fresh-syntax" | "rebel-mint" | "dream-shift" | "curious-sky", string> = + { + "fresh-syntax": "bg-fresh-syntax", + "rebel-mint": "bg-rebel-mint", + "dream-shift": "bg-dream-shift", + "curious-sky": "bg-curious-sky", + }; + +function ProjectIcon({ icon }: { icon: "marcode" | "round" | "lawn" | "folder" }) { + if (icon === "marcode") { + return ( + + + M + + + ); + } + if (icon === "round") { + return ( + + + + ); + } + if (icon === "lawn") { + return ( + + + + + + ); + } + return ( + + + + ); +} + +function ToolCallRow({ call }: { call: { kind: ToolCallKind; heading: string; preview: string } }) { + const Icon = TOOL_ICONS[call.kind]; + return ( +
+ +
+
{call.heading}
+
{call.preview}
+
+
+ ); +} + +function TurnView({ turn }: { turn: PreviewTurn }) { + if (turn.type === "user") { + return ( +
+
+ {turn.text} +
+
+ ); + } + + if (turn.type === "tool") { + return ( +
+ + + +
+
{turn.title}
+
+ {turn.calls.map((call, i) => ( + + ))} +
+
+
+ ); + } + + return ( +
+ + + M + + +
+ {turn.text} +
+
+ ); +} + +function StatusDot({ status }: { status: "Working" | "Completed" }) { + if (status === "Working") { + return ( + + + + + ); + } + return ; +} + +function PopoverButton({ + label, + value, + icon, + accent, +}: { + label: string; + value: string; + icon?: React.ReactNode; + accent?: string; +}) { + return ( + + ); +} + +export function AppPreview() { + const [activeThreadId, setActiveThreadId] = useState(previewThreads[0]!.id); + const [activeModel, setActiveModel] = useState(MODELS[0]!.model); + const [collapsedProjects, setCollapsedProjects] = useState>(new Set()); + + const activeThread = useMemo( + () => previewThreads.find((t) => t.id === activeThreadId) ?? previewThreads[0]!, + [activeThreadId], + ); + const activeTurns = previewTurns[activeThreadId] ?? []; + const AccessIcon = ACCESS_ICONS[activeThread.access]; + const accessAccent = ACCESS_ACCENTS[activeThread.access]; + + const modelMeta = MODELS.find((m) => m.model === activeModel) ?? MODELS[0]!; + + const toggleProject = (projectId: string) => { + setCollapsedProjects((prev) => { + const next = new Set(prev); + if (next.has(projectId)) next.delete(projectId); + else next.add(projectId); + return next; + }); + }; + + return ( +
+ {/* Decorative glow */} +
+
+
+
+ +
+
+ {/* ── Sidebar ─────────────────────────────────────────── */} + + + {/* ── Main pane ───────────────────────────────────────── */} +
+ {/* Header */} +
+
+
+ {activeThread.title} +
+
+ p.id === activeThread.projectId)?.icon ?? "folder" + } + /> + {previewProjects.find((p) => p.id === activeThread.projectId)?.title} + · + {activeThread.age} +
+
+ {activeThread.status === "Working" ? ( + + + Working + + ) : activeThread.status === "Completed" ? ( + + + Completed + + ) : null} +
+ + {/* Timeline */} +
+
+ {activeTurns.length === 0 ? ( +
+ Send a prompt to begin a new turn. +
+ ) : ( + activeTurns.map((turn, i) => ) + )} + {activeThread.status === "Working" && ( +
+ + Working... +
+ )} +
+
+ + {/* Composer */} +
+
+