// site/HeroReveal.jsx — "Build the terminal" reveal sequence. // // Scene: empty dark page, one blinking prompt "❯ build terminal_". // User clicks / hits Enter → the prompt morphs into a chat panel, // then panels assemble around it in 8 steps, an AI narrates what's // happening in the chat, and at the end the whole assembly scales // down to sit in the hero like a poster. // // Caching: sessionStorage flag. Skip replay if user already saw it. // Hard reload with empty storage = full replay. const { useState, useEffect, useRef, useCallback } = React; // ───────── audio (tiny synthesized ticks, no assets) ───────── function useAudioTick() { const ctxRef = useRef(null); const mutedRef = useRef(localStorage.getItem('mlc-muted') === '1'); const ensure = () => { if (!ctxRef.current) { try { ctxRef.current = new (window.AudioContext || window.webkitAudioContext)(); } catch { ctxRef.current = null; } } return ctxRef.current; }; const tick = useCallback((kind = 'click') => { if (mutedRef.current) return; const ctx = ensure(); if (!ctx) return; const now = ctx.currentTime; const o = ctx.createOscillator(); const g = ctx.createGain(); o.connect(g); g.connect(ctx.destination); if (kind === 'click') { o.frequency.value = 1800; o.type = 'square'; g.gain.setValueAtTime(0.02, now); g.gain.exponentialRampToValueAtTime(0.0001, now + 0.04); o.start(now); o.stop(now + 0.05); } if (kind === 'type') { o.frequency.value = 2400; o.type = 'square'; g.gain.setValueAtTime(0.008, now); g.gain.exponentialRampToValueAtTime(0.0001, now + 0.02); o.start(now); o.stop(now + 0.03); } if (kind === 'snap') { o.frequency.value = 880; o.type = 'triangle'; g.gain.setValueAtTime(0.04, now); g.gain.exponentialRampToValueAtTime(0.0001, now + 0.08); o.start(now); o.stop(now + 0.1); } if (kind === 'chime') { const f = [660, 990, 1320]; f.forEach((hz, i) => { const oo = ctx.createOscillator(); const gg = ctx.createGain(); oo.connect(gg); gg.connect(ctx.destination); oo.frequency.value = hz; oo.type = 'sine'; gg.gain.setValueAtTime(0.00, now + i * 0.05); gg.gain.linearRampToValueAtTime(0.05, now + i * 0.05 + 0.02); gg.gain.exponentialRampToValueAtTime(0.0001, now + i * 0.05 + 0.5); oo.start(now + i * 0.05); oo.stop(now + i * 0.05 + 0.6); }); } }, []); const setMuted = (v) => { mutedRef.current = v; localStorage.setItem('mlc-muted', v ? '1' : '0'); }; const isMuted = () => mutedRef.current; return { tick, setMuted, isMuted }; } // ───────── the reveal sequence ───────── // Each step adds/removes panels and optionally sets a narration line. // Total duration: ~17 seconds after Build is pressed. const STEPS = [ // step 0 is "before build" (initial) // step 1: chat panel appears in center (zoom-in from a command line) { t: 0, layout: 'chat', click: null, narrate: { who: 'you', text: 'build terminal' } }, { t: 600, layout: 'chat', click: null, narrate: { who: 'claude', text: 'Starting with the agent panel. This is where I live when I\'m running inside an IDE — a plain chat, with context.' } }, { t: 2200, layout: 'chat', click: null, narrate: { who: 'claude', text: 'Adding the icon rail. Terminals are keyboard-first, but when you do reach for the mouse, everything has a home.' } }, { t: 3600, layout: 'chat+rail', click: 'rail-wl', narrate: null }, { t: 4200, layout: 'chat+rail+watchlist', click: null, narrate: { who: 'claude', text: 'Watchlist opened. Each row is a symbol, a bar, a sparkline. No menus, nothing hidden.' } }, { t: 5800, layout: 'chat+rail+watchlist', click: 'wl-row', narrate: null }, { t: 6400, layout: 'full-chart', click: null, narrate: { who: 'claude', text: 'Clicking a ticker opens the chart. Canvas is WebGPU — streams, not polls. The chat collapsed to give it the room.' } }, { t: 8600, layout: 'full-chart', click: 'rail-agents', narrate: null }, { t: 9200, layout: 'chart+agents', click: null, narrate: { who: 'claude', text: 'I\'m back — now I can see the chart you\'re looking at. Everything the UI renders from, I can read.' } }, { t: 11200, layout: 'chart+agents', click: 'top-sym', narrate: null }, { t: 11800, layout: 'full', click: null, narrate: { who: 'claude', text: 'Last pieces: window chrome, left drawing tools, top symbol bar, bottom status. This is a full workstation.' } }, { t: 14000, layout: 'full', click: null, narrate: { who: 'claude', text: 'That\'s the terminal. Every panel here is a surface I can touch over HTTP.' } }, { t: 16200, layout: 'full', click: null, narrate: null, zoomOut: true }, ]; // ───────── panel atoms ───────── const PIXEL = (n) => `${n}px`; function IconBtn({ label, active, pulse, onClick, children }) { return ( ); } // Icon primitives (trimmed to match the screenshot's vocabulary) const I = { term: (c='currentColor') => , chat: (c='currentColor') => , bell: (c='currentColor') => , layers: (c='currentColor') => , chart: (c='currentColor') => , disk: (c='currentColor') => , gear: (c='currentColor') => , mascot: (c='currentColor') => , plane: (c='currentColor') => , pen: (c='currentColor') => , line: (c='currentColor') => , rect: (c='currentColor') => , text: (c='currentColor') => , trash: (c='currentColor') => , }; // ═══════════════════════════════════════════════════════════ // Agents (chat) panel — variable width depending on layout // ═══════════════════════════════════════════════════════════ function ChatPanel({ width, messages, typing, active, highlight }) { const bodyRef = useRef(null); useEffect(() => { if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }, [messages, typing]); return (
{/* Panel titlebar — "Agents" */}
🧙 Agents ×
{/* Tiny toolbar row — CL / CX / GM / OC / split / R / × */}
{I.term('#F4CD63')} {I.chat()} CL CX GM OC R ×
{/* Session tabs */}
CLAUDE Sessions ▾ + ×
{/* Messages */}
{messages.map((m, i) => )} {typing && (
{[0,1,2].map((k) => ( ))}
)}
{/* Composer */}
Message…
Claude Sonnet 4.6 Default ◐ 3%
); } function Msg({ m }) { if (m.who === 'you') { return (
{m.text}
); } return (
{m.text}
); } // ═══════════════════════════════════════════════════════════ // Right icon rail // ═══════════════════════════════════════════════════════════ function IconRail({ highlight }) { const items = [ { key: 'alerts', icon: I.bell }, { key: 'layers', icon: I.layers }, { key: 'wl', icon: I.chart }, { key: 'disk', icon: I.disk }, { key: 'gear', icon: I.gear }, { key: 'mascot', icon: I.mascot }, { key: 'plane1', icon: I.plane }, { key: 'plane2', icon: I.plane }, { key: 'plane3', icon: I.plane }, ]; return (
{items.map((it) => ( {it.icon(highlight === 'rail-' + it.key ? '#F4CD63' : undefined)} ))}
); } // ═══════════════════════════════════════════════════════════ // Watchlist panel // ═══════════════════════════════════════════════════════════ const WL = [ { s: 'BTCUSDT', l: 77180.5, c: +1.24 }, { s: 'ETHUSDT', l: 3912.7, c: +0.42 }, { s: 'SOLUSDT', l: 184.3, c: +2.88 }, { s: 'BNBUSDT', l: 712.1, c: -0.18 }, { s: 'XRPUSDT', l: 2.48, c: +0.91 }, { s: 'DOGEUSDT',l: 0.381,c: -1.14 }, { s: 'ADAUSDT', l: 1.04, c: +0.36 }, { s: 'AVAXUSDT',l: 42.18, c: -0.72 }, ]; function WatchlistPanel({ highlight }) { return (
WATCHLIST crypto · 8
{WL.map((r, i) => { const active = highlight === 'wl-row' && i === 0; return (
{r.s}
{r.l.toLocaleString(undefined, { maximumFractionDigits: 3 })}
= 0 ? '#7ec98f' : '#e07a7a', fontVariantNumeric: 'tabular-nums', textAlign: 'right', minWidth: 44, }}>{r.c >= 0 ? '+' : ''}{r.c.toFixed(2)}%
{active && ( )}
); })}
); } // ═══════════════════════════════════════════════════════════ // Chart panel — candlestick + crosshair // ═══════════════════════════════════════════════════════════ function ChartPanel({ draw = false }) { // Generate deterministic candles const candles = React.useMemo(() => { const out = []; let p = 76000; for (let i = 0; i < 60; i++) { const drift = Math.sin(i / 6) * 400 + Math.cos(i / 11) * 200; const o = p; const c = 76000 + drift + (((i * 7919) % 400) - 200); const h = Math.max(o, c) + ((i * 37) % 180) + 40; const l = Math.min(o, c) - ((i * 53) % 180) - 40; out.push({ o, h, l, c }); p = c; } return out; }, []); const min = Math.min(...candles.map(c => c.l)); const max = Math.max(...candles.map(c => c.h)); const span = max - min; const W = 700, H = 360; const PAD = 40; const innerW = W - PAD * 2, innerH = H - 40; const cw = innerW / candles.length; const yOf = (v) => 20 + (1 - (v - min) / span) * innerH; return (
{/* Chart sub-titlebar */}
BTCUSDT · 15m · bybit · S
{/* Canvas */}
{/* grid */} {[0,1,2,3,4].map((i) => { const y = 20 + (i / 4) * innerH; return ; })} {/* candles */} {candles.map((c, i) => { const x = PAD + i * cw; const up = c.c >= c.o; const col = up ? '#44c47d' : '#e07a7a'; const yO = yOf(c.o), yC = yOf(c.c); const yH = yOf(c.h), yL = yOf(c.l); return ( ); })} {/* last price line */} {/* price axis */} {[0, 0.25, 0.5, 0.75, 1].map((f, i) => { const v = min + f * span; const y = yOf(v); return ( {v.toFixed(0)} ); })}
); } // Left drawing-tools rail function LeftToolbar() { const tools = [I.pen, I.line, I.rect, I.text, I.trash]; return (
{tools.map((Tool, i) => (
{Tool()}
))}
); } // Top symbol/timeframe toolbar function TopToolbar({ highlight }) { const active = highlight === 'top-sym'; return (
BTCUSDT {active && }
+ 15m | | | ↶ ↷ □ ⟂ 📷
); } // Window chrome (top tab bar) function WindowChrome() { return (
{['Untitled 32', 'Untitled 29', 'Untitled 35', 'Untitled 36'].map((t, i) => (
{t} ×
))} + ↗ ⚙ × — □ ×
); } // Bottom status bar function StatusBar() { return (
[UTC+0] 6:21:33 PM
); } // ═══════════════════════════════════════════════════════════ // The sequence component itself // ═══════════════════════════════════════════════════════════ function HeroReveal() { const audio = useAudioTick(); const alreadySeen = typeof window !== 'undefined' && sessionStorage.getItem('mlc-reveal-seen') === '1'; const [phase, setPhase] = useState(alreadySeen ? 'done' : 'idle'); // idle | building | done const [stepIdx, setStepIdx] = useState(0); const [messages, setMessages] = useState([]); const [typing, setTyping] = useState(false); const [typedBuild, setTypedBuild] = useState(''); // characters of "build terminal" typed const buildCmd = 'build terminal'; // Track viewport so layout stays responsive on rotate / resize const [vp, setVp] = useState(() => ({ w: typeof window !== 'undefined' ? window.innerWidth : 1200, h: typeof window !== 'undefined' ? window.innerHeight : 800, })); useEffect(() => { if (typeof window === 'undefined') return; const onResize = () => setVp({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', onResize); window.addEventListener('orientationchange', onResize); return () => { window.removeEventListener('resize', onResize); window.removeEventListener('orientationchange', onResize); }; }, []); const isMobile = vp.w < 760; // Idle: blinking cursor on "❯ " // Click or Enter → start build const start = () => { if (phase !== 'idle') return; setPhase('building'); audio.tick('click'); // Type out "build terminal" over 600ms let i = 0; const typeIv = setInterval(() => { i++; setTypedBuild(buildCmd.slice(0, i)); audio.tick('type'); if (i >= buildCmd.length) { clearInterval(typeIv); setTimeout(() => { audio.tick('snap'); runSequence(); }, 300); } }, 55); }; const runSequence = () => { // Kick off the STEPS schedule STEPS.forEach((step, i) => { setTimeout(() => { setStepIdx(i); if (step.click) audio.tick('click'); if (step.narrate) { const { who, text } = step.narrate; if (who === 'you') { setMessages((m) => [...m, { who, text }]); } else { setTyping(true); // stream-typing feel: land the message in ~800ms with typing indicator setTimeout(() => { setTyping(false); setMessages((m) => [...m, { who, text }]); }, 900); } } if (step.zoomOut) { audio.tick('chime'); setTimeout(() => { sessionStorage.setItem('mlc-reveal-seen', '1'); setPhase('done'); }, 900); } }, step.t); }); }; // Keyboard: Enter starts, M toggles mute useEffect(() => { const onKey = (e) => { if (phase === 'idle' && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); start(); } if (e.key === 'm' || e.key === 'M') { audio.setMuted(!audio.isMuted()); forceMuteIcon(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [phase]); const [muteIcon, setMuteIcon] = useState(audio.isMuted()); const forceMuteIcon = () => setMuteIcon(audio.isMuted()); // Allow the user to replay const replay = () => { sessionStorage.removeItem('mlc-reveal-seen'); setPhase('idle'); setStepIdx(0); setMessages([]); setTyping(false); setTypedBuild(''); }; // Force a re-render right after phase flips to 'done', so the portal // can find the slot element (IIFE reads document directly). const [, force] = useState(0); useEffect(() => { if (phase !== 'done') return; // next frame — ensures the slot is in the DOM and layout is stable requestAnimationFrame(() => force(n => n + 1)); }, [phase]); // Phase flags — must be declared before use below. const building = phase === 'building'; const done = phase === 'done'; const idle = phase === 'idle'; // When `done`, freeze on the final layout so the terminal doesn't // re-animate its sub-panels when we swap containers. const current = done ? { layout: 'full', click: null } : (STEPS[stepIdx] || { layout: 'chat' }); const layout = current.layout; const highlight = current.click; // ───────── layout decisions based on `layout` string ───────── // chat, chat+rail, chat+rail+watchlist, full-chart, chart+agents, full const showRail = !isMobile && (layout.includes('rail') || layout === 'full-chart' || layout === 'chart+agents' || layout === 'full'); const showWatchlist = !isMobile && (layout.includes('watchlist') || layout === 'full-chart' || layout === 'chart+agents' || layout === 'full'); const showChart = layout === 'full-chart' || layout === 'chart+agents' || layout === 'full'; const showChat = layout === 'chat' || layout === 'chat+rail' || layout === 'chat+rail+watchlist' || layout === 'chart+agents' || layout === 'full'; const showLeftTb = !isMobile && layout === 'full'; const showTopTb = layout === 'full' || layout === 'chart+agents'; const showChrome = layout === 'full'; const showStatus = !isMobile && layout === 'full'; // Chat panel width let chatW = 0; if (showChat && !showChart) chatW = layout === 'chat' ? 580 : layout === 'chat+rail' ? 580 : 520; if (showChat && showChart) chatW = 320; if (layout === 'full-chart') chatW = 0; // On mobile: chat takes full available width (subtracting other panels later) if (isMobile && showChat) { // When chart is also visible, hide chat (chart wins) and vice versa — never both on tiny screens if (showChart) { chatW = 0; } else { chatW = Math.max(260, vp.w - 16); // panel container fills bW; flex handles it } } // watchlist hidden in chart+agents? — in chart+agents it *is* visible const showWatchlistInline = showWatchlist && (layout === 'chat+rail+watchlist' || layout === 'full-chart' || layout === 'chart+agents' || layout === 'full'); // Build container const terminalScale = done ? 1 : 1; // ───── sizes (single place, no dual-render) ───── const heroTarget = done; // slot into hero when done // Width by layout step during build. Sum of child panel widths + small slack. // chat=580, chat+rail = chat 580 + rail 40 = 620. // chat+rail+watchlist = chat 520 + watchlist 260 + rail 40 = 820. let bW = 580, bH = 560; if (layout === 'chat+rail') { bW = 620; bH = 560; } else if (layout === 'chat+rail+watchlist') { bW = 820; bH = 580; } else if (layout === 'full-chart' || layout === 'chart+agents' || layout === 'full') { bW = Math.min(1280, Math.round(vp.w * 0.92)); bH = Math.min(700, Math.round(vp.h * 0.78)); } // Clamp to viewport so terminal never overflows on narrow screens const wBudget = Math.max(280, vp.w - 16); const hBudget = Math.max(360, vp.h - 80); bW = Math.min(bW, wBudget); bH = Math.min(bH, hBudget); // On mobile, chat fills whatever bW is (since rail/watchlist are hidden) if (isMobile && showChat && !showChart) chatW = bW; return (
{/* Lock page scroll while the reveal overlay is up */} {!done && } {/* Backdrop — fades out on done */}
{/* Idle prompt — standalone, fades out when build starts */} {idle && (
build terminal
press or click anywhere
)} {/* Persistent terminal wrapper. During build: fixed+centered over viewport. When done: portaled INTO the hero slot so it scrolls with the page. */} {!idle && (() => { const slot = done && typeof document !== 'undefined' ? document.getElementById('hero-terminal-slot') : null; const terminalNode = (
{showChrome && } {showTopTb && }
{showLeftTb && } {showChart && } {showChat && ( )} {showWatchlistInline && } {showRail && }
{showStatus && } {done && ( )}
); return slot && ReactDOM.createPortal ? ReactDOM.createPortal(terminalNode, slot) : terminalNode; })()} {/* Mute toggle (during build only) */} {!done && ( )} {/* Skip */} {!done && ( )} {/* Animations */}
); }