// terminal-section.jsx — terminal que se escribe sola cuando está en view const TERMINAL_LINES = [ { type: 'cmd', text: 'git clone git@github.com:irontec-comms/nexus.git' }, { type: 'out', text: 'Cloning into \u0027nexus\u0027… done.' }, { type: 'cmd', text: 'pnpm install' }, { type: 'out', text: '+ 1247 packages installed · 18.3s' }, { type: 'cmd', text: 'pnpm --filter server payload migrate' }, { type: 'out', text: '✓ migrations applied · Postgres schema ready' }, { type: 'cmd', text: 'pnpm dev' }, { type: 'out', text: '▲ Next.js 16 · ready on http://localhost:3000' }, { type: 'out', text: '● Payload admin → /admin' }, { type: 'out', text: '● MCP proxy → /api/search/mcp' }, { type: 'out', text: '● Keycloak OIDC → ready' }, { type: 'ok', text: 'Nexus is live.' }, ]; const Terminal = ({ active }) => { const [charCount, setCharCount] = React.useState(0); // Build flat character stream with per-line markers const stream = React.useMemo(() => { const out = []; TERMINAL_LINES.forEach((line, i) => { const prefix = line.type === 'cmd' ? '$ ' : line.type === 'ok' ? '✓ ' : ' '; const full = prefix + line.text; for (let c = 0; c < full.length; c++) { out.push({ ch: full[c], line: i, type: line.type }); } out.push({ ch: '\n', line: i, type: line.type, newline: true }); }); return out; }, []); React.useEffect(() => { if (!active) return; let idx = 0; setCharCount(0); let cancelled = false; const tick = () => { if (cancelled) return; idx++; setCharCount(idx); if (idx < stream.length) { const ch = stream[idx - 1]?.ch; const delay = ch === '\n' ? 180 : (ch === ' ' ? 18 : 22); setTimeout(tick, delay); } }; const start = setTimeout(tick, 400); return () => { cancelled = true; clearTimeout(start); }; }, [active, stream]); const visible = stream.slice(0, charCount); const lines = []; let current = { parts: [], type: 'out', key: 0 }; visible.forEach((c, i) => { if (c.newline) { lines.push(current); current = { parts: [], type: 'out', key: lines.length }; } else { current.parts.push(c.ch); current.type = c.type; } }); if (current.parts.length > 0) lines.push(current); const caretVisible = charCount < stream.length ? true : (Math.floor(Date.now() / 500) % 2 === 0); return (
{/* Window chrome */}
~/nexus · zsh
{lines.map((line, i) => { const text = line.parts.join(''); const color = line.type === 'cmd' ? '#f9f9f9' : line.type === 'ok' ? '#a1f75e' : 'rgba(249,249,249,0.55)'; const weight = line.type === 'cmd' ? 600 : 400; const isLast = i === lines.length - 1; return (
{text} {isLast && caretVisible && ( )}
); })}
); }; const TerminalSection = () => { const ref = React.useRef(null); const inView = useInView(ref, { threshold: 0.35, once: true }); return (
{/* Background grid */}
02 · Arranque

De cero a producción
en cuatro comandos.

Monorepo TypeScript, Payload CMS, Keycloak, Postgres, Typesense y MCP configurados y cableados. Helm charts, ArgoCD y Talos listos en deploy/.

{['TypeScript · pnpm · Turborepo', 'Next.js 16 + Payload CMS 3', 'better-auth + Keycloak OIDC', 'Typesense hybrid search + RAG'].map(s => (
{s}
))}
); }; Object.assign(window, { TerminalSection });