(() => { 'use strict'; const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const source = $('#terminal-data'); if (!source) return; let DATA; try { DATA = JSON.parse(source.textContent); } catch (err) { return; } const contactHref = (subject) => { const link = $('[data-contact]'); const href = link ? link.getAttribute('href') || '' : ''; const address = href.startsWith('mailto:') ? href.slice(7).split('?')[0] : ''; return address ? `mailto:${address}?subject=${encodeURIComponent(subject)}` : '#contact'; }; const VERIFY = [ ['tx-sys', 'Running verifier in a fresh sandbox…'], ['tx-ok', '✓ All checks passed'], ]; class Replayer { constructor(screen, lines) { this.screen = screen; this.code = $('code', screen) || screen; this.lines = lines; this.timer = 0; this.running = false; this.caret = document.createElement('i'); this.caret.className = 'tx-caret'; this.reset(); } reset() { this.code.replaceChildren(); this.line = 0; this.char = 0; this.phase = 'type'; this.verified = 0; this.text = null; } prompt(symbol) { const span = document.createElement('span'); span.className = 'tx-cmd'; const b = document.createElement('b'); b.textContent = symbol; const text = document.createTextNode(''); span.append(b, ' ', text); this.code.append(span); return { span, text }; } status(cls, message) { const span = document.createElement('span'); span.className = cls; span.textContent = message; this.code.append(span); } renderStatic() { this.stop(); this.reset(); this.lines.forEach(([symbol, line]) => { this.prompt(symbol).text.data = line; }); VERIFY.forEach(([cls, message]) => this.status(cls, message)); } start(delay = 0) { if (this.running) return; this.running = true; this.schedule(delay); } stop() { this.running = false; clearTimeout(this.timer); } schedule(ms) { clearTimeout(this.timer); if (this.running) this.timer = setTimeout(() => this.tick(), ms); } scroll() { this.screen.scrollTop = this.screen.scrollHeight; } tick() { if (!this.running) return; if (this.phase === 'type') { if (this.line >= this.lines.length) { this.caret.remove(); this.phase = 'verify'; this.schedule(420); return; } const [symbol, line] = this.lines[this.line]; if (!this.text) { const { span, text } = this.prompt(symbol); span.append(this.caret); this.text = text; this.scroll(); this.schedule(160); return; } this.char = Math.min(line.length, this.char + Math.max(1, Math.ceil(line.length / 40))); this.text.data = line.slice(0, this.char); if (this.char >= line.length) { this.line += 1; this.char = 0; this.text = null; this.scroll(); this.schedule(300); return; } this.schedule(22); return; } if (this.phase === 'verify') { if (this.verified >= VERIFY.length) { this.prompt('$').span.append(this.caret); this.scroll(); this.phase = 'hold'; this.schedule(3200); return; } const [cls, message] = VERIFY[this.verified]; this.verified += 1; this.status(cls, message); this.scroll(); this.schedule(this.verified === 1 ? 1000 : 200); return; } this.reset(); this.schedule(260); } } function initCards(tasks) { const grid = $('#tx-grid'); if (!grid) return; const cards = $$('.tx-card', grid); const players = cards.map((card) => { const task = tasks[Number(card.dataset.index)]; return task ? new Replayer($('.tx-screen', card), task.replay) : null; }); if (reduceMotion || !('IntersectionObserver' in window)) { players.forEach((p) => p && p.renderStatic()); } else { const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { const i = cards.indexOf(entry.target); const player = players[i]; if (!player) return; if (entry.isIntersecting) { player.start(player.started ? 0 : (i % 3) * 450); player.started = true; } else { player.stop(); } }); }, { threshold: 0.25 }); cards.forEach((card) => observer.observe(card)); } const more = $('#tx-more'); const extras = cards.filter((card) => card.classList.contains('is-extra')); if (more) { if (!extras.length) more.hidden = true; more.textContent = `Show ${extras.length} more terminal tasks`; more.addEventListener('click', () => { grid.classList.add('show-all'); more.setAttribute('aria-expanded', 'true'); if (extras[0]) extras[0].focus({ preventScroll: true }); }); } return cards; } function initModal(tasks, cards) { const modal = $('#tx-modal'); if (!modal || !cards) return; const sheet = $('.ex-sheet', modal); const closeButton = $('.icon-btn[data-tx-close]', modal); const field = (id) => document.getElementById(id); const f = { area: field('txm-area'), title: field('txm-title'), tags: field('txm-tags'), instruction: field('txm-instruction'), base: field('txm-base'), termBase: field('txm-term-base'), lines: field('txm-lines'), replay: field('txm-replay'), code: field('txm-code'), cta: field('txm-cta'), }; let current = 0; let lastFocus = null; let player = null; const play = () => { if (player) player.stop(); player = new Replayer(f.replay, tasks[current].replay); if (reduceMotion) player.renderStatic(); else player.start(250); }; const render = (index) => { current = (index + tasks.length) % tasks.length; const t = tasks[current]; f.area.textContent = t.area; f.title.textContent = t.title; f.tags.replaceChildren(...t.tags.map((tag) => { const li = document.createElement('li'); li.className = 'trk trk-mod'; li.textContent = tag; return li; })); f.instruction.innerHTML = t.instruction; f.base.textContent = t.base; f.termBase.textContent = t.base; f.lines.textContent = `${t.lines}-line solve.sh`; f.code.textContent = t.code; f.code.scrollTop = 0; f.cta.href = contactHref(`Terminal task sample: ${t.title}`); sheet.scrollTop = 0; if (!modal.hidden) play(); }; const open = (index) => { lastFocus = document.activeElement; modal.hidden = false; render(index); document.body.classList.add('modal-open'); closeButton.focus(); }; const close = () => { modal.hidden = true; if (player) player.stop(); document.body.classList.remove('modal-open'); if (lastFocus) lastFocus.focus({ preventScroll: true }); }; cards.forEach((card) => { const index = Number(card.dataset.index); card.addEventListener('click', () => open(index)); card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(index); } }); }); $$('[data-tx-close]', modal).forEach((el) => el.addEventListener('click', close)); $('[data-tx-prev]', modal).addEventListener('click', () => render(current - 1)); $('[data-tx-next]', modal).addEventListener('click', () => render(current + 1)); document.addEventListener('keydown', (e) => { if (modal.hidden) return; if (e.key === 'Escape') { e.preventDefault(); close(); } else if (e.key === 'ArrowRight') { render(current + 1); } else if (e.key === 'ArrowLeft') { render(current - 1); } else if (e.key === 'Tab') { const focusables = $$('button, a[href]', sheet); const first = focusables[0]; const last = focusables[focusables.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } }); } function initGrowth(growth) { const root = $('#growth'); if (!root || !growth || !growth.stages || !growth.stages.length) return; const stages = growth.stages; const steps = $('#growth-steps'); const instruction = $('#growth-instruction'); const linesEl = $('#growth-lines'); const wordsEl = $('#growth-words'); const linesBar = $('#growth-lines-bar'); const wordsBar = $('#growth-words-bar'); const spark = $('#growth-spark'); const maxLines = Math.max(...stages.map((s) => s.lines)); const maxWords = Math.max(...stages.map((s) => s.words)); const svgNS = 'http://www.w3.org/2000/svg'; const W = 240; const H = 72; const P = 6; const xs = stages.map((_, i) => P + (i * (W - 2 * P)) / Math.max(1, stages.length - 1)); const ys = stages.map((s) => H - P - ((H - 2 * P) * s.lines) / maxLines); const pts = xs.map((x, i) => `${x.toFixed(1)},${ys[i].toFixed(1)}`).join(' '); const make = (tag, attrs) => { const node = document.createElementNS(svgNS, tag); Object.entries(attrs).forEach(([k, v]) => node.setAttribute(k, v)); spark.append(node); return node; }; spark.replaceChildren(); make('polygon', { class: 'gs-area', points: `${P},${H - P} ${pts} ${W - P},${H - P}` }); make('polyline', { class: 'gs-line', points: pts }); const guide = make('line', { class: 'gs-guide', x1: 0, x2: 0, y1: 2, y2: H - 2 }); const dots = xs.map((x, i) => make('circle', { class: 'gs-dot', cx: x.toFixed(1), cy: ys[i].toFixed(1), r: 3 })); let index = 0; let shown = { lines: 0, words: 0 }; let tween = 0; const countTo = (target) => { cancelAnimationFrame(tween); const from = { ...shown }; const t0 = performance.now(); const step = (now) => { const k = reduceMotion ? 1 : Math.min(1, (now - t0) / 600); const e = 1 - (1 - k) ** 3; shown = { lines: Math.round(from.lines + (target.lines - from.lines) * e), words: Math.round(from.words + (target.words - from.words) * e), }; linesEl.textContent = `${shown.lines} lines`; wordsEl.textContent = `${shown.words} words`; if (k < 1) tween = requestAnimationFrame(step); }; tween = requestAnimationFrame(step); }; const buttons = stages.map((_, i) => { const b = document.createElement('button'); b.type = 'button'; b.className = 'gstep'; b.setAttribute('aria-label', `Stage ${i + 1} of ${stages.length}`); b.innerHTML = `Stage${i + 1}`; b.addEventListener('click', () => { setAuto(false); show(i); }); return b; }); const toggle = document.createElement('button'); toggle.type = 'button'; toggle.className = 'gplay'; steps.replaceChildren(...buttons, toggle); const show = (i) => { index = i; const stage = stages[i]; buttons.forEach((b, k) => { b.setAttribute('aria-pressed', String(k === i)); b.classList.toggle('is-done', k < i); }); const text = document.createElement('p'); text.className = 'g-text'; text.innerHTML = stage.segments.map((s) => (s.new ? `${s.html}` : s.html)).join(' '); instruction.replaceChildren(text); linesBar.style.width = `${((100 * stage.lines) / maxLines).toFixed(1)}%`; wordsBar.style.width = `${((100 * stage.words) / maxWords).toFixed(1)}%`; countTo(stage); dots.forEach((d, k) => { d.classList.toggle('is-on', k <= i); d.setAttribute('r', k === i ? 5 : 3); }); guide.style.transform = `translateX(${xs[i].toFixed(1)}px)`; }; let auto = !reduceMotion; let visible = false; let timer = 0; const schedule = () => { clearTimeout(timer); if (!auto || !visible) return; timer = setTimeout(() => { show(index === stages.length - 1 ? 0 : index + 1); schedule(); }, index === stages.length - 1 ? 6000 : 4200); }; const setAuto = (on) => { auto = on; toggle.textContent = on ? 'Pause' : 'Play'; toggle.setAttribute('aria-label', on ? 'Pause stage autoplay' : 'Play stages automatically'); schedule(); }; toggle.addEventListener('click', () => setAuto(!auto)); show(0); setAuto(auto); if ('IntersectionObserver' in window) { new IntersectionObserver((entries) => { visible = entries[0].isIntersecting; schedule(); }, { threshold: 0.35 }).observe(root); } } const tasks = DATA.tasks || []; const cards = initCards(tasks); initModal(tasks, cards); initGrowth(DATA.growth); })();