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,215 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.LineChart — SVG line chart with optional area fill.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.LineChart({
8
+ * section: 5,
9
+ * title: '05 · adoption curve',
10
+ * series: [
11
+ * { label: 'Q1', color: 'accent', points: [10, 15, 22, 36, 51, 70] },
12
+ * { label: 'Q2', color: 'blue', points: [ 8, 12, 18, 24, 35, 48] }
13
+ * ],
14
+ * xLabels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
15
+ * yMax: 80, // optional, defaults to max+10%
16
+ * area: true, // optional, fills below line
17
+ * reveal: 'animated' // 'animated' | 'instant'
18
+ * }));
19
+ *
20
+ * On init each line draws from left to right using stroke-dasharray.
21
+ *
22
+ * Edit paths: series[i].label, xLabels[i]
23
+ */
24
+
25
+ (function (root) {
26
+ const Stage = root.Stage = root.Stage || {};
27
+
28
+ const COLOR_MAP = {
29
+ accent: 'var(--accent)',
30
+ amber: 'var(--amber)',
31
+ blue: 'var(--blue)',
32
+ red: 'var(--red)',
33
+ dim: 'var(--dim)',
34
+ fg: 'var(--fg)'
35
+ };
36
+
37
+ function colorFor(name, idx) {
38
+ if (COLOR_MAP[name]) return COLOR_MAP[name];
39
+ const cycle = ['var(--accent)', 'var(--blue)', 'var(--amber)', 'var(--red)', 'var(--dim)'];
40
+ return cycle[idx % cycle.length];
41
+ }
42
+
43
+ // viewBox geometry
44
+ const VB_W = 800;
45
+ const VB_H = 400;
46
+ const PAD_L = 50;
47
+ const PAD_R = 20;
48
+ const PAD_T = 20;
49
+ const PAD_B = 40;
50
+ const PLOT_W = VB_W - PAD_L - PAD_R;
51
+ const PLOT_H = VB_H - PAD_T - PAD_B;
52
+
53
+ Stage.LineChart = function (opts) {
54
+ const series = opts.series || [];
55
+ const xLabels = opts.xLabels || [];
56
+ const reveal = opts.reveal || 'animated';
57
+ const area = !!opts.area;
58
+
59
+ // Find global max
60
+ let allMax = 0;
61
+ series.forEach(s => (s.points || []).forEach(p => { if (Number(p) > allMax) allMax = Number(p); }));
62
+ const yMax = Number(opts.yMax) > 0 ? Number(opts.yMax) : (allMax > 0 ? allMax * 1.1 : 1);
63
+
64
+ // Use the longest series for x grid
65
+ const maxLen = series.reduce((m, s) => Math.max(m, (s.points || []).length), 0);
66
+
67
+ function xAt(i, n) {
68
+ if (n <= 1) return PAD_L + PLOT_W / 2;
69
+ return PAD_L + (i / (n - 1)) * PLOT_W;
70
+ }
71
+ function yAt(v) {
72
+ return PAD_T + PLOT_H - (v / yMax) * PLOT_H;
73
+ }
74
+
75
+ function pathFor(points) {
76
+ if (!points.length) return '';
77
+ const n = points.length;
78
+ return points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${xAt(i, n).toFixed(2)} ${yAt(Number(p) || 0).toFixed(2)}`).join(' ');
79
+ }
80
+ function areaFor(points) {
81
+ if (!points.length) return '';
82
+ const n = points.length;
83
+ const top = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${xAt(i, n).toFixed(2)} ${yAt(Number(p) || 0).toFixed(2)}`).join(' ');
84
+ const right = `L ${xAt(n - 1, n).toFixed(2)} ${(PAD_T + PLOT_H).toFixed(2)}`;
85
+ const left = `L ${xAt(0, n).toFixed(2)} ${(PAD_T + PLOT_H).toFixed(2)} Z`;
86
+ return `${top} ${right} ${left}`;
87
+ }
88
+
89
+ // y-axis ticks: 4 evenly spaced labels
90
+ const yTicks = [0, 0.25, 0.5, 0.75, 1].map(t => ({
91
+ y: PAD_T + PLOT_H - t * PLOT_H,
92
+ label: formatNum(yMax * t)
93
+ }));
94
+
95
+ function formatNum(v) {
96
+ if (v >= 1000) return (v / 1000).toFixed(v >= 10000 ? 0 : 1) + 'k';
97
+ return Number.isInteger(v) ? String(v) : v.toFixed(1);
98
+ }
99
+
100
+ const slide = {
101
+ section: opts.section,
102
+ title: opts.title,
103
+ transition: opts.transition,
104
+ render(el) {
105
+ const gridLines = yTicks.map(t => `
106
+ <line class="lc-grid" x1="${PAD_L}" x2="${VB_W - PAD_R}" y1="${t.y}" y2="${t.y}"/>
107
+ <text class="lc-y-label" x="${PAD_L - 8}" y="${t.y + 4}" text-anchor="end">${escape(t.label)}</text>
108
+ `).join('');
109
+
110
+ const xTicks = xLabels.slice(0, maxLen).map((lbl, i) => `
111
+ <text class="lc-x-label"
112
+ data-stage-edit="xLabels[${i}]"
113
+ x="${xAt(i, maxLen).toFixed(2)}"
114
+ y="${(VB_H - PAD_B + 22).toFixed(2)}"
115
+ text-anchor="middle">${escape(lbl)}</text>
116
+ `).join('');
117
+
118
+ const seriesSvg = series.map((s, i) => {
119
+ const points = s.points || [];
120
+ const stroke = colorFor(s.color, i);
121
+ const d = pathFor(points);
122
+ const aPath = area ? areaFor(points) : '';
123
+ return `
124
+ <g class="lc-series" data-i="${i}" data-stage-key="LineChart/series[${i}]">
125
+ ${area ? `<path class="lc-area" data-i="${i}" d="${aPath}" fill="${stroke}" fill-opacity="0.12"/>` : ''}
126
+ <path class="lc-line" data-i="${i}" d="${d}" stroke="${stroke}"/>
127
+ ${points.map((p, j) => `
128
+ <circle class="lc-dot" data-i="${i}" data-j="${j}"
129
+ cx="${xAt(j, points.length).toFixed(2)}"
130
+ cy="${yAt(Number(p) || 0).toFixed(2)}"
131
+ r="3" fill="${stroke}"/>
132
+ `).join('')}
133
+ </g>
134
+ `;
135
+ }).join('');
136
+
137
+ const legend = series.map((s, i) => `
138
+ <div class="lc-leg-row" data-stage-key="LineChart/legend[${i}]">
139
+ <span class="lc-dot-leg" style="--lc-color: ${colorFor(s.color, i)};"></span>
140
+ <span class="lc-leg-label" data-stage-edit="series[${i}].label">${escape(s.label || '')}</span>
141
+ </div>
142
+ `).join('');
143
+
144
+ el.innerHTML = `
145
+ <div class="linechart" data-stage-key="LineChart">
146
+ <div class="lc-svg-wrap">
147
+ <svg class="lc-svg" viewBox="0 0 ${VB_W} ${VB_H}" preserveAspectRatio="xMidYMid meet">
148
+ ${gridLines}
149
+ ${xTicks}
150
+ ${seriesSvg}
151
+ </svg>
152
+ </div>
153
+ ${series.length > 1 ? `<div class="lc-legend">${legend}</div>` : ''}
154
+ </div>
155
+ `;
156
+
157
+ if (reveal === 'instant') {
158
+ el.querySelectorAll('.lc-line').forEach(line => {
159
+ line.style.strokeDasharray = 'none';
160
+ line.style.strokeDashoffset = '0';
161
+ });
162
+ el.querySelectorAll('.lc-dot').forEach(d => d.classList.add('in'));
163
+ el.querySelectorAll('.lc-area').forEach(a => a.classList.add('in'));
164
+ }
165
+ }
166
+ };
167
+
168
+ if (reveal === 'animated') {
169
+ slide.init = function (el) {
170
+ const lines = Array.from(el.querySelectorAll('.lc-line'));
171
+ const areas = Array.from(el.querySelectorAll('.lc-area'));
172
+ const dots = Array.from(el.querySelectorAll('.lc-dot'));
173
+ const timers = [];
174
+
175
+ lines.forEach((line, idx) => {
176
+ const length = line.getTotalLength ? line.getTotalLength() : 1000;
177
+ line.style.strokeDasharray = `${length}`;
178
+ line.style.strokeDashoffset = `${length}`;
179
+ // Force reflow
180
+ // eslint-disable-next-line no-unused-expressions
181
+ line.getBoundingClientRect();
182
+ timers.push(setTimeout(() => {
183
+ line.style.transition = 'stroke-dashoffset 1400ms cubic-bezier(0.16, 1, 0.3, 1)';
184
+ line.style.strokeDashoffset = '0';
185
+ }, 100 + idx * 200));
186
+ });
187
+ areas.forEach((a, idx) => {
188
+ timers.push(setTimeout(() => a.classList.add('in'), 800 + idx * 200));
189
+ });
190
+ dots.forEach((d, idx) => {
191
+ timers.push(setTimeout(() => d.classList.add('in'), 800 + idx * 60));
192
+ });
193
+
194
+ return () => timers.forEach(clearTimeout);
195
+ };
196
+ slide.replay = function (el) {
197
+ el.querySelectorAll('.lc-line').forEach(line => {
198
+ line.style.transition = 'none';
199
+ const length = line.getTotalLength ? line.getTotalLength() : 1000;
200
+ line.style.strokeDasharray = `${length}`;
201
+ line.style.strokeDashoffset = `${length}`;
202
+ });
203
+ el.querySelectorAll('.lc-dot').forEach(d => d.classList.remove('in'));
204
+ el.querySelectorAll('.lc-area').forEach(a => a.classList.remove('in'));
205
+ return this.init(el);
206
+ };
207
+ }
208
+
209
+ return slide;
210
+ };
211
+
212
+ function escape(s) {
213
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
214
+ }
215
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Manifesto — numbered list of "We believe..." declarations.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Manifesto({
8
+ * section: 3,
9
+ * title: '03 · What we believe',
10
+ * intro: 'A working manifesto, revised quarterly.',
11
+ * declarations: [
12
+ * { text: 'Taste is the scarce part.', emphasis: ['scarce'] },
13
+ * { text: 'Speed without review is debt.', emphasis: ['debt'] },
14
+ * { text: 'The model is a colleague, not an oracle.', emphasis: ['colleague','oracle'] }
15
+ * ],
16
+ * reveal: 'staggered' // 'staggered' | 'per-click' | 'instant'
17
+ * }));
18
+ *
19
+ * Edit paths:
20
+ * intro / declarations[i].text
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ Stage.Manifesto = function (opts) {
27
+ const declarations = opts.declarations || [];
28
+ const intro = opts.intro || '';
29
+ const reveal = opts.reveal || 'staggered';
30
+
31
+ const slide = {
32
+ section: opts.section,
33
+ title: opts.title,
34
+ transition: opts.transition,
35
+ render(el) {
36
+ el.innerHTML = `
37
+ <div class="manifesto" data-stage-key="Manifesto">
38
+ ${intro ? `<div class="mf-intro" data-stage-edit="intro" data-stage-key="Manifesto/intro">${escape(intro)}</div>` : ''}
39
+ <ol class="mf-list">
40
+ ${declarations.map((d, i) => `
41
+ <li class="mf-item" data-step="${i + 1}" data-stage-key="Manifesto/decl[${i}]">
42
+ <span class="mf-num" aria-hidden="true">${String(i + 1).padStart(2, '0')}</span>
43
+ <span class="mf-text" data-stage-edit="declarations[${i}].text" data-stage-key="Manifesto/decl[${i}]/text">${emphasizeHTML(d.text || '', d.emphasis || [])}</span>
44
+ </li>
45
+ `).join('')}
46
+ </ol>
47
+ </div>
48
+ `;
49
+ if (reveal === 'instant') {
50
+ el.querySelectorAll('.mf-item, .mf-intro').forEach(n => n.classList.add('in'));
51
+ }
52
+ }
53
+ };
54
+
55
+ if (reveal === 'staggered') {
56
+ slide.init = function (el) {
57
+ const intro = el.querySelector('.mf-intro');
58
+ const timers = [];
59
+ if (intro) timers.push(setTimeout(() => intro.classList.add('in'), 100));
60
+ const items = el.querySelectorAll('.mf-item');
61
+ items.forEach((n, i) => {
62
+ timers.push(setTimeout(() => n.classList.add('in'), 400 + i * 220));
63
+ });
64
+ return () => timers.forEach(clearTimeout);
65
+ };
66
+ slide.replay = function (el) {
67
+ el.querySelectorAll('.mf-item, .mf-intro').forEach(n => n.classList.remove('in'));
68
+ return this.init(el);
69
+ };
70
+ } else if (reveal === 'per-click') {
71
+ slide.steps = declarations.length;
72
+ slide.init = function (el) {
73
+ const intro = el.querySelector('.mf-intro');
74
+ if (intro) setTimeout(() => intro.classList.add('in'), 100);
75
+ };
76
+ slide.onStep = function (el, step) {
77
+ el.querySelectorAll('.mf-item').forEach(n => {
78
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
79
+ });
80
+ };
81
+ }
82
+
83
+ return slide;
84
+ };
85
+
86
+ function emphasizeHTML(raw, emphasis) {
87
+ let html = escape(raw);
88
+ const sorted = [...emphasis].filter(Boolean).sort((a, b) => b.length - a.length);
89
+ for (const phrase of sorted) {
90
+ const esc = escape(phrase);
91
+ const re = new RegExp(escapeRegex(esc), 'g');
92
+ html = html.replace(re, `<span class="mf-em">${esc}</span>`);
93
+ }
94
+ return html;
95
+ }
96
+
97
+ function escapeRegex(s) {
98
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
99
+ }
100
+
101
+ function escape(s) {
102
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
103
+ }
104
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Marquee — horizontal scrolling text strip (stock-ticker style).
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Marquee({
8
+ * section: 8,
9
+ * title: '08 · Headlines',
10
+ * items: ['ship faster', 'review harder', 'measure everything', 'sleep sometimes'],
11
+ * direction: 'left', // 'left' | 'right'
12
+ * speed: 'medium', // 'slow' | 'medium' | 'fast'
13
+ * double: true // optional second track in opposite direction
14
+ * }));
15
+ *
16
+ * Pauses on hover. Pure CSS animation.
17
+ *
18
+ * Edit paths:
19
+ * items[i]
20
+ */
21
+
22
+ (function (root) {
23
+ const Stage = root.Stage = root.Stage || {};
24
+
25
+ Stage.Marquee = function (opts) {
26
+ const items = Array.isArray(opts.items) ? opts.items : [];
27
+ const direction = opts.direction === 'right' ? 'right' : 'left';
28
+ const speed = ['slow', 'medium', 'fast'].includes(opts.speed) ? opts.speed : 'medium';
29
+ const double = !!opts.double;
30
+
31
+ return {
32
+ section: opts.section,
33
+ title: opts.title,
34
+ transition: opts.transition,
35
+ render(el) {
36
+ el.classList.add('marquee-host');
37
+ el.innerHTML = `
38
+ <div class="marquee mq--${escape(speed)} mq--${escape(direction)}" data-stage-key="Marquee">
39
+ ${renderTrack(items, 'a')}
40
+ ${double ? renderTrack(items, 'b', true) : ''}
41
+ </div>
42
+ `;
43
+ }
44
+ };
45
+ };
46
+
47
+ function renderTrack(items, key, reverse) {
48
+ // Duplicate items so we can loop translateX 0 → -50% seamlessly.
49
+ const track = items.map((it, i) => `
50
+ <span class="mq-item" data-stage-edit="items[${i}]" data-stage-key="Marquee/item[${i}]">${escape(it)}</span>
51
+ <span class="mq-sep" aria-hidden="true">·</span>
52
+ `).join('');
53
+ return `
54
+ <div class="mq-track mq-track--${key} ${reverse ? 'mq-track--rev' : ''}" aria-hidden="${key === 'b' ? 'true' : 'false'}">
55
+ <div class="mq-row">${track}${track}</div>
56
+ </div>
57
+ `;
58
+ }
59
+
60
+ function escape(s) {
61
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
62
+ }
63
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Matrix2x2 — classic 2x2 decision/priority matrix
5
+ * (e.g. Eisenhower, Effort/Impact, Importance/Urgency).
6
+ *
7
+ * Usage:
8
+ * Stage.register(Stage.Matrix2x2({
9
+ * section: 5,
10
+ * title: '05 · prioritize',
11
+ * axes: {
12
+ * x: { label: 'effort', low: 'low', high: 'high' },
13
+ * y: { label: 'impact', low: 'low', high: 'high' }
14
+ * },
15
+ * quadrants: [
16
+ * { x: 'low', y: 'high', label: 'Quick wins', body: 'do these first', color: 'accent' },
17
+ * { x: 'high', y: 'high', label: 'Big bets', body: 'plan carefully', color: 'blue' },
18
+ * { x: 'low', y: 'low', label: 'Fill-ins', body: 'spare cycles' },
19
+ * { x: 'high', y: 'low', label: 'Money pit', body: 'avoid', color: 'red' }
20
+ * ],
21
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
22
+ * }));
23
+ *
24
+ * Layer-3 (inline edit):
25
+ * axes.x.label, axes.x.low, axes.x.high
26
+ * axes.y.label, axes.y.low, axes.y.high
27
+ * quadrants[i].label, quadrants[i].body
28
+ *
29
+ * Per-click: each click highlights one more quadrant (in array order).
30
+ */
31
+
32
+ (function (root) {
33
+ const Stage = root.Stage = root.Stage || {};
34
+
35
+ const COLOR_MAP = {
36
+ accent: 'var(--accent)',
37
+ amber: 'var(--amber)',
38
+ blue: 'var(--blue)',
39
+ red: 'var(--red)',
40
+ dim: 'var(--dim)',
41
+ fg: 'var(--fg)'
42
+ };
43
+
44
+ Stage.Matrix2x2 = function (opts) {
45
+ const axes = opts.axes || { x: {}, y: {} };
46
+ const xAxis = axes.x || {};
47
+ const yAxis = axes.y || {};
48
+ const quadrants = (opts.quadrants || []).slice(0, 4);
49
+ const reveal = opts.reveal || 'instant';
50
+
51
+ // Build a 2x2 grid lookup keyed by `${yPos}-${xPos}`. Render order is
52
+ // top-left, top-right, bottom-left, bottom-right (CSS grid order).
53
+ const cellOrder = [
54
+ { x: 'low', y: 'high' },
55
+ { x: 'high', y: 'high' },
56
+ { x: 'low', y: 'low' },
57
+ { x: 'high', y: 'low' }
58
+ ];
59
+
60
+ function findQuadrant(x, y) {
61
+ const idx = quadrants.findIndex(q => q.x === x && q.y === y);
62
+ return idx >= 0 ? { q: quadrants[idx], idx } : { q: null, idx: -1 };
63
+ }
64
+
65
+ function colorFor(name) {
66
+ return COLOR_MAP[name] || COLOR_MAP.accent;
67
+ }
68
+
69
+ const slide = {
70
+ section: opts.section,
71
+ title: opts.title,
72
+ transition: opts.transition,
73
+ render(el) {
74
+ el.classList.add('has-matrix');
75
+ const cells = cellOrder.map((pos, cellIdx) => {
76
+ const { q, idx } = findQuadrant(pos.x, pos.y);
77
+ if (!q) {
78
+ return `<div class="quadrant placeholder" data-stage-key="Matrix2x2/cell[${cellIdx}]"></div>`;
79
+ }
80
+ const color = colorFor(q.color);
81
+ const tag = `${labelShort(pos.y)} · ${labelShort(pos.x)}`;
82
+ return `
83
+ <div class="quadrant"
84
+ data-quad-index="${idx}"
85
+ data-stage-key="Matrix2x2/quadrant[${idx}]"
86
+ style="--q-color: ${color};">
87
+ <div class="q-tag">${escape(tag)}</div>
88
+ <div class="q-label" data-stage-edit="quadrants[${idx}].label">${escape(q.label || '')}</div>
89
+ ${q.body ? `<div class="q-body" data-stage-edit="quadrants[${idx}].body">${escape(q.body)}</div>` : ''}
90
+ </div>
91
+ `;
92
+ }).join('');
93
+
94
+ el.innerHTML = `
95
+ <div class="matrix2x2" data-stage-key="Matrix2x2">
96
+ <div class="y-axis" data-stage-key="Matrix2x2/y-axis">
97
+ <span class="hi" data-stage-edit="axes.y.high">${escape(yAxis.high || 'high')}</span>
98
+ <span class="label" data-stage-edit="axes.y.label">${escape(yAxis.label || '')}</span>
99
+ <span class="lo" data-stage-edit="axes.y.low">${escape(yAxis.low || 'low')}</span>
100
+ </div>
101
+ <div class="grid" data-stage-key="Matrix2x2/grid">
102
+ ${cells}
103
+ </div>
104
+ <div class="x-axis" data-stage-key="Matrix2x2/x-axis">
105
+ <span class="lo" data-stage-edit="axes.x.low">${escape(xAxis.low || 'low')}</span>
106
+ <span class="label" data-stage-edit="axes.x.label">${escape(xAxis.label || '')}</span>
107
+ <span class="hi" data-stage-edit="axes.x.high">${escape(xAxis.high || 'high')}</span>
108
+ </div>
109
+ </div>
110
+ `;
111
+
112
+ if (reveal === 'instant') {
113
+ el.querySelectorAll('.quadrant[data-quad-index]').forEach(n => n.classList.add('active'));
114
+ }
115
+ }
116
+ };
117
+
118
+ if (reveal === 'staggered') {
119
+ slide.init = function (el) {
120
+ const nodes = el.querySelectorAll('.quadrant[data-quad-index]');
121
+ const timers = [];
122
+ nodes.forEach((n, i) => {
123
+ timers.push(setTimeout(() => n.classList.add('active'), 220 + i * 260));
124
+ });
125
+ return () => timers.forEach(clearTimeout);
126
+ };
127
+ slide.replay = function (el) {
128
+ el.querySelectorAll('.quadrant').forEach(n => n.classList.remove('active'));
129
+ return this.init(el);
130
+ };
131
+ } else if (reveal === 'per-click') {
132
+ slide.steps = quadrants.length;
133
+ slide.onStep = function (el, step) {
134
+ el.querySelectorAll('.quadrant[data-quad-index]').forEach(n => {
135
+ const idx = Number(n.dataset.quadIndex);
136
+ n.classList.toggle('active', idx < step);
137
+ });
138
+ };
139
+ }
140
+
141
+ return slide;
142
+ };
143
+
144
+ function labelShort(pos) {
145
+ return pos === 'high' ? 'hi' : 'lo';
146
+ }
147
+
148
+ function escape(s) {
149
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
150
+ }
151
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Pillars — N evenly-spaced columns, each with material icon, heading, body.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Pillars({
8
+ * section: 4,
9
+ * title: '04 · Three pillars',
10
+ * intro: 'How the work splits',
11
+ * pillars: [
12
+ * { icon: 'bolt', heading: 'Speed', body: 'tight loops', color: 'accent' },
13
+ * { icon: 'shield',heading: 'Trust', body: 'evidence first',color: 'blue' },
14
+ * { icon: 'eco', heading: 'Restraint',body: 'do less', color: 'amber' }
15
+ * ],
16
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
17
+ * }));
18
+ *
19
+ * Edit paths:
20
+ * intro / pillars[i].icon / pillars[i].heading / pillars[i].body
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ Stage.Pillars = function (opts) {
27
+ const pillars = opts.pillars || [];
28
+ const reveal = opts.reveal || 'instant';
29
+ const intro = opts.intro || '';
30
+ const count = Math.min(pillars.length, 5);
31
+
32
+ const slide = {
33
+ section: opts.section,
34
+ title: opts.title,
35
+ transition: opts.transition,
36
+ render(el) {
37
+ el.innerHTML = `
38
+ <div class="pillars-wrap" data-stage-key="Pillars">
39
+ ${intro ? `<div class="pillars-intro" data-stage-edit="intro" data-stage-key="Pillars/intro">${escape(intro)}</div>` : ''}
40
+ <div class="pillars-grid" style="--pillars-count: ${count};">
41
+ ${pillars.map((p, i) => `
42
+ <div class="pillar ${p.color ? 'pc-' + escape(p.color) : ''}" data-step="${i + 1}" data-stage-key="Pillars/pillar[${i}]">
43
+ <span class="pillar-icon material-symbols-outlined" data-stage-edit="pillars[${i}].icon">${escape(p.icon || 'circle')}</span>
44
+ <div class="pillar-heading" data-stage-edit="pillars[${i}].heading" data-stage-key="Pillars/pillar[${i}]/heading">${escape(p.heading || '')}</div>
45
+ ${p.body ? `<div class="pillar-body" data-stage-edit="pillars[${i}].body" data-stage-key="Pillars/pillar[${i}]/body">${escape(p.body)}</div>` : ''}
46
+ </div>
47
+ `).join('')}
48
+ </div>
49
+ </div>
50
+ `;
51
+ if (reveal === 'instant') {
52
+ el.querySelectorAll('.pillar').forEach(n => n.classList.add('in'));
53
+ }
54
+ }
55
+ };
56
+
57
+ if (reveal === 'staggered') {
58
+ slide.init = function (el) {
59
+ return Stage.staggerIn(el.querySelectorAll('.pillar'), 180, 200);
60
+ };
61
+ slide.replay = function (el) {
62
+ el.querySelectorAll('.pillar').forEach(n => n.classList.remove('in'));
63
+ return this.init(el);
64
+ };
65
+ } else if (reveal === 'per-click') {
66
+ slide.steps = pillars.length;
67
+ slide.onStep = function (el, step) {
68
+ el.querySelectorAll('.pillar').forEach(n => {
69
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
70
+ });
71
+ };
72
+ }
73
+
74
+ return slide;
75
+ };
76
+
77
+ function escape(s) {
78
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
79
+ }
80
+ })(typeof window !== 'undefined' ? window : globalThis);