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