/** * Episteme Presentation Renderer * * Data-driven slide rendering with multi-deck support. * * Modes: * flat - single deck, slides left/right (default Reveal.js) * combined - menu column (↓ browse, → enter), decks are vertical (↓↑ navigate, ← back) * * Slide types are registered in RENDERERS. To add a new type, add one function. */ (async function () { const dataUrl = window.PRESENTATION_DATA_URL || '/generated/stemedb.json'; let data; try { const resp = await fetch(dataUrl); data = await resp.json(); } catch (err) { document.getElementById('slides').innerHTML = `

Error Loading Presentation

Could not load ${dataUrl}

Run: npm run generate:all

`; Reveal.initialize(); return; } const root = document.getElementById('slides'); const isCombined = !!data.combined; // ── Utilities ────────────────────────────────────────────── function esc(text) { const el = document.createElement('div'); el.textContent = text; return el.innerHTML; } function actorInitials(actor) { if (!actor?.label) return '??'; return actor.label.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2); } function fragmentList(items, cls) { return ``; } // ── Slide-Type Registry ──────────────────────────────────── // // Each renderer receives (slide, ctx) and returns { css?, attrs?, html }. // ctx = { actors, sectionNum } const RENDERERS = { title() { return { css: 'title-slide', html: '

stemedb

', }; }, hook(slide) { return { css: 'hook-slide', html: `

${slide.line}

${slide.subline ? `

${slide.subline}

` : ''}
`, }; }, code(slide) { return { css: 'code-slide', html: `

${slide.title}

${esc(slide.code.trim())}
`, }; }, problem(slide) { return { css: 'problem-slide', html: `

${slide.title}

${fragmentList(slide.points, 'problem-points')}
`, }; }, insight(slide) { return { css: 'insight-slide', html: `

${slide.title}

${fragmentList(slide.points, 'insight-points')}
`, }; }, vision(slide) { return { css: 'vision-slide', html: `

${slide.title}

${fragmentList(slide.points, 'vision-points')} ${slide.tagline ? `

${slide.tagline}

` : ''}
`, }; }, 'section-title'(slide, ctx) { ctx.sectionNum++; return { html: `
Section ${ctx.sectionNum}

${slide.title}

${slide.subtitle}

${slide.description.trim()}

`, }; }, contrast(slide) { return { css: 'contrast-slide', html: `

${slide.title}

${slide.before.title}

    ${slide.before.points.map(p => `
  • ${p}
  • `).join('')}

${slide.after.title}

    ${slide.after.points.map(p => `
  • ${p}
  • `).join('')}
`, }; }, sequence(slide, ctx) { return { attrs: { 'data-auto-animate': '' }, html: renderSequence(slide, ctx.actors), }; }, }; // ── Section Factory ──────────────────────────────────────── function createSection(slide, ctx) { const render = RENDERERS[slide.type]; if (!render) return null; const { css, attrs, html } = render(slide, ctx); const el = document.createElement('section'); if (css) el.classList.add(css); if (attrs) Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v)); el.innerHTML = html; return el; } // ── Build: Flat (single deck) ────────────────────────────── function buildFlat(slides, actors) { const ctx = { actors: actors || {}, sectionNum: 0 }; for (const slide of slides) { const el = createSection(slide, ctx); if (el) root.appendChild(el); } } // ── Build: Combined (multi-deck) ────────────────────────── function buildCombined(decks) { // Column 0: vertical menu stack const menu = document.createElement('section'); // Title (reuses the registered title renderer — single source of truth) menu.appendChild(createSection({ type: 'title' }, { actors: {}, sectionNum: 0 })); // Deck entry cards decks.forEach((deck, i) => { const card = document.createElement('section'); card.classList.add('menu-slide'); card.innerHTML = `
${String(i + 1).padStart(2, '0')}

${deck.meta.title}

${deck.meta.subtitle ? `` : ''}
`; menu.appendChild(card); }); root.appendChild(menu); // Columns 1-N: each deck as a horizontal section wrapping vertical slides for (const deck of decks) { const col = document.createElement('section'); const ctx = { actors: deck.actors || {}, sectionNum: 0 }; for (const slide of deck.slides) { if (slide.type === 'title') continue; // menu is the landing const el = createSection(slide, ctx); if (el) col.appendChild(el); } root.appendChild(col); } } // ── Sequence Slide (complex layout, kept separate) ───────── function renderSequence(slide, allActors) { const actors = slide.actors.map(id => typeof id === 'string' ? allActors[id] || { id, label: id, color: '#666666' } : id ); const lookup = {}; actors.forEach(a => { lookup[a.id] = a; const key = Object.keys(allActors).find(k => allActors[k].id === a.id); if (key) lookup[key] = a; }); const actorsHtml = actors.map(a => `
${actorInitials(a)}
${a.label}
`).join(''); const stepsHtml = slide.steps.map((step, i) => { const cls = step.danger ? 'danger' : step.success ? 'success' : step.warning ? 'warning' : ''; const from = lookup[step.from] || allActors[step.from] || { label: step.from }; const to = lookup[step.to] || allActors[step.to] || { label: step.to }; return `
${i + 1}
${from.label} ${to.label}
${step.action}
${step.label}
${step.note ? `
${step.note}
` : ''} ${step.callout ? `
${step.callout}
` : ''}
`; }).join(''); return `

${slide.title}

${actorsHtml}
${stepsHtml}
`; } // ── Keyboard: Combined Mode ──────────────────────────────── // // Menu (h=0): ↓↑ browse cards, → enter deck // Deck (h>0): ↓↑ navigate slides, ← back to menu // → also advances (alias for ↓) so arrow-mashing works function combinedKeys() { return { // → : menu → enter deck; inside deck → next slide 39: function () { const { h, v } = Reveal.getIndices(); if (h === 0) { Reveal.slide(v > 0 ? v : 1, 0); } else { Reveal.down(); } }, // ← : first slide of deck → back to its menu card; deeper → prev slide 37: function () { const { h, v } = Reveal.getIndices(); if (h > 0 && v === 0) { Reveal.slide(0, h); } else if (h > 0) { Reveal.up(); } }, }; } // ── Render & Initialize ──────────────────────────────────── if (isCombined) { buildCombined(data.decks); } else { buildFlat(data.slides, data.actors || {}); } Reveal.initialize({ hash: true, history: true, controls: true, progress: true, center: true, transition: 'fade', transitionSpeed: 'fast', keyboard: isCombined ? combinedKeys() : { 39: 'next', 37: 'prev' }, }); Reveal.on('fragmentshown', e => e.fragment.classList.add('visible')); Reveal.on('fragmenthidden', e => e.fragment.classList.remove('visible')); })();