// components.jsx — Markdown renderer, Sidebar, ReaderPane, Controls, FullMarkdown, Uploader.
// ── Markdown ────────────────────────────────────────────────────────────
function Markdown({ text, className }) {
const ref = React.useRef(null);
React.useEffect(() => {
if (!ref.current) return;
const html = window.marked ? window.marked.parse(text || "") : (text || "");
ref.current.innerHTML = html;
if (window.hljs) {
ref.current.querySelectorAll("pre code").forEach((block) => {
// hljs has no qml grammar — alias to javascript for sensible coloring
block.className = block.className.replace("language-qml", "language-javascript");
try { window.hljs.highlightElement(block); } catch (e) {}
});
}
}, [text]);
return
;
}
// ── Sidebar outline ─────────────────────────────────────────────────────
function Sidebar({ spec, outline, currentStop, onJump, stats, perStop }) {
return (
);
}
// ── Reader pane (left) ──────────────────────────────────────────────────
function ReaderPane({ stop, spec, platform, release }) {
if (stop.kind === "intro") {
return (
EXECUTABLE TUTORIAL
{spec.name}
{spec.intro &&
}
{spec.what_you_build && (
What you'll build
{spec.what_you_build}
)}
{Array.isArray(spec.what_you_learn) && spec.what_you_learn.length > 0 && (
What you'll learn
{spec.what_you_learn.map((x, i) => (
))}
)}
{Array.isArray(spec.prerequisites) && spec.prerequisites.length > 0 && (
Prerequisites
{spec.prerequisites.map((x, i) => (
))}
)}
);
}
const heading = stop.section.title;
const md = window.TutorialParser.stopMarkdown(stop, platform, release);
const showSectionLead = stop.kind === "step" && stop.first && stop.section.text;
return (
{stop.stepNo != null && STEP {stop.stepNo} }
{heading}
{stop.kind === "step" && !stop.first && · continued }
{showSectionLead && (
)}
);
}
// ── Bottom control bar ──────────────────────────────────────────────────
function Controls({
index, total, onPrev, onNext, playing, onTogglePlay, speed, onSpeed,
platform, onPlatform, onRestart, release,
}) {
return (
⟲
←
{playing ? "❚❚" : "▶"}
→
{String(index + 1).padStart(2, "0")} / {String(total).padStart(2, "0")}
onPlatform("linux")}>Linux
onPlatform("macos")}>macOS
{[0.5, 1, 2, 3].map((s) => (
onSpeed(s)}>
{s}×
))}
);
}
// ── Full generated markdown overlay ─────────────────────────────────────
function FullMarkdown({ spec, platform, release, onClose }) {
const md = window.TutorialParser.generateMarkdown(spec, platform, release);
const [view, setView] = React.useState("rendered");
return (
⌁ Generated markdown
{spec.output || "tutorial.md"}
setView("rendered")}>Rendered
setView("source")}>Source
navigator.clipboard && navigator.clipboard.writeText(md)}>Copy
✕
{view === "rendered" ?
:
{md} }
);
}
// ── Uploader / landing ──────────────────────────────────────────────────
function Uploader({ onLoad, error }) {
const [drag, setDrag] = React.useState(false);
const [showPaste, setShowPaste] = React.useState(false);
const [paste, setPaste] = React.useState("");
const fileRef = React.useRef(null);
function handleFile(file) {
const reader = new FileReader();
reader.onload = (e) => onLoad(e.target.result, file.name);
reader.readAsText(file);
}
return (
▸ tutorial_runner · player
Executable Tutorial Player
Drop a .test.yaml spec and step through it — reading the rendered tutorial on the left
while watching every command, file write, and assertion execute on the right.
{ e.preventDefault(); setDrag(true); }}
onDragLeave={() => setDrag(false)}
onDrop={(e) => {
e.preventDefault();
setDrag(false);
if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
}}
onClick={() => fileRef.current && fileRef.current.click()}
>
⤓
Drop your .test.yaml here
or click to browse · { e.stopPropagation(); setShowPaste((s) => !s); }}>paste YAML
e.target.files[0] && handleFile(e.target.files[0])}
/>
{showPaste && (
)}
{error &&
⚠ {error}
}
Or load an example
{Object.entries(window.EXAMPLES).map(([key, ex]) => (
onLoad(ex.yaml, ex.filename)}>
{ex.filename}
))}
);
}
function ExampleMeta({ yaml }) {
let title = "", stats = null;
try {
const spec = window.TutorialParser.parse(yaml);
title = spec.name;
stats = window.TutorialParser.specStats(spec);
} catch (e) {}
return (
{title}
{stats && (
{stats.stepSections} steps · {stats.cmds} cmds · {stats.files} files · {stats.asserts} checks
)}
);
}
Object.assign(window, { Markdown, Sidebar, ReaderPane, Controls, FullMarkdown, Uploader });