<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TLA+ demo walkthrough</title>
<style>
:root {
--bg: #0f1115; --panel: #171a21; --line: #2a2f3a; --fg: #e6e9ef;
--dim: #8b93a3; --accent: #6ea8fe; --ok: #4ec98a; --bad: #ff6b6b;
--chg: #3a2f12; --chgline: #d8a534;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--fg);
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
header { padding: 16px 22px; border-bottom: 1px solid var(--line); }
header h1 { margin: 0; font-size: 19px; font-weight: 650; }
nav#beat-list { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 22px; border-bottom: 1px solid var(--line); }
.beat-tab {
background: var(--panel); color: var(--fg); border: 1px solid var(--line);
border-radius: 7px; padding: 7px 12px; cursor: pointer; font-size: 13px;
}
.beat-tab:hover { border-color: var(--accent); }
.beat-tab.active { border-color: var(--accent); background: #1d2330; }
.beat-tab.fail { border-left: 3px solid var(--bad); }
.beat-tab.pass { border-left: 3px solid var(--ok); }
main { padding: 20px 22px 60px; max-width: 1100px; }
main h2 { margin: 4px 0 6px; font-size: 18px; }
.note { color: var(--dim); margin: 0 0 16px; max-width: 760px; }
.scenario { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; margin-bottom: 14px; }
.sline { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; color: var(--dim); padding: 2px 6px; border-radius: 4px; }
.sline.cur { color: var(--fg); background: #1d2330; }
.steps { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.steps button { background: var(--panel); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 6px 12px; cursor: pointer; }
.steps button:hover { border-color: var(--accent); }
.steps .pos { color: var(--dim); font-variant-numeric: tabular-nums; }
.cols { display: flex; gap: 16px; flex-wrap: wrap; }
.col { flex: 1 1 280px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; min-width: 260px; }
.vhead { font-weight: 650; margin-bottom: 4px; }
.label { color: var(--accent); font-family: ui-monospace, monospace; font-size: 13px; margin-bottom: 8px; }
table.state { width: 100%; border-collapse: collapse; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
table.state td { padding: 3px 6px; border-bottom: 1px solid var(--line); vertical-align: top; }
table.state .vn { color: var(--dim); white-space: nowrap; }
table.state .vv { text-align: right; word-break: break-all; }
table.state tr.chg { background: var(--chg); }
table.state tr.chg .vv { color: var(--chgline); font-weight: 650; }
ul.asserts { list-style: none; margin: 12px 0 0; padding: 0; font-size: 13px; }
ul.asserts li { padding: 2px 0; }
code { font-family: ui-monospace, monospace; background: #1d2330; padding: 1px 5px; border-radius: 4px; }
.ok { color: var(--ok); }
.bad { color: var(--bad); }
footer { position: fixed; bottom: 0; left: 0; right: 0; padding: 8px 22px; background: var(--panel); border-top: 1px solid var(--line); color: var(--dim); font-size: 12px; }
</style>
</head>
<body>
<header><h1 id="demo-title"></h1></header>
<nav id="beat-list"></nav>
<main id="beat-view"></main>
<footer>← / → step · [ / ] beat · click a beat above</footer>
<script>
const DATA = ;
let beatIdx = 0, step = 0;
function esc(s) {
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function maxStep(beat) {
return Math.max(0, ...beat.runs.map(r => r.trace.length - 1));
}
function badge(passed) {
return passed ? '<span class="ok">✓</span>' : '<span class="bad">✗</span>';
}
function renderBeatList() {
const nav = document.getElementById("beat-list");
nav.innerHTML = "";
DATA.beats.forEach((b, i) => {
const el = document.createElement("button");
el.className = "beat-tab " + (b.passed ? "pass" : "fail") + (i === beatIdx ? " active" : "");
el.textContent = (i + 1) + ". " + b.title + (b.passed ? " ✓" : " ✗");
el.onclick = () => { beatIdx = i; step = 0; render(); };
nav.appendChild(el);
});
}
function renderBeat() {
const beat = DATA.beats[beatIdx];
const top = maxStep(beat);
if (step > top) step = top;
let h = "";
h += "<h2>" + esc(beat.title) + " " + badge(beat.passed) + "</h2>";
if (beat.note) h += '<p class="note">' + esc(beat.note) + "</p>";
h += '<div class="scenario">';
beat.scenario.forEach((line, k) => {
h += '<div class="sline' + (k + 1 === step ? " cur" : "") + '">' + esc(line) + "</div>";
});
h += "</div>";
h += '<div class="steps">';
h += '<button id="prev">‹ prev</button>';
h += '<span class="pos">step ' + step + " / " + top + "</span>";
h += '<button id="next">next ›</button>';
h += "</div>";
h += '<div class="cols">';
beat.runs.forEach(run => {
h += '<div class="col">';
h += '<div class="vhead">' + esc(run.variant) + " " + badge(run.passed) + "</div>";
if (run.failure) h += '<div class="bad">error: ' + esc(run.failure) + "</div>";
const cur = run.trace[Math.min(step, run.trace.length - 1)];
const prev = step > 0 ? run.trace[Math.min(step - 1, run.trace.length - 1)] : null;
h += '<div class="label">' + esc(cur.label) + "</div>";
h += '<table class="state">';
run.vars.forEach(v => {
const val = cur.vars[v] !== undefined ? cur.vars[v] : "";
const changed = prev && prev.vars[v] !== val;
h += "<tr class=\"" + (changed ? "chg" : "") + "\">";
h += '<td class="vn">' + esc(v) + "</td>";
h += '<td class="vv">' + esc(val) + "</td></tr>";
});
h += "</table>";
if (run.assertions.length) {
h += '<ul class="asserts">';
run.assertions.forEach(a => {
let line = (a.passed ? "✓" : "✗") + " <code>" + esc(a.expectation) + "</code>";
if (!a.passed && a.detail) line += " — " + esc(a.detail);
h += '<li class="' + (a.passed ? "ok" : "bad") + '">' + line + "</li>";
});
h += "</ul>";
}
h += "</div>";
});
h += "</div>";
const view = document.getElementById("beat-view");
view.innerHTML = h;
document.getElementById("prev").onclick = () => { if (step > 0) { step--; render(); } };
document.getElementById("next").onclick = () => { if (step < maxStep(beat)) { step++; render(); } };
}
function render() {
document.getElementById("demo-title").textContent = DATA.title;
document.title = DATA.title;
renderBeatList();
renderBeat();
}
document.addEventListener("keydown", e => {
const beat = DATA.beats[beatIdx];
if (e.key === "ArrowRight") { if (step < maxStep(beat)) { step++; render(); } }
else if (e.key === "ArrowLeft") { if (step > 0) { step--; render(); } }
else if (e.key === "]") { beatIdx = (beatIdx + 1) % DATA.beats.length; step = 0; render(); }
else if (e.key === "[") { beatIdx = (beatIdx - 1 + DATA.beats.length) % DATA.beats.length; step = 0; render(); }
});
render();
</script>
</body>
</html>