stagecraft 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/AGENT.md +792 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -0
  4. package/bin/cli.js +51 -0
  5. package/bin/export.js +137 -0
  6. package/bin/init.js +52 -0
  7. package/bin/lib/edit-ops.js +405 -0
  8. package/bin/serve.js +278 -0
  9. package/dist/stagecraft.bundle.css +4443 -0
  10. package/dist/stagecraft.bundle.js +7621 -0
  11. package/dist/themes/brand.bundle.css +5262 -0
  12. package/dist/themes/neon.bundle.css +5289 -0
  13. package/dist/themes/paper.bundle.css +5276 -0
  14. package/dist/themes/phosphor.bundle.css +4443 -0
  15. package/dist/themes/shopware.bundle.css +5850 -0
  16. package/examples/closing-card.js +74 -0
  17. package/examples/orchestration-graph.js +156 -0
  18. package/examples/terminal-log.js +109 -0
  19. package/examples/token-stream.js +96 -0
  20. package/examples/whoami.js +90 -0
  21. package/package.json +41 -0
  22. package/src/components/activity-list.js +75 -0
  23. package/src/components/agenda.js +79 -0
  24. package/src/components/bar-chart.js +162 -0
  25. package/src/components/before-after.js +135 -0
  26. package/src/components/bento.js +73 -0
  27. package/src/components/big-number.js +87 -0
  28. package/src/components/callout.js +75 -0
  29. package/src/components/checklist.js +81 -0
  30. package/src/components/code-block.js +141 -0
  31. package/src/components/code-diff.js +98 -0
  32. package/src/components/compare.js +85 -0
  33. package/src/components/counter.js +80 -0
  34. package/src/components/cta.js +69 -0
  35. package/src/components/cycle.js +146 -0
  36. package/src/components/definition.js +96 -0
  37. package/src/components/donut-chart.js +179 -0
  38. package/src/components/full-image.js +82 -0
  39. package/src/components/funnel.js +111 -0
  40. package/src/components/gauge.js +147 -0
  41. package/src/components/heatmap.js +141 -0
  42. package/src/components/image-grid.js +80 -0
  43. package/src/components/image-text.js +96 -0
  44. package/src/components/kinetic-text.js +72 -0
  45. package/src/components/kpi.js +106 -0
  46. package/src/components/line-chart.js +215 -0
  47. package/src/components/manifesto.js +104 -0
  48. package/src/components/marquee.js +63 -0
  49. package/src/components/matrix2x2.js +151 -0
  50. package/src/components/pillars.js +80 -0
  51. package/src/components/pricing.js +90 -0
  52. package/src/components/process-flow.js +133 -0
  53. package/src/components/progress.js +136 -0
  54. package/src/components/punchline.js +82 -0
  55. package/src/components/pyramid.js +107 -0
  56. package/src/components/qanda.js +60 -0
  57. package/src/components/quote.js +70 -0
  58. package/src/components/roadmap.js +130 -0
  59. package/src/components/section-card.js +45 -0
  60. package/src/components/shift-arrow.js +41 -0
  61. package/src/components/spark-line.js +147 -0
  62. package/src/components/spotlight.js +85 -0
  63. package/src/components/statement.js +106 -0
  64. package/src/components/stats.js +91 -0
  65. package/src/components/steps.js +83 -0
  66. package/src/components/swot.js +110 -0
  67. package/src/components/team-grid.js +87 -0
  68. package/src/components/testimonial.js +99 -0
  69. package/src/components/timeline.js +91 -0
  70. package/src/components/tip.js +63 -0
  71. package/src/components/venn.js +198 -0
  72. package/src/edit-mode.js +1256 -0
  73. package/src/engine.js +823 -0
  74. package/src/helpers.js +169 -0
  75. package/src/transitions.js +101 -0
  76. package/starter/index.html +40 -0
  77. package/starter/slides/00-title.js +12 -0
  78. package/starter/stagecraft.config.js +8 -0
  79. package/themes/brand/base.css +4 -0
  80. package/themes/brand/components-business.css +173 -0
  81. package/themes/brand/components-chart.css +65 -0
  82. package/themes/brand/components-content.css +126 -0
  83. package/themes/brand/components-data.css +162 -0
  84. package/themes/brand/components-diagram.css +115 -0
  85. package/themes/brand/components-layout.css +112 -0
  86. package/themes/brand/components.css +46 -0
  87. package/themes/brand/manifest.json +20 -0
  88. package/themes/brand/tokens.css +20 -0
  89. package/themes/brand/transitions.css +4 -0
  90. package/themes/neon/base.css +10 -0
  91. package/themes/neon/components-business.css +189 -0
  92. package/themes/neon/components-chart.css +70 -0
  93. package/themes/neon/components-content.css +112 -0
  94. package/themes/neon/components-data.css +160 -0
  95. package/themes/neon/components-diagram.css +109 -0
  96. package/themes/neon/components-layout.css +87 -0
  97. package/themes/neon/components.css +87 -0
  98. package/themes/neon/manifest.json +21 -0
  99. package/themes/neon/tokens.css +17 -0
  100. package/themes/neon/transitions.css +13 -0
  101. package/themes/paper/base.css +9 -0
  102. package/themes/paper/components-business.css +196 -0
  103. package/themes/paper/components-chart.css +74 -0
  104. package/themes/paper/components-content.css +108 -0
  105. package/themes/paper/components-data.css +168 -0
  106. package/themes/paper/components-diagram.css +89 -0
  107. package/themes/paper/components-layout.css +105 -0
  108. package/themes/paper/components.css +60 -0
  109. package/themes/paper/manifest.json +10 -0
  110. package/themes/paper/tokens.css +21 -0
  111. package/themes/paper/transitions.css +11 -0
  112. package/themes/phosphor/base.css +511 -0
  113. package/themes/phosphor/components-business.css +818 -0
  114. package/themes/phosphor/components-chart.css +415 -0
  115. package/themes/phosphor/components-content.css +530 -0
  116. package/themes/phosphor/components-data.css +824 -0
  117. package/themes/phosphor/components-diagram.css +427 -0
  118. package/themes/phosphor/components-layout.css +450 -0
  119. package/themes/phosphor/components.css +223 -0
  120. package/themes/phosphor/manifest.json +11 -0
  121. package/themes/phosphor/tokens.css +17 -0
  122. package/themes/phosphor/transitions.css +213 -0
  123. package/themes/shopware/base.css +94 -0
  124. package/themes/shopware/components-business.css +344 -0
  125. package/themes/shopware/components-chart.css +121 -0
  126. package/themes/shopware/components-content.css +169 -0
  127. package/themes/shopware/components-data.css +219 -0
  128. package/themes/shopware/components-diagram.css +129 -0
  129. package/themes/shopware/components-layout.css +166 -0
  130. package/themes/shopware/components.css +83 -0
  131. package/themes/shopware/manifest.json +21 -0
  132. package/themes/shopware/tokens.css +68 -0
  133. package/themes/shopware/transitions.css +22 -0
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cookbook · Closing Card
5
+ * -----------------------
6
+ * Technique: a staggered-reveal closing slide with a live-rendered QR
7
+ * code, theme-matched, with a graceful URL-text fallback if the CDN
8
+ * dependency fails to load.
9
+ *
10
+ * Dependency:
11
+ * This slide expects `qrcode-generator` (global `qrcode` function) to be
12
+ * loaded ahead of it. Add to your index.html:
13
+ * <script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
14
+ * The slide expects a URL at `Stage.CLOSING_URL` (set it from your config
15
+ * or inline before the deck boots).
16
+ *
17
+ * What to copy:
18
+ * - The fallback-first DOM: the markup ships with the URL rendered as
19
+ * text inside `#qrFrame`. If the QR library loads, `renderQR` replaces
20
+ * that content with an inline SVG. If not, the text remains — no
21
+ * "broken image" state, no error UI.
22
+ * - String-rewriting the QR library's SVG (`fill="black"` → theme color)
23
+ * instead of restyling via CSS — `qrcode-generator` emits raw SVG, so
24
+ * a regex replace is the most direct path to a theme-matched code.
25
+ * - `Stage.staggerIn(el.querySelectorAll('.stagger > *'), 180)` for a
26
+ * soft sequential reveal. Pair with `.stagger > *` CSS in your theme
27
+ * (initial opacity 0; `.in` class triggers transition).
28
+ * - The `<div class="thanks-underline">` is intentionally empty — pure
29
+ * CSS decoration to ground the headline.
30
+ */
31
+
32
+ function renderQR(el) {
33
+ const frame = el.querySelector('#qrFrame');
34
+ if (!frame) return;
35
+ if (typeof qrcode !== 'function') return; // CDN failed — fallback stays
36
+ try {
37
+ const qr = qrcode(0, 'M');
38
+ qr.addData(Stage.CLOSING_URL);
39
+ qr.make();
40
+ const svg = qr.createSvgTag({ cellSize: 6, margin: 2, scalable: true });
41
+ const themed = svg
42
+ .replace(/fill="black"/g, 'fill="#E6E6E6"')
43
+ .replace(/fill="white"/g, 'fill="#0d0d0d"')
44
+ .replace(/<svg /, '<svg style="width:240px;height:240px;display:block;" ');
45
+ frame.innerHTML = themed;
46
+ } catch (e) {
47
+ console.warn('QR render failed', e);
48
+ }
49
+ }
50
+
51
+ Stage.register({
52
+ section: 5,
53
+ title: 'Example · Closing Card',
54
+ render(el) {
55
+ el.innerHTML = `
56
+ <div class="closing-wrap stagger">
57
+ <div class="pre-label blue"><span class="dot"></span>Continue the conversation</div>
58
+ <div class="qr-frame" id="qrFrame">
59
+ <div class="qr-fallback">${Stage.CLOSING_URL}</div>
60
+ </div>
61
+ <div class="thanks">Thank you.</div>
62
+ <div class="thanks-underline"></div>
63
+ <div class="thanks-names">
64
+ <strong>Presenter One</strong>&nbsp;&nbsp;·&nbsp;&nbsp;<strong>Presenter Two</strong>
65
+ </div>
66
+ <div class="thanks-url"><span class="accent-blue">→</span>&nbsp;&nbsp;${Stage.CLOSING_URL}</div>
67
+ </div>
68
+ `;
69
+ },
70
+ init(el) {
71
+ Stage.staggerIn(el.querySelectorAll('.stagger > *'), 180);
72
+ renderQR(el);
73
+ }
74
+ });
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cookbook · Orchestration Graph
5
+ * ------------------------------
6
+ * Technique: SVG hex graph with a center node and N satellites,
7
+ * pulses + particles flowing inward.
8
+ *
9
+ * What to copy:
10
+ * - Polar-to-cartesian satellite layout: place N items on a circle
11
+ * by mapping each `angle` (degrees) to (cos, sin) * R.
12
+ * - The `document.createElementNS('http://www.w3.org/2000/svg', ...)`
13
+ * pattern — needed for SVG nodes; plain createElement won't render.
14
+ * - Staggered fade-in with per-item `setTimeout` offsets — gives
15
+ * the graph a "build itself" feel rather than popping in.
16
+ * - `Stage.emitParticle(parent, x1, y1, x2, y2, dur)` flowing
17
+ * satellite → center; combine with a transient `.active` class on
18
+ * the source node for a synchronized pulse.
19
+ * - Combined cleanup: track both setInterval AND setTimeout in one
20
+ * `intervals` array — `clearTimeout`/`clearInterval` are interchangeable
21
+ * in browsers for IDs from either, so a single cleanup loop works.
22
+ */
23
+
24
+ function playOrchestrationGraph(el) {
25
+ const nodesG = el.querySelector('#nodes');
26
+ const edgesG = el.querySelector('#edges');
27
+ const particlesG = el.querySelector('#particles');
28
+ if (!nodesG) return () => {};
29
+ nodesG.innerHTML = '';
30
+ edgesG.innerHTML = '';
31
+ particlesG.innerHTML = '';
32
+
33
+ const satellites = [
34
+ { id: 'n1', label: 'Node 1', angle: -90 },
35
+ { id: 'n2', label: 'Node 2', angle: -30 },
36
+ { id: 'n3', label: 'Node 3', angle: 30 },
37
+ { id: 'n4', label: 'Node 4', angle: 90 },
38
+ { id: 'n5', label: 'Node 5', angle: 150 },
39
+ { id: 'n6', label: 'Node 6', angle: -150 },
40
+ ];
41
+ const R = 150;
42
+ const nodeR = 40;
43
+
44
+ const positions = { center: { x: 0, y: 0 } };
45
+ satellites.forEach(s => {
46
+ const rad = s.angle * Math.PI / 180;
47
+ positions[s.id] = { x: Math.cos(rad) * R, y: Math.sin(rad) * R };
48
+ });
49
+
50
+ // edges
51
+ satellites.forEach(s => {
52
+ const p = positions[s.id];
53
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
54
+ line.setAttribute('class', 'edge');
55
+ line.setAttribute('x1', 0);
56
+ line.setAttribute('y1', 0);
57
+ line.setAttribute('x2', p.x);
58
+ line.setAttribute('y2', p.y);
59
+ edgesG.appendChild(line);
60
+ });
61
+
62
+ // satellites
63
+ satellites.forEach(s => {
64
+ const p = positions[s.id];
65
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
66
+ g.setAttribute('transform', `translate(${p.x}, ${p.y})`);
67
+ g.setAttribute('data-id', s.id);
68
+
69
+ const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
70
+ c.setAttribute('class', 'node-circle');
71
+ c.setAttribute('r', nodeR);
72
+ g.appendChild(c);
73
+
74
+ const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
75
+ t.setAttribute('class', 'node-label');
76
+ t.textContent = s.label;
77
+ g.appendChild(t);
78
+
79
+ nodesG.appendChild(g);
80
+ });
81
+
82
+ // center node
83
+ {
84
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
85
+ const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
86
+ c.setAttribute('class', 'node-circle center');
87
+ c.setAttribute('r', 52);
88
+ g.appendChild(c);
89
+ const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
90
+ t.setAttribute('class', 'node-label center');
91
+ t.textContent = 'CORE';
92
+ g.appendChild(t);
93
+ nodesG.appendChild(g);
94
+ }
95
+
96
+ let cancelled = false;
97
+ const intervals = [];
98
+
99
+ // fade-in nodes + edges
100
+ nodesG.querySelectorAll('g').forEach((n, i) => {
101
+ n.style.opacity = '0';
102
+ n.style.transition = 'opacity 500ms ease-out';
103
+ setTimeout(() => { if (!cancelled) n.style.opacity = '1'; }, 200 + i * 120);
104
+ });
105
+ edgesG.querySelectorAll('line').forEach((e, i) => {
106
+ e.style.opacity = '0';
107
+ e.style.transition = 'opacity 700ms ease-out';
108
+ setTimeout(() => { if (!cancelled) e.style.opacity = '1'; }, 600 + i * 80);
109
+ });
110
+
111
+ // particles flow inward (satellite → center)
112
+ let pulseIdx = 0;
113
+ const pulse = () => {
114
+ if (cancelled) return;
115
+ const s = satellites[pulseIdx % satellites.length];
116
+ const nodeG = nodesG.querySelector(`[data-id="${s.id}"]`);
117
+ const circle = nodeG?.querySelector('.node-circle');
118
+ if (circle) {
119
+ circle.classList.add('active');
120
+ setTimeout(() => circle.classList.remove('active'), 700);
121
+ }
122
+ const p = positions[s.id];
123
+ Stage.emitParticle(particlesG, p.x, p.y, 0, 0, 1100);
124
+ pulseIdx++;
125
+ };
126
+ const startT = setTimeout(() => {
127
+ pulse();
128
+ intervals.push(setInterval(pulse, 900));
129
+ }, 1400);
130
+ intervals.push(startT);
131
+
132
+ return () => {
133
+ cancelled = true;
134
+ intervals.forEach(clearTimeout);
135
+ intervals.forEach(clearInterval);
136
+ };
137
+ }
138
+
139
+ Stage.register({
140
+ section: 2,
141
+ title: 'Example · Orchestration Graph',
142
+ render(el) {
143
+ el.innerHTML = `
144
+ <div class="graph-wrap">
145
+ <svg class="graph-svg" viewBox="-200 -200 400 400">
146
+ <g class="edges" id="edges"></g>
147
+ <g class="particles" id="particles"></g>
148
+ <g class="nodes" id="nodes"></g>
149
+ </svg>
150
+ <div class="graph-caption">The core coordinates <span style="color:var(--accent)">node 1 · node 2 · node 3 · …</span></div>
151
+ </div>
152
+ `;
153
+ },
154
+ init(el) { return playOrchestrationGraph(el); },
155
+ replay(el) { return playOrchestrationGraph(el); }
156
+ });
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cookbook · Terminal Log
5
+ * -----------------------
6
+ * Technique: streaming colored log lines that build a small narrative —
7
+ * a stream of "successful" output, a long pause, then a contrasting
8
+ * realization line and a final highlighted reveal.
9
+ *
10
+ * What to copy:
11
+ * - The data-driven log model: an array of `{lvl, src, msg}` rows
12
+ * rendered by a single `renderLine(line, delay)` helper. Keeps
13
+ * the timing logic separate from the content.
14
+ * - Linear schedule with `200 + i * 280` — predictable cadence
15
+ * that still reads as "live". Pair with a longer gap (1400ms)
16
+ * before the punchline so the audience has time to catch up.
17
+ * - The three "phases": (1) loop of normal lines, (2) interjection
18
+ * in a different color/class, (3) final emphasis line with a caret.
19
+ * - The `node.style.opacity = '1'` inside `requestAnimationFrame`
20
+ * after appending — guarantees the transition runs from 0 → 1.
21
+ * - The terminal chrome (3 dots + title bar) is just static HTML —
22
+ * the only animated region is `#termBody`.
23
+ */
24
+
25
+ function playTerminalLog(el) {
26
+ const body = el.querySelector('#termBody');
27
+ body.innerHTML = '';
28
+ let cancelled = false;
29
+ const timers = [];
30
+
31
+ const lines = [
32
+ { lvl: 'INFO', src: 'agent.run', msg: 'task=do_thing start' },
33
+ { lvl: 'INFO', src: 'tool.step.one', msg: 'OK — step one finished' },
34
+ { lvl: 'INFO', src: 'tool.step.two', msg: 'OK — step two finished (id=abc123)' },
35
+ { lvl: 'INFO', src: 'agent.observe', msg: 'no errors detected' },
36
+ { lvl: 'INFO', src: 'tool.step.three', msg: 'OK — step three finished' },
37
+ { lvl: 'INFO', src: 'agent.check', msg: 'OK — 0 / 0 checks failed' },
38
+ { lvl: 'INFO', src: 'agent.run', msg: 'task=do_thing complete' },
39
+ { lvl: 'INFO', src: 'agent.run', msg: 'task=announce start' },
40
+ { lvl: 'WARN', src: 'tool.notify', msg: 'OK — posted notification "all good"' },
41
+ ];
42
+
43
+ const renderLine = (line, delay) => {
44
+ timers.push(setTimeout(() => {
45
+ if (cancelled) return;
46
+ const ts = new Date(Date.now() - (lines.length - timers.length) * 1700);
47
+ const tstr = ts.toISOString().slice(11, 19);
48
+ const node = document.createElement('span');
49
+ node.className = `log-line ${line.lvl.toLowerCase()}`;
50
+ node.innerHTML = `<span class="ts">${tstr}</span><span class="lvl">${line.lvl}</span>[<span class="src">${line.src}</span>] ${line.msg}`;
51
+ body.appendChild(node);
52
+ requestAnimationFrame(() => {
53
+ node.style.transition = 'opacity 250ms ease-out';
54
+ node.style.opacity = '1';
55
+ });
56
+ }, delay));
57
+ };
58
+
59
+ lines.forEach((l, i) => renderLine(l, 200 + i * 280));
60
+
61
+ // long pause… then human-style realization in a contrasting color
62
+ timers.push(setTimeout(() => {
63
+ if (cancelled) return;
64
+ const node = document.createElement('span');
65
+ node.className = 'log-line human';
66
+ node.innerHTML = `<br>// wait — "0 / 0 checks failed" because 0 checks exist.`;
67
+ body.appendChild(node);
68
+ requestAnimationFrame(() => {
69
+ node.style.transition = 'opacity 600ms ease-out';
70
+ node.style.opacity = '1';
71
+ });
72
+ }, 200 + lines.length * 280 + 1400));
73
+
74
+ // final emphasized reveal
75
+ timers.push(setTimeout(() => {
76
+ if (cancelled) return;
77
+ const node = document.createElement('span');
78
+ node.className = 'log-line error';
79
+ node.innerHTML = `<span class="ts">··:··:··</span><span class="lvl">ERROR</span>[<span class="src">coverage</span>] no checks for src/feature/ — agent never wrote any<span class="caret" style="margin-left:0.3em"></span>`;
80
+ body.appendChild(node);
81
+ requestAnimationFrame(() => {
82
+ node.style.transition = 'opacity 400ms ease-out';
83
+ node.style.opacity = '1';
84
+ });
85
+ }, 200 + lines.length * 280 + 2700));
86
+
87
+ return () => {
88
+ cancelled = true;
89
+ timers.forEach(clearTimeout);
90
+ };
91
+ }
92
+
93
+ Stage.register({
94
+ section: 3,
95
+ title: 'Example · Terminal Log',
96
+ render(el) {
97
+ el.innerHTML = `
98
+ <div class="terminal">
99
+ <div class="terminal-head">
100
+ <span class="tdot"></span><span class="tdot"></span><span class="tdot"></span>
101
+ <span class="ttitle">user@host ~/project — agent.do_thing()</span>
102
+ </div>
103
+ <div class="terminal-body" id="termBody"></div>
104
+ </div>
105
+ `;
106
+ },
107
+ init(el) { return playTerminalLog(el); },
108
+ replay(el) { return playTerminalLog(el); }
109
+ });
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cookbook · Token Stream
5
+ * -----------------------
6
+ * Technique: interleaved word-fill across a split panel.
7
+ *
8
+ * Two columns ("Side A" / "Side B") fill with words, alternating
9
+ * A → B → A → B. When one side runs out, the other continues alone.
10
+ * Only the final landing column gets a caret.
11
+ *
12
+ * What to copy:
13
+ * - The interleaved queue construction (the for-loop that zips two arrays).
14
+ * - The per-item randomized delay (320 + Math.random() * 280) for an
15
+ * organic, non-mechanical streaming feel.
16
+ * - The inline <style> element pattern — slide-scoped CSS injected by
17
+ * the slide itself, decoupled from the global theme.
18
+ * - The cancellation pattern: a `cancelled` flag plus a tracked `timers`
19
+ * array, returned from `init`/`replay` so the engine can clean up
20
+ * when the slide unmounts.
21
+ */
22
+
23
+ function playTokenStream(el) {
24
+ const sideA = ['task A', 'task B', 'task C', 'task D', 'task E', 'task F', 'task G'];
25
+ const sideB = ['skill 1', 'skill 2', 'skill 3', 'skill 4', 'skill 5', 'skill 6', 'skill 7', 'skill 8'];
26
+
27
+ const aEl = el.querySelector('#side-a');
28
+ const bEl = el.querySelector('#side-b');
29
+ aEl.innerHTML = '';
30
+ bEl.innerHTML = '';
31
+
32
+ // build interleaved queue: a[0], b[0], a[1], b[1], ...
33
+ // when one side exhausts, only the other contributes.
34
+ const queue = [];
35
+ const max = Math.max(sideA.length, sideB.length);
36
+ for (let i = 0; i < max; i++) {
37
+ if (i < sideA.length) queue.push({ container: aEl, word: sideA[i] });
38
+ if (i < sideB.length) queue.push({ container: bEl, word: sideB[i] });
39
+ }
40
+
41
+ const style = document.createElement('style');
42
+ style.textContent = `.stream-text .item { transition: opacity 200ms ease-out; }`;
43
+ el.appendChild(style);
44
+
45
+ let idx = 0;
46
+ let cancelled = false;
47
+ const timers = [];
48
+
49
+ function nextOne() {
50
+ if (cancelled) return;
51
+ if (idx >= queue.length) {
52
+ // landed on side B — attach caret there
53
+ const c = document.createElement('span');
54
+ c.className = 'caret';
55
+ bEl.appendChild(c);
56
+ return;
57
+ }
58
+ const { container, word } = queue[idx++];
59
+ const item = document.createElement('span');
60
+ item.className = 'item';
61
+ item.textContent = word;
62
+ container.appendChild(item);
63
+ requestAnimationFrame(() => item.style.opacity = '1');
64
+ const delay = 320 + Math.random() * 280;
65
+ timers.push(setTimeout(nextOne, delay));
66
+ }
67
+
68
+ timers.push(setTimeout(nextOne, 400));
69
+
70
+ return () => {
71
+ cancelled = true;
72
+ timers.forEach(clearTimeout);
73
+ };
74
+ }
75
+
76
+ Stage.register({
77
+ section: 1,
78
+ title: 'Example · Token Stream',
79
+ render(el) {
80
+ el.innerHTML = `
81
+ <div class="split">
82
+ <div class="split-col">
83
+ <div class="split-label cheap"><span class="dot"></span>Side A</div>
84
+ <div class="stream-text" id="side-a"></div>
85
+ </div>
86
+ <div class="split-divider"></div>
87
+ <div class="split-col">
88
+ <div class="split-label expensive"><span class="dot"></span>Side B</div>
89
+ <div class="stream-text" id="side-b"></div>
90
+ </div>
91
+ </div>
92
+ `;
93
+ },
94
+ init(el) { return playTokenStream(el); },
95
+ replay(el) { return playTokenStream(el); }
96
+ });
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cookbook · whoami
5
+ * -----------------
6
+ * Technique: terminal prompt cycling through a list of strings,
7
+ * landing on a final one.
8
+ *
9
+ * What to copy:
10
+ * - The replace-in-place pattern: each tick wipes `out.innerHTML`
11
+ * and renders one new span. Simpler (and less fiddly) than
12
+ * cross-fading between two layered elements.
13
+ * - The `isFinal` branch — the last identity gets a different
14
+ * class (.final) and a caret appended; the cycle stops there.
15
+ * - Cumulative-delay scheduling (`t += 2000`) — easier to reason
16
+ * about than computing each delay from the start.
17
+ * - The static prompt (`~/path $ whoami`) is plain HTML — only
18
+ * the output area animates. Cheap separation of layout vs motion.
19
+ */
20
+
21
+ function playWhoami(el) {
22
+ const out = el.querySelector('#whoami-out');
23
+ if (!out) return () => {};
24
+ out.innerHTML = '';
25
+
26
+ const identities = [
27
+ 'role_one',
28
+ 'role_two',
29
+ 'role_three',
30
+ 'role_four',
31
+ 'role_five',
32
+ 'role_six',
33
+ '¯\\_(ツ)_/¯',
34
+ ];
35
+
36
+ let cancelled = false;
37
+ const timers = [];
38
+
39
+ const show = (text, delay, isFinal) => {
40
+ timers.push(setTimeout(() => {
41
+ if (cancelled) return;
42
+ out.innerHTML = '';
43
+ const span = document.createElement('span');
44
+ span.className = isFinal ? 'wm-id final' : 'wm-id';
45
+ span.textContent = text;
46
+ out.appendChild(span);
47
+ if (isFinal) {
48
+ const c = document.createElement('span');
49
+ c.className = 'caret';
50
+ out.appendChild(c);
51
+ }
52
+ // small fade-in
53
+ span.style.opacity = '0';
54
+ span.style.transition = 'opacity 180ms ease-out';
55
+ requestAnimationFrame(() => { span.style.opacity = '1'; });
56
+ }, delay));
57
+ };
58
+
59
+ // initial prompt typed already — start cycling — ~2s per identity
60
+ let t = 700; // initial pause after prompt
61
+ identities.forEach((id, i) => {
62
+ const isFinal = i === identities.length - 1;
63
+ show(id, t, isFinal);
64
+ t += isFinal ? 0 : 2000;
65
+ });
66
+
67
+ return () => {
68
+ cancelled = true;
69
+ timers.forEach(clearTimeout);
70
+ };
71
+ }
72
+
73
+ Stage.register({
74
+ section: 4,
75
+ title: 'Example · whoami',
76
+ render(el) {
77
+ el.innerHTML = `
78
+ <div class="whoami">
79
+ <div class="wm-prompt">
80
+ <span class="wm-path accent">~/example</span>
81
+ <span class="wm-sigil">$</span>
82
+ <span class="wm-cmd">whoami</span>
83
+ </div>
84
+ <div class="wm-out" id="whoami-out"></div>
85
+ </div>
86
+ `;
87
+ },
88
+ init(el) { return playWhoami(el); },
89
+ replay(el) { return playWhoami(el); }
90
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "stagecraft",
3
+ "version": "0.1.0",
4
+ "description": "Cinematic, agent-authored presentations in a single HTML file.",
5
+ "type": "module",
6
+ "main": "src/engine.js",
7
+ "bin": {
8
+ "stagecraft": "bin/cli.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "themes",
13
+ "dist",
14
+ "examples",
15
+ "bin",
16
+ "starter",
17
+ "AGENT.md",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "serve": "node bin/serve.js",
22
+ "init": "node bin/init.js",
23
+ "demo": "node bin/serve.js --root demo",
24
+ "verify": "node scripts/verify.js",
25
+ "build": "node scripts/bundle.js",
26
+ "test:visual": "node tests/visual/run.js",
27
+ "test:visual:update": "node tests/visual/run.js --update-baseline",
28
+ "export": "node bin/export.js"
29
+ },
30
+ "dependencies": {
31
+ "@babel/generator": "^7.24.0",
32
+ "@babel/parser": "^7.24.0",
33
+ "@babel/traverse": "^7.24.0",
34
+ "@babel/types": "^7.24.0",
35
+ "chokidar": "^3.6.0",
36
+ "mime-types": "^2.1.35",
37
+ "ws": "^8.16.0"
38
+ },
39
+ "keywords": ["slides", "presentation", "reveal", "agent", "llm"],
40
+ "license": "MIT"
41
+ }
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.ActivityList — numbered items (num · name · description).
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.ActivityList({
8
+ * section: 3,
9
+ * title: 'Where the work goes',
10
+ * items: [
11
+ * { num: '01', name: 'Problem framing', desc: 'understand the actual ask' },
12
+ * { num: '02', name: 'Architecture', desc: 'pick the shape' }
13
+ * ],
14
+ * reveal: 'staggered' // | 'per-click' | 'instant'
15
+ * }));
16
+ *
17
+ * Spare defaults: passing nothing for `reveal` gives instant.
18
+ * Use 'staggered' for entrance animation, 'per-click' for step-by-step.
19
+ */
20
+
21
+ (function (root) {
22
+ const Stage = root.Stage = root.Stage || {};
23
+
24
+ Stage.ActivityList = function (opts) {
25
+ const items = opts.items || [];
26
+ const reveal = opts.reveal || 'instant';
27
+
28
+ const slide = {
29
+ section: opts.section,
30
+ title: opts.title,
31
+ transition: opts.transition,
32
+ render(el) {
33
+ el.innerHTML = `
34
+ <div class="activities" data-stage-key="ActivityList">
35
+ ${items.map((it, i) => `
36
+ <div class="activity" data-step="${i + 1}" data-stage-key="ActivityList/item[${i}]">
37
+ <div class="num" data-stage-edit="items[${i}].num">${escape(it.num)}</div>
38
+ <div class="body">
39
+ <div class="name" data-stage-edit="items[${i}].name">${it.name || ''}</div>
40
+ ${it.desc ? `<div class="desc" data-stage-edit="items[${i}].desc">${escape(it.desc)}</div>` : ''}
41
+ </div>
42
+ </div>
43
+ `).join('')}
44
+ </div>
45
+ `;
46
+ if (reveal === 'instant') {
47
+ el.querySelectorAll('.activity').forEach(n => n.classList.add('in'));
48
+ }
49
+ }
50
+ };
51
+
52
+ if (reveal === 'staggered') {
53
+ slide.init = function (el) {
54
+ return Stage.staggerIn(el.querySelectorAll('.activity'), 300, 200);
55
+ };
56
+ slide.replay = function (el) {
57
+ el.querySelectorAll('.activity').forEach(n => n.classList.remove('in'));
58
+ return this.init(el);
59
+ };
60
+ } else if (reveal === 'per-click') {
61
+ slide.steps = items.length;
62
+ slide.onStep = function (el, step) {
63
+ el.querySelectorAll('.activity').forEach(n => {
64
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
65
+ });
66
+ };
67
+ }
68
+
69
+ return slide;
70
+ };
71
+
72
+ function escape(s) {
73
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
74
+ }
75
+ })(typeof window !== 'undefined' ? window : globalThis);