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,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Pricing — 3-tier (or N-tier) pricing card layout.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Pricing({
8
+ * section: 60,
9
+ * title: '60 · Pricing',
10
+ * tiers: [
11
+ * { name: 'Starter', price: '$0', period: '/mo',
12
+ * features: ['1 project', 'Community support'], ctaLabel: 'Start free' },
13
+ * { name: 'Pro', price: '$29', period: '/mo', featured: true,
14
+ * features: ['Unlimited projects', 'Priority support', 'SAML SSO'],
15
+ * ctaLabel: 'Start trial' },
16
+ * { name: 'Scale', price: 'Custom',
17
+ * features: ['SLA', 'Dedicated CSM', 'On-prem'], ctaLabel: 'Talk to sales' }
18
+ * ],
19
+ * reveal: 'staggered' // 'instant' | 'staggered'
20
+ * }));
21
+ *
22
+ * Edit paths: tiers[i].name / .price / .period / .features[k] / .ctaLabel
23
+ */
24
+
25
+ (function (root) {
26
+ const Stage = root.Stage = root.Stage || {};
27
+
28
+ Stage.Pricing = function (opts) {
29
+ const tiers = opts.tiers || [];
30
+ const reveal = opts.reveal || 'instant';
31
+ const cols = Math.min(4, Math.max(1, tiers.length));
32
+
33
+ const slide = {
34
+ section: opts.section,
35
+ title: opts.title,
36
+ transition: opts.transition,
37
+ render(el) {
38
+ el.innerHTML = `
39
+ <div class="pricing pricing--cols-${cols}" data-stage-key="Pricing">
40
+ ${tiers.map((t, i) => `
41
+ <div class="pricing-tier${t.featured ? ' pricing-tier--featured' : ''}"
42
+ data-step="${i + 1}"
43
+ data-stage-key="Pricing/tier[${i}]">
44
+ ${t.featured ? `<div class="pricing-badge">most popular</div>` : ''}
45
+ <div class="pricing-name" data-stage-edit="tiers[${i}].name">${escape(t.name || '')}</div>
46
+ <div class="pricing-figure">
47
+ <span class="pricing-price" data-stage-edit="tiers[${i}].price">${escape(t.price || '')}</span>
48
+ ${t.period ? `<span class="pricing-period" data-stage-edit="tiers[${i}].period">${escape(t.period)}</span>` : ''}
49
+ </div>
50
+ <ul class="pricing-features" data-stage-key="Pricing/tier[${i}]/features">
51
+ ${(t.features || []).map((f, k) => `
52
+ <li class="pricing-feature" data-stage-key="Pricing/tier[${i}]/feature[${k}]">
53
+ <span class="pricing-check material-symbols-outlined">check</span>
54
+ <span data-stage-edit="tiers[${i}].features[${k}]">${escape(f)}</span>
55
+ </li>
56
+ `).join('')}
57
+ </ul>
58
+ ${t.ctaLabel ? `
59
+ <div class="pricing-cta" data-stage-edit="tiers[${i}].ctaLabel">
60
+ ${escape(t.ctaLabel)}
61
+ <span class="pricing-cta-arrow material-symbols-outlined">arrow_forward</span>
62
+ </div>
63
+ ` : ''}
64
+ </div>
65
+ `).join('')}
66
+ </div>
67
+ `;
68
+ if (reveal === 'instant') {
69
+ el.querySelectorAll('.pricing-tier').forEach(n => n.classList.add('in'));
70
+ }
71
+ }
72
+ };
73
+
74
+ if (reveal === 'staggered') {
75
+ slide.init = function (el) {
76
+ return Stage.staggerIn(el.querySelectorAll('.pricing-tier'), 180, 200);
77
+ };
78
+ slide.replay = function (el) {
79
+ el.querySelectorAll('.pricing-tier').forEach(n => n.classList.remove('in'));
80
+ return this.init(el);
81
+ };
82
+ }
83
+
84
+ return slide;
85
+ };
86
+
87
+ function escape(s) {
88
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
89
+ }
90
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.ProcessFlow — sequence of step cards joined by arrows.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.ProcessFlow({
8
+ * section: 3,
9
+ * title: '03 · the loop',
10
+ * orientation: 'horizontal', // default | 'vertical'
11
+ * steps: [
12
+ * { icon: 'edit_note', label: 'Draft', body: 'frame the intent', color: 'accent' },
13
+ * { icon: 'visibility', label: 'Review', body: 'read the diff', color: 'blue' },
14
+ * { icon: 'science', label: 'Verify', body: 'run the evidence', color: 'amber' },
15
+ * { icon: 'rocket_launch',label: 'Ship', body: 'land the change' }
16
+ * ],
17
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
18
+ * }));
19
+ *
20
+ * Icons are Google Material Symbols (rendered via
21
+ * <span class="material-symbols-outlined">name</span>).
22
+ *
23
+ * For `per-click` reveal each arrow pulses briefly as the next step lands.
24
+ *
25
+ * Layer-3 (inline edit): steps[i].label, steps[i].body, steps[i].icon.
26
+ */
27
+
28
+ (function (root) {
29
+ const Stage = root.Stage = root.Stage || {};
30
+
31
+ Stage.ProcessFlow = function (opts) {
32
+ const steps = opts.steps || [];
33
+ const orientation = opts.orientation === 'vertical' ? 'vertical' : 'horizontal';
34
+ const reveal = opts.reveal || 'instant';
35
+ const arrowChar = orientation === 'vertical' ? '↓' : '→';
36
+
37
+ const slide = {
38
+ section: opts.section,
39
+ title: opts.title,
40
+ transition: opts.transition,
41
+ render(el) {
42
+ const parts = [];
43
+ steps.forEach((s, i) => {
44
+ const colorCls = s.color ? ` ${s.color}` : '';
45
+ parts.push(`
46
+ <div class="pf-step${colorCls}"
47
+ data-step="${i + 1}"
48
+ data-stage-key="ProcessFlow/step[${i}]">
49
+ ${s.icon ? `
50
+ <div class="pf-icon" data-stage-key="ProcessFlow/step[${i}]/icon">
51
+ <span class="material-symbols-outlined" data-stage-edit="steps[${i}].icon">${escape(s.icon)}</span>
52
+ </div>` : ''}
53
+ <div class="pf-label" data-stage-edit="steps[${i}].label">${escape(s.label || '')}</div>
54
+ ${s.body ? `<div class="pf-body" data-stage-edit="steps[${i}].body">${escape(s.body)}</div>` : ''}
55
+ </div>
56
+ `);
57
+ if (i < steps.length - 1) {
58
+ parts.push(`
59
+ <div class="pf-arrow"
60
+ data-step="${i + 1}"
61
+ data-arrow-after="${i}"
62
+ data-stage-key="ProcessFlow/arrow[${i}]"
63
+ aria-hidden="true">${arrowChar}</div>
64
+ `);
65
+ }
66
+ });
67
+
68
+ el.innerHTML = `
69
+ <div class="processflow ${orientation}" data-stage-key="ProcessFlow">
70
+ ${parts.join('')}
71
+ </div>
72
+ `;
73
+
74
+ if (reveal === 'instant') {
75
+ el.querySelectorAll('.pf-step, .pf-arrow').forEach(n => n.classList.add('in'));
76
+ }
77
+ }
78
+ };
79
+
80
+ if (reveal === 'staggered') {
81
+ slide.init = function (el) {
82
+ const nodes = Array.from(el.querySelectorAll('.pf-step, .pf-arrow'));
83
+ const timers = [];
84
+ nodes.forEach((n, i) => {
85
+ // Steps in a bit longer than arrows so the card lands before the arrow.
86
+ const delay = 200 + i * 220;
87
+ timers.push(setTimeout(() => n.classList.add('in'), delay));
88
+ });
89
+ return () => timers.forEach(clearTimeout);
90
+ };
91
+ slide.replay = function (el) {
92
+ el.querySelectorAll('.pf-step, .pf-arrow').forEach(n => {
93
+ n.classList.remove('in');
94
+ n.classList.remove('pulse');
95
+ });
96
+ return this.init(el);
97
+ };
98
+ } else if (reveal === 'per-click') {
99
+ slide.steps = steps.length;
100
+ slide.onStep = function (el, step) {
101
+ const stepNodes = el.querySelectorAll('.pf-step');
102
+ const arrowNodes = el.querySelectorAll('.pf-arrow');
103
+ stepNodes.forEach(n => {
104
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
105
+ });
106
+ arrowNodes.forEach(n => {
107
+ const idx = Number(n.dataset.arrowAfter);
108
+ // Arrow between step[i] and step[i+1] becomes visible after step[i+1] lands.
109
+ const visible = (idx + 1) < step;
110
+ n.classList.toggle('in', visible);
111
+ });
112
+ // Pulse the arrow that just connected (if any).
113
+ if (step >= 2) {
114
+ const justConnectedIdx = step - 2; // arrow index between step (step-1) and step
115
+ const arrow = el.querySelector(`.pf-arrow[data-arrow-after="${justConnectedIdx}"]`);
116
+ if (arrow) {
117
+ arrow.classList.remove('pulse');
118
+ // Restart animation
119
+ // eslint-disable-next-line no-unused-expressions
120
+ arrow.offsetWidth;
121
+ arrow.classList.add('pulse');
122
+ }
123
+ }
124
+ };
125
+ }
126
+
127
+ return slide;
128
+ };
129
+
130
+ function escape(s) {
131
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
132
+ }
133
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Progress — stacked horizontal progress bars.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Progress({
8
+ * section: 6,
9
+ * title: '06 · How far we got',
10
+ * items: [
11
+ * { label: 'Spec', value: 92, color: 'accent' },
12
+ * { label: 'Implementation',value: 68, color: 'blue' },
13
+ * { label: 'Tests', value: 41, color: 'amber' },
14
+ * { label: 'Docs', value: 12, color: 'red' }
15
+ * ],
16
+ * reveal: 'animated' // 'instant' | 'animated' | 'per-click'
17
+ * }));
18
+ *
19
+ * Each item: `value` is treated as a fraction of `total` (default 100).
20
+ * So the default is straight percentages.
21
+ *
22
+ * Layer-3 (inline edit): items[i].label, items[i].value.
23
+ */
24
+
25
+ (function (root) {
26
+ const Stage = root.Stage = root.Stage || {};
27
+
28
+ Stage.Progress = function (opts) {
29
+ const items = opts.items || [];
30
+ const reveal = opts.reveal || 'instant';
31
+
32
+ function pctFor(it) {
33
+ const total = Number(it.total) > 0 ? Number(it.total) : 100;
34
+ const v = Number(it.value) || 0;
35
+ return Math.max(0, Math.min(100, (v / total) * 100));
36
+ }
37
+
38
+ function displayFor(it) {
39
+ // For total=100 (default), show the value itself as percent. Otherwise
40
+ // show the computed percentage rounded to nearest integer.
41
+ const total = Number(it.total) > 0 ? Number(it.total) : 100;
42
+ if (total === 100) return `${Math.round(Number(it.value) || 0)}%`;
43
+ return `${Math.round(pctFor(it))}%`;
44
+ }
45
+
46
+ const slide = {
47
+ section: opts.section,
48
+ title: opts.title,
49
+ transition: opts.transition,
50
+ render(el) {
51
+ const rows = items.map((it, i) => {
52
+ const colorCls = it.color ? ` ${it.color}` : '';
53
+ const p = pctFor(it);
54
+ return `
55
+ <div class="progress-row${colorCls}"
56
+ data-step="${i + 1}"
57
+ data-pct="${p}"
58
+ data-stage-key="Progress/item[${i}]">
59
+ <div class="progress-label" data-stage-edit="items[${i}].label">${escape(it.label || '')}</div>
60
+ <div class="progress-track" data-stage-key="Progress/item[${i}]/track">
61
+ <div class="progress-fill" style="width: 0%;"></div>
62
+ </div>
63
+ <div class="progress-value" data-stage-edit="items[${i}].value">${escape(displayFor(it))}</div>
64
+ </div>
65
+ `;
66
+ }).join('');
67
+
68
+ el.innerHTML = `
69
+ <div class="progress-list" data-stage-key="Progress">
70
+ ${rows}
71
+ </div>
72
+ `;
73
+
74
+ if (reveal === 'instant') {
75
+ el.querySelectorAll('.progress-row').forEach(row => {
76
+ row.classList.add('in');
77
+ const fill = row.querySelector('.progress-fill');
78
+ if (fill) {
79
+ fill.style.transition = 'none';
80
+ fill.style.width = `${row.dataset.pct}%`;
81
+ // eslint-disable-next-line no-unused-expressions
82
+ fill.offsetWidth;
83
+ fill.style.transition = '';
84
+ }
85
+ });
86
+ }
87
+ }
88
+ };
89
+
90
+ if (reveal === 'animated') {
91
+ slide.init = function (el) {
92
+ const rows = Array.from(el.querySelectorAll('.progress-row'));
93
+ const timers = [];
94
+ rows.forEach((row, i) => {
95
+ timers.push(setTimeout(() => {
96
+ row.classList.add('in');
97
+ const fill = row.querySelector('.progress-fill');
98
+ if (fill) fill.style.width = `${row.dataset.pct}%`;
99
+ }, 200 + i * 180));
100
+ });
101
+ return () => timers.forEach(clearTimeout);
102
+ };
103
+ slide.replay = function (el) {
104
+ el.querySelectorAll('.progress-row').forEach(row => {
105
+ row.classList.remove('in');
106
+ const fill = row.querySelector('.progress-fill');
107
+ if (fill) {
108
+ fill.style.transition = 'none';
109
+ fill.style.width = '0%';
110
+ // eslint-disable-next-line no-unused-expressions
111
+ fill.offsetWidth;
112
+ fill.style.transition = '';
113
+ }
114
+ });
115
+ return this.init(el);
116
+ };
117
+ } else if (reveal === 'per-click') {
118
+ slide.steps = items.length;
119
+ slide.onStep = function (el, step) {
120
+ el.querySelectorAll('.progress-row').forEach((row) => {
121
+ const s = Number(row.dataset.step);
122
+ const on = s <= step;
123
+ row.classList.toggle('in', on);
124
+ const fill = row.querySelector('.progress-fill');
125
+ if (fill) fill.style.width = on ? `${row.dataset.pct}%` : '0%';
126
+ });
127
+ };
128
+ }
129
+
130
+ return slide;
131
+ };
132
+
133
+ function escape(s) {
134
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
135
+ }
136
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Punchline — small buildup lines fade in, then a massive payoff lands.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Punchline({
8
+ * section: 4,
9
+ * title: '04 · The setup',
10
+ * buildup: [
11
+ * 'We hired the best engineers.',
12
+ * 'We bought the best tools.',
13
+ * 'We followed the best practices.'
14
+ * ],
15
+ * payoff: 'And we still shipped the bug.',
16
+ * reveal: 'auto' // 'auto' (1.2s pacing) | 'manual' (per-click)
17
+ * }));
18
+ *
19
+ * Edit paths:
20
+ * buildup[i] / payoff
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ Stage.Punchline = function (opts) {
27
+ const buildup = Array.isArray(opts.buildup) ? opts.buildup : [];
28
+ const payoff = opts.payoff || '';
29
+ const reveal = opts.reveal || 'auto';
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="punchline" data-stage-key="Punchline">
38
+ <div class="pl-buildup">
39
+ ${buildup.map((b, i) => `
40
+ <div class="pl-line" data-step="${i + 1}" data-stage-edit="buildup[${i}]" data-stage-key="Punchline/build[${i}]">${escape(b)}</div>
41
+ `).join('')}
42
+ </div>
43
+ <div class="pl-payoff" data-step="${buildup.length + 1}" data-stage-edit="payoff" data-stage-key="Punchline/payoff">${escape(payoff)}</div>
44
+ </div>
45
+ `;
46
+ }
47
+ };
48
+
49
+ if (reveal === 'auto') {
50
+ slide.init = function (el) {
51
+ const lines = el.querySelectorAll('.pl-line');
52
+ const pay = el.querySelector('.pl-payoff');
53
+ const timers = [];
54
+ lines.forEach((n, i) => {
55
+ timers.push(setTimeout(() => n.classList.add('in'), 200 + i * 1200));
56
+ });
57
+ timers.push(setTimeout(() => pay && pay.classList.add('in'), 200 + lines.length * 1200 + 400));
58
+ return () => timers.forEach(clearTimeout);
59
+ };
60
+ slide.replay = function (el) {
61
+ el.querySelectorAll('.pl-line, .pl-payoff').forEach(n => n.classList.remove('in'));
62
+ return this.init(el);
63
+ };
64
+ } else {
65
+ // 'manual' — per-click, one buildup line per step, payoff last
66
+ slide.steps = buildup.length + 1;
67
+ slide.onStep = function (el, step) {
68
+ el.querySelectorAll('.pl-line').forEach(n => {
69
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
70
+ });
71
+ const pay = el.querySelector('.pl-payoff');
72
+ if (pay) pay.classList.toggle('in', step >= (buildup.length + 1));
73
+ };
74
+ }
75
+
76
+ return slide;
77
+ };
78
+
79
+ function escape(s) {
80
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
81
+ }
82
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Pyramid — stacked horizontal layers (clip-path trapezoids).
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Pyramid({
8
+ * section: 6,
9
+ * title: '06 · Trust pyramid',
10
+ * orientation: 'up', // 'up' | 'down' (up = top narrow)
11
+ * layers: [
12
+ * { label: 'Vision', body: 'rare', color: 'accent' },
13
+ * { label: 'Taste', body: 'felt', color: 'blue' },
14
+ * { label: 'Skill', body: 'practiced' },
15
+ * { label: 'Effort', body: 'available', color: 'dim' }
16
+ * ],
17
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
18
+ * }));
19
+ *
20
+ * Reveal order: bottom-up for 'up'; top-down for 'down'.
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ Stage.Pyramid = function (opts) {
27
+ const layers = opts.layers || [];
28
+ const reveal = opts.reveal || 'instant';
29
+ const orientation = opts.orientation === 'down' ? 'down' : 'up';
30
+ const n = layers.length;
31
+
32
+ // Width ratio per layer index (0 = top in render order).
33
+ // For 'up' shape: top layer is narrowest, bottom widest.
34
+ // For 'down': inverted.
35
+ function widthFor(i) {
36
+ if (n <= 1) return 100;
37
+ const min = 32; // %
38
+ const max = 100;
39
+ const t = i / (n - 1);
40
+ const ratio = orientation === 'up' ? t : (1 - t);
41
+ return min + (max - min) * ratio;
42
+ }
43
+
44
+ const slide = {
45
+ section: opts.section,
46
+ title: opts.title,
47
+ transition: opts.transition,
48
+ render(el) {
49
+ el.innerHTML = `
50
+ <div class="pyramid pyramid--${orientation}" data-stage-key="Pyramid">
51
+ ${layers.map((layer, i) => {
52
+ const w = widthFor(i).toFixed(2);
53
+ // Reveal order: for 'up' we reveal bottom-to-top -> step n-i, for 'down' top-to-bottom -> step i+1.
54
+ const step = orientation === 'up' ? (n - i) : (i + 1);
55
+ return `
56
+ <div class="pyr-layer ${layer.color ? 'pyc-' + escape(layer.color) : ''}"
57
+ data-step="${step}"
58
+ style="--pyr-w: ${w}%;"
59
+ data-stage-key="Pyramid/layer[${i}]">
60
+ <div class="pyr-shape">
61
+ <div class="pyr-content">
62
+ <div class="pyr-label" data-stage-edit="layers[${i}].label" data-stage-key="Pyramid/layer[${i}]/label">${escape(layer.label || '')}</div>
63
+ ${layer.body ? `<div class="pyr-body" data-stage-edit="layers[${i}].body" data-stage-key="Pyramid/layer[${i}]/body">${escape(layer.body)}</div>` : ''}
64
+ </div>
65
+ </div>
66
+ </div>
67
+ `;
68
+ }).join('')}
69
+ </div>
70
+ `;
71
+ if (reveal === 'instant') {
72
+ el.querySelectorAll('.pyr-layer').forEach(n => n.classList.add('in'));
73
+ }
74
+ }
75
+ };
76
+
77
+ if (reveal === 'staggered') {
78
+ slide.init = function (el) {
79
+ // Walk in reveal-order (low step first).
80
+ const nodes = Array.from(el.querySelectorAll('.pyr-layer'))
81
+ .sort((a, b) => Number(a.dataset.step) - Number(b.dataset.step));
82
+ const timers = [];
83
+ nodes.forEach((node, i) => {
84
+ timers.push(setTimeout(() => node.classList.add('in'), 200 + i * 240));
85
+ });
86
+ return () => timers.forEach(clearTimeout);
87
+ };
88
+ slide.replay = function (el) {
89
+ el.querySelectorAll('.pyr-layer').forEach(n => n.classList.remove('in'));
90
+ return this.init(el);
91
+ };
92
+ } else if (reveal === 'per-click') {
93
+ slide.steps = layers.length;
94
+ slide.onStep = function (el, step) {
95
+ el.querySelectorAll('.pyr-layer').forEach(n => {
96
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
97
+ });
98
+ };
99
+ }
100
+
101
+ return slide;
102
+ };
103
+
104
+ function escape(s) {
105
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
106
+ }
107
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.QandA — large rhetorical question with a big answer below.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.QandA({
8
+ * section: 2,
9
+ * title: '02 · The question',
10
+ * question: 'What if the bottleneck was never the typing?',
11
+ * answer: 'It was always the thinking.',
12
+ * attribution: 'after years of waiting for compilers' // optional, small
13
+ * }));
14
+ *
15
+ * Edit paths:
16
+ * question / answer / attribution
17
+ */
18
+
19
+ (function (root) {
20
+ const Stage = root.Stage = root.Stage || {};
21
+
22
+ Stage.QandA = function (opts) {
23
+ const question = opts.question || '';
24
+ const answer = opts.answer || '';
25
+ const attribution = opts.attribution || '';
26
+
27
+ return {
28
+ section: opts.section,
29
+ title: opts.title,
30
+ transition: opts.transition,
31
+ render(el) {
32
+ el.innerHTML = `
33
+ <div class="qanda" data-stage-key="QandA">
34
+ <div class="qa-q" data-stage-edit="question" data-stage-key="QandA/question">${escape(question)}</div>
35
+ <div class="qa-a" data-stage-edit="answer" data-stage-key="QandA/answer">${escape(answer)}</div>
36
+ ${attribution ? `<div class="qa-attr" data-stage-edit="attribution" data-stage-key="QandA/attribution">— ${escape(attribution)}</div>` : ''}
37
+ </div>
38
+ `;
39
+ },
40
+ init(el) {
41
+ const q = el.querySelector('.qa-q');
42
+ const a = el.querySelector('.qa-a');
43
+ const attr = el.querySelector('.qa-attr');
44
+ const timers = [];
45
+ timers.push(setTimeout(() => q && q.classList.add('in'), 150));
46
+ timers.push(setTimeout(() => a && a.classList.add('in'), 900));
47
+ timers.push(setTimeout(() => attr && attr.classList.add('in'), 1500));
48
+ return () => timers.forEach(clearTimeout);
49
+ },
50
+ replay(el) {
51
+ el.querySelectorAll('.qa-q, .qa-a, .qa-attr').forEach(n => n.classList.remove('in'));
52
+ return this.init(el);
53
+ }
54
+ };
55
+ };
56
+
57
+ function escape(s) {
58
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
59
+ }
60
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Quote — centered pull quote with attribution line.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Quote({
8
+ * section: 5,
9
+ * title: '05 · Voices',
10
+ * quote: 'The model is a junior teammate who never sleeps and never learns.',
11
+ * author: 'A senior engineer',
12
+ * role: 'platform team', // optional
13
+ * source: 'internal Slack' // optional
14
+ * }));
15
+ *
16
+ * Edit paths: quote / author / role / source
17
+ */
18
+
19
+ (function (root) {
20
+ const Stage = root.Stage = root.Stage || {};
21
+
22
+ Stage.Quote = function (opts) {
23
+ const quote = opts.quote || '';
24
+ const author = opts.author || '';
25
+ const role = opts.role || '';
26
+ const source = opts.source || '';
27
+
28
+ return {
29
+ section: opts.section,
30
+ title: opts.title,
31
+ transition: opts.transition,
32
+ render(el) {
33
+ const meta = [
34
+ author ? `<span class="q-author" data-stage-edit="author" data-stage-key="Quote/author">${escape(author)}</span>` : '',
35
+ role ? `<span class="q-role" data-stage-edit="role" data-stage-key="Quote/role">${escape(role)}</span>` : '',
36
+ source ? `<span class="q-source" data-stage-edit="source" data-stage-key="Quote/source">${escape(source)}</span>` : ''
37
+ ].filter(Boolean).join('<span class="q-sep">·</span>');
38
+
39
+ el.innerHTML = `
40
+ <div class="quote" data-stage-key="Quote">
41
+ <div class="q-mark q-mark--open" aria-hidden="true">&#10077;</div>
42
+ <blockquote class="q-text" data-stage-edit="quote" data-stage-key="Quote/text">${escape(quote)}</blockquote>
43
+ <div class="q-mark q-mark--close" aria-hidden="true">&#10078;</div>
44
+ ${meta ? `<div class="q-meta" data-stage-key="Quote/meta">${meta}</div>` : ''}
45
+ </div>
46
+ `;
47
+ },
48
+ init(el) {
49
+ const timers = [];
50
+ const openMark = el.querySelector('.q-mark--open');
51
+ const text = el.querySelector('.q-text');
52
+ const closeMark = el.querySelector('.q-mark--close');
53
+ const meta = el.querySelector('.q-meta');
54
+ timers.push(setTimeout(() => openMark && openMark.classList.add('in'), 100));
55
+ timers.push(setTimeout(() => text && text.classList.add('in'), 400));
56
+ timers.push(setTimeout(() => closeMark && closeMark.classList.add('in'), 900));
57
+ timers.push(setTimeout(() => meta && meta.classList.add('in'), 1200));
58
+ return () => timers.forEach(clearTimeout);
59
+ },
60
+ replay(el) {
61
+ el.querySelectorAll('.q-mark, .q-text, .q-meta').forEach(n => n.classList.remove('in'));
62
+ return this.init(el);
63
+ }
64
+ };
65
+ };
66
+
67
+ function escape(s) {
68
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
69
+ }
70
+ })(typeof window !== 'undefined' ? window : globalThis);