);
}
// 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 = (