// 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 (
{String(index + 1).padStart(2, "0")} / {String(total).padStart(2, "0")}
{[0.5, 1, 2, 3].map((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"}
{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.target.files[0] && handleFile(e.target.files[0])} />
{showPaste && (