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,79 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Agenda — vertical schedule with time on left, label on right.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Agenda({
8
+ * section: 63,
9
+ * title: '63 · Today',
10
+ * items: [
11
+ * { time: '09:00', label: 'Welcome', duration: '15 min', icon: 'waving_hand' },
12
+ * { time: '09:15', label: 'Keynote', duration: '45 min', icon: 'campaign' },
13
+ * { time: '10:00', label: 'Coffee', duration: '20 min', icon: 'coffee' }
14
+ * ],
15
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
16
+ * }));
17
+ *
18
+ * Edit paths: items[i].time / .label / .duration / .icon
19
+ */
20
+
21
+ (function (root) {
22
+ const Stage = root.Stage = root.Stage || {};
23
+
24
+ Stage.Agenda = 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="agenda" data-stage-key="Agenda">
35
+ <div class="agenda-rail"></div>
36
+ ${items.map((it, i) => `
37
+ <div class="agenda-item" data-step="${i + 1}" data-stage-key="Agenda/item[${i}]">
38
+ ${it.time ? `<div class="agenda-time" data-stage-edit="items[${i}].time">${escape(it.time)}</div>` : `<div class="agenda-time"></div>`}
39
+ <div class="agenda-dot">
40
+ ${it.icon ? `<span class="material-symbols-outlined" data-stage-edit="items[${i}].icon">${escape(it.icon)}</span>` : ''}
41
+ </div>
42
+ <div class="agenda-body">
43
+ <div class="agenda-label" data-stage-edit="items[${i}].label">${escape(it.label || '')}</div>
44
+ ${it.duration ? `<div class="agenda-duration" data-stage-edit="items[${i}].duration">${escape(it.duration)}</div>` : ''}
45
+ </div>
46
+ </div>
47
+ `).join('')}
48
+ </div>
49
+ `;
50
+ if (reveal === 'instant') {
51
+ el.querySelectorAll('.agenda-item').forEach(n => n.classList.add('in'));
52
+ }
53
+ }
54
+ };
55
+
56
+ if (reveal === 'staggered') {
57
+ slide.init = function (el) {
58
+ return Stage.staggerIn(el.querySelectorAll('.agenda-item'), 140, 180);
59
+ };
60
+ slide.replay = function (el) {
61
+ el.querySelectorAll('.agenda-item').forEach(n => n.classList.remove('in'));
62
+ return this.init(el);
63
+ };
64
+ } else if (reveal === 'per-click') {
65
+ slide.steps = items.length;
66
+ slide.onStep = function (el, step) {
67
+ el.querySelectorAll('.agenda-item').forEach(n => {
68
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
69
+ });
70
+ };
71
+ }
72
+
73
+ return slide;
74
+ };
75
+
76
+ function escape(s) {
77
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
78
+ }
79
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.BarChart — simple horizontal or vertical bar chart.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.BarChart({
8
+ * section: 4,
9
+ * title: '04 · where the hours go',
10
+ * orientation: 'horizontal', // default
11
+ * bars: [
12
+ * { label: 'Coding', value: 6, color: 'accent' },
13
+ * { label: 'Meetings', value: 3, color: 'amber' },
14
+ * { label: 'Slack', value: 2, color: 'blue' },
15
+ * { label: 'Thinking', value: 1, color: 'dim' }
16
+ * ],
17
+ * unit: ' h',
18
+ * reveal: 'animated' // 'instant' | 'animated' | 'per-click'
19
+ * }));
20
+ *
21
+ * `maxValue` defaults to max(values) * 1.1.
22
+ * Animation: CSS transitions on width/height when `.fill` class lands.
23
+ *
24
+ * Layer-3 (inline edit): bars[i].label, bars[i].value, unit, title.
25
+ */
26
+
27
+ (function (root) {
28
+ const Stage = root.Stage = root.Stage || {};
29
+
30
+ Stage.BarChart = function (opts) {
31
+ const bars = opts.bars || [];
32
+ const orientation = opts.orientation === 'vertical' ? 'vertical' : 'horizontal';
33
+ const unit = opts.unit || '';
34
+ const reveal = opts.reveal || 'instant';
35
+ const values = bars.map(b => Number(b.value) || 0);
36
+ const computedMax = values.length ? Math.max(...values) : 1;
37
+ const maxValue = Number(opts.maxValue) > 0
38
+ ? Number(opts.maxValue)
39
+ : (computedMax > 0 ? computedMax * 1.1 : 1);
40
+
41
+ function pct(v) {
42
+ const p = (Number(v) || 0) / maxValue * 100;
43
+ return Math.max(0, Math.min(100, p));
44
+ }
45
+
46
+ function formatValue(v) {
47
+ // Keep simple — leave numeric formatting to the caller via value/unit.
48
+ return `${v}${unit}`;
49
+ }
50
+
51
+ const slide = {
52
+ section: opts.section,
53
+ title: opts.title,
54
+ transition: opts.transition,
55
+ render(el) {
56
+ const sizeProp = orientation === 'vertical' ? 'height' : 'width';
57
+ const rows = bars.map((b, i) => {
58
+ const colorCls = b.color ? ` ${b.color}` : '';
59
+ const p = pct(b.value);
60
+ return `
61
+ <div class="bar-row${colorCls}"
62
+ data-step="${i + 1}"
63
+ data-pct="${p}"
64
+ data-stage-key="BarChart/bar[${i}]">
65
+ <div class="bar-label" data-stage-edit="bars[${i}].label">${escape(b.label || '')}</div>
66
+ <div class="bar-track" data-stage-key="BarChart/bar[${i}]/track">
67
+ <div class="bar-fill" style="${sizeProp}: 0%;"></div>
68
+ </div>
69
+ <div class="bar-value" data-stage-edit="bars[${i}].value">${escape(formatValue(b.value))}</div>
70
+ </div>
71
+ `;
72
+ }).join('');
73
+
74
+ el.innerHTML = `
75
+ <div class="barchart ${orientation}" data-stage-key="BarChart">
76
+ ${rows}
77
+ </div>
78
+ `;
79
+
80
+ if (reveal === 'instant') {
81
+ // Place rows + fills in their final state with no transition.
82
+ el.querySelectorAll('.bar-row').forEach(n => n.classList.add('in'));
83
+ el.querySelectorAll('.bar-row').forEach(row => {
84
+ const fill = row.querySelector('.bar-fill');
85
+ const p = row.dataset.pct;
86
+ if (fill) {
87
+ // Suppress the transition for the initial paint only.
88
+ fill.style.transition = 'none';
89
+ fill.style[sizeProp] = `${p}%`;
90
+ // Force reflow then restore transition.
91
+ // eslint-disable-next-line no-unused-expressions
92
+ fill.offsetWidth;
93
+ fill.style.transition = '';
94
+ }
95
+ });
96
+ }
97
+ }
98
+ };
99
+
100
+ const sizeProp = orientation === 'vertical' ? 'height' : 'width';
101
+
102
+ function fillRow(row, immediate) {
103
+ const fill = row.querySelector('.bar-fill');
104
+ if (!fill) return;
105
+ const p = row.dataset.pct;
106
+ if (immediate) {
107
+ fill.style.transition = 'none';
108
+ fill.style[sizeProp] = `${p}%`;
109
+ // eslint-disable-next-line no-unused-expressions
110
+ fill.offsetWidth;
111
+ fill.style.transition = '';
112
+ } else {
113
+ fill.style[sizeProp] = `${p}%`;
114
+ }
115
+ }
116
+
117
+ if (reveal === 'animated') {
118
+ slide.init = function (el) {
119
+ const rows = Array.from(el.querySelectorAll('.bar-row'));
120
+ const timers = [];
121
+ rows.forEach((row, i) => {
122
+ timers.push(setTimeout(() => {
123
+ row.classList.add('in');
124
+ fillRow(row, false);
125
+ }, 200 + i * 220));
126
+ });
127
+ return () => timers.forEach(clearTimeout);
128
+ };
129
+ slide.replay = function (el) {
130
+ el.querySelectorAll('.bar-row').forEach(row => {
131
+ row.classList.remove('in');
132
+ const fill = row.querySelector('.bar-fill');
133
+ if (fill) {
134
+ fill.style.transition = 'none';
135
+ fill.style[sizeProp] = '0%';
136
+ // eslint-disable-next-line no-unused-expressions
137
+ fill.offsetWidth;
138
+ fill.style.transition = '';
139
+ }
140
+ });
141
+ return this.init(el);
142
+ };
143
+ } else if (reveal === 'per-click') {
144
+ slide.steps = bars.length;
145
+ slide.onStep = function (el, step) {
146
+ el.querySelectorAll('.bar-row').forEach((row) => {
147
+ const s = Number(row.dataset.step);
148
+ const on = s <= step;
149
+ row.classList.toggle('in', on);
150
+ const fill = row.querySelector('.bar-fill');
151
+ if (fill) fill.style[sizeProp] = on ? `${row.dataset.pct}%` : '0%';
152
+ });
153
+ };
154
+ }
155
+
156
+ return slide;
157
+ };
158
+
159
+ function escape(s) {
160
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
161
+ }
162
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.BeforeAfter — split comparison: image-vs-image or text-vs-text.
5
+ *
6
+ * Usage (text vs text):
7
+ * Stage.register(Stage.BeforeAfter({
8
+ * section: 69,
9
+ * title: '69 · Before / After',
10
+ * before: { label: 'Before', text: 'Manual reviews. 3-day cycles.' },
11
+ * after: { label: 'After', text: 'AI-drafted reviews. 30-minute cycles.' },
12
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'slider'
13
+ * }));
14
+ *
15
+ * Usage (image vs image with slider):
16
+ * Stage.register(Stage.BeforeAfter({
17
+ * section: 69,
18
+ * before: { label: 'Before', image: 'https://picsum.photos/seed/before/1200/800' },
19
+ * after: { label: 'After', image: 'https://picsum.photos/seed/after/1200/800' },
20
+ * reveal: 'slider'
21
+ * }));
22
+ *
23
+ * `reveal: 'slider'` animates a diagonal divider from 0 to 50% on init (image mode).
24
+ *
25
+ * Edit paths: before.label / before.text / after.label / after.text
26
+ */
27
+
28
+ (function (root) {
29
+ const Stage = root.Stage = root.Stage || {};
30
+
31
+ Stage.BeforeAfter = function (opts) {
32
+ const before = opts.before || {};
33
+ const after = opts.after || {};
34
+ const reveal = opts.reveal || 'instant';
35
+ const isImage = !!(before.image || after.image);
36
+ const mode = isImage ? 'image' : 'text';
37
+
38
+ const slide = {
39
+ section: opts.section,
40
+ title: opts.title,
41
+ transition: opts.transition,
42
+ render(el) {
43
+ if (mode === 'image') {
44
+ el.innerHTML = `
45
+ <div class="before-after before-after--image" data-stage-key="BeforeAfter" style="--ba-clip: 0%;">
46
+ <div class="ba-pane ba-before" data-stage-key="BeforeAfter/before">
47
+ ${before.image ? `<img class="ba-img" src="${escape(before.image)}" alt="${escape(before.label || 'before')}">` : ''}
48
+ <div class="ba-label ba-label--left">
49
+ <span class="ba-tag">${escape(before.label || 'Before')}</span>
50
+ </div>
51
+ </div>
52
+ <div class="ba-pane ba-after" data-stage-key="BeforeAfter/after">
53
+ ${after.image ? `<img class="ba-img" src="${escape(after.image)}" alt="${escape(after.label || 'after')}">` : ''}
54
+ <div class="ba-label ba-label--right">
55
+ <span class="ba-tag ba-tag--accent">${escape(after.label || 'After')}</span>
56
+ </div>
57
+ </div>
58
+ <div class="ba-divider"></div>
59
+ </div>
60
+ `;
61
+ } else {
62
+ el.innerHTML = `
63
+ <div class="before-after before-after--text" data-stage-key="BeforeAfter">
64
+ <div class="ba-col ba-before" data-stage-key="BeforeAfter/before">
65
+ <div class="ba-col-tag">${escape(before.label || 'Before')}</div>
66
+ <div class="ba-col-text" data-stage-edit="before.text">${escape(before.text || '')}</div>
67
+ </div>
68
+ <div class="ba-arrow material-symbols-outlined">arrow_forward</div>
69
+ <div class="ba-col ba-after" data-stage-key="BeforeAfter/after">
70
+ <div class="ba-col-tag ba-col-tag--accent">${escape(after.label || 'After')}</div>
71
+ <div class="ba-col-text" data-stage-edit="after.text">${escape(after.text || '')}</div>
72
+ </div>
73
+ </div>
74
+ `;
75
+ }
76
+
77
+ if (reveal === 'instant') {
78
+ el.querySelectorAll('.ba-pane, .ba-col, .ba-arrow').forEach(n => n.classList.add('in'));
79
+ if (mode === 'image') {
80
+ const root = el.querySelector('.before-after--image');
81
+ if (root) root.style.setProperty('--ba-clip', '50%');
82
+ }
83
+ }
84
+ }
85
+ };
86
+
87
+ if (reveal === 'staggered') {
88
+ slide.init = function (el) {
89
+ const nodes = mode === 'image'
90
+ ? el.querySelectorAll('.ba-pane')
91
+ : el.querySelectorAll('.ba-col, .ba-arrow');
92
+ const cleanup = Stage.staggerIn(nodes, 220, 200);
93
+ let extra;
94
+ if (mode === 'image') {
95
+ extra = setTimeout(() => {
96
+ const root = el.querySelector('.before-after--image');
97
+ if (root) root.style.setProperty('--ba-clip', '50%');
98
+ }, 600);
99
+ }
100
+ return () => { cleanup(); if (extra) clearTimeout(extra); };
101
+ };
102
+ slide.replay = function (el) {
103
+ el.querySelectorAll('.ba-pane, .ba-col, .ba-arrow').forEach(n => n.classList.remove('in'));
104
+ if (mode === 'image') {
105
+ const root = el.querySelector('.before-after--image');
106
+ if (root) root.style.setProperty('--ba-clip', '0%');
107
+ }
108
+ return this.init(el);
109
+ };
110
+ } else if (reveal === 'slider') {
111
+ slide.init = function (el) {
112
+ el.querySelectorAll('.ba-pane, .ba-col, .ba-arrow').forEach(n => n.classList.add('in'));
113
+ const root = el.querySelector('.before-after--image');
114
+ if (!root) return;
115
+ root.style.setProperty('--ba-clip', '0%');
116
+ // Force reflow then animate.
117
+ // eslint-disable-next-line no-unused-expressions
118
+ root.offsetWidth;
119
+ const t = setTimeout(() => root.style.setProperty('--ba-clip', '50%'), 200);
120
+ return () => clearTimeout(t);
121
+ };
122
+ slide.replay = function (el) {
123
+ const root = el.querySelector('.before-after--image');
124
+ if (root) root.style.setProperty('--ba-clip', '0%');
125
+ return this.init(el);
126
+ };
127
+ }
128
+
129
+ return slide;
130
+ };
131
+
132
+ function escape(s) {
133
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
134
+ }
135
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Bento — modular bento grid of cells.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Bento({
8
+ * section: 7,
9
+ * title: '07 · The kit',
10
+ * cells: [
11
+ * { span: 2, heading: 'Agents', body: 'long-running, async, tool-using.', color: 'accent' },
12
+ * { heading: 'Reviews', body: 'human in the loop.' },
13
+ * { heading: 'Eval', body: 'measure, then ship.', color: 'amber' },
14
+ * { heading: 'Specs', image: { src: 'https://...' } },
15
+ * { heading: 'Notes', body: 'cheap, persistent.', color: 'blue' }
16
+ * ]
17
+ * }));
18
+ *
19
+ * Layout: 4-column CSS grid. Cells default to 1 col, `span: 2` makes a cell wider.
20
+ *
21
+ * Edit paths:
22
+ * cells[i].heading / cells[i].body / cells[i].image.src
23
+ */
24
+
25
+ (function (root) {
26
+ const Stage = root.Stage = root.Stage || {};
27
+
28
+ Stage.Bento = function (opts) {
29
+ const cells = opts.cells || [];
30
+
31
+ return {
32
+ section: opts.section,
33
+ title: opts.title,
34
+ transition: opts.transition,
35
+ render(el) {
36
+ el.innerHTML = `
37
+ <div class="bento" data-stage-key="Bento">
38
+ ${cells.map((c, i) => {
39
+ const colorClass = c.color ? `bento-cell--${c.color}` : '';
40
+ const spanClass = c.span === 2 ? 'bento-cell--span-2' : '';
41
+ const hasImage = c.image && c.image.src;
42
+ const bg = hasImage
43
+ ? `style="background-image: linear-gradient(180deg, rgba(10,10,10,0.45), rgba(10,10,10,0.85)), url('${escape(c.image.src)}')"`
44
+ : '';
45
+ return `
46
+ <div class="bento-cell ${spanClass} ${colorClass} ${hasImage ? 'bento-cell--image' : ''}"
47
+ ${bg}
48
+ data-stage-key="Bento/cell[${i}]">
49
+ <div class="bento-cell-body">
50
+ <div class="bento-heading" data-stage-edit="cells[${i}].heading">${escape(c.heading || '')}</div>
51
+ ${c.body ? `<div class="bento-body" data-stage-edit="cells[${i}].body">${escape(c.body)}</div>` : ''}
52
+ </div>
53
+ </div>
54
+ `;
55
+ }).join('')}
56
+ </div>
57
+ `;
58
+ },
59
+ init(el) {
60
+ const nodes = el.querySelectorAll('.bento-cell');
61
+ return Stage.staggerIn(nodes, 120, 150);
62
+ },
63
+ replay(el) {
64
+ el.querySelectorAll('.bento-cell').forEach(n => n.classList.remove('in'));
65
+ return this.init(el);
66
+ }
67
+ };
68
+ };
69
+
70
+ function escape(s) {
71
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
72
+ }
73
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.BigNumber — one massive headline number with a label.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.BigNumber({
8
+ * section: 6,
9
+ * title: '06 · Cost collapse',
10
+ * number: 92,
11
+ * unit: '%', // optional
12
+ * label: 'drop in cost-per-token',
13
+ * caption: '2023 → 2026, frontier models' // optional
14
+ * }));
15
+ *
16
+ * On init the number counts up from 0 → target in ~1.2s.
17
+ *
18
+ * Edit paths: number / unit / label / caption
19
+ */
20
+
21
+ (function (root) {
22
+ const Stage = root.Stage = root.Stage || {};
23
+
24
+ Stage.BigNumber = function (opts) {
25
+ const target = Number(opts.number) || 0;
26
+ const unit = opts.unit || '';
27
+ const label = opts.label || '';
28
+ const caption = opts.caption || '';
29
+
30
+ return {
31
+ section: opts.section,
32
+ title: opts.title,
33
+ transition: opts.transition,
34
+ render(el) {
35
+ el.innerHTML = `
36
+ <div class="big-number" data-stage-key="BigNumber">
37
+ <div class="bn-figure" data-stage-key="BigNumber/figure">
38
+ <span class="bn-num" data-stage-edit="number" data-target="${target}">0</span>${unit ? `<span class="bn-unit" data-stage-edit="unit">${escape(unit)}</span>` : ''}
39
+ </div>
40
+ <div class="bn-label" data-stage-edit="label" data-stage-key="BigNumber/label">${escape(label)}</div>
41
+ ${caption ? `<div class="bn-caption" data-stage-edit="caption" data-stage-key="BigNumber/caption">${escape(caption)}</div>` : ''}
42
+ </div>
43
+ `;
44
+ },
45
+ init(el) {
46
+ const numEl = el.querySelector('.bn-num');
47
+ const unitEl = el.querySelector('.bn-unit');
48
+ const labelEl = el.querySelector('.bn-label');
49
+ const capEl = el.querySelector('.bn-caption');
50
+ const duration = 1200;
51
+ const start = performance.now();
52
+ const isInt = Number.isInteger(target);
53
+ let rafId;
54
+ function tick(now) {
55
+ const t = Math.min(1, (now - start) / duration);
56
+ // easeOutCubic
57
+ const eased = 1 - Math.pow(1 - t, 3);
58
+ const value = target * eased;
59
+ numEl.textContent = isInt ? Math.floor(value).toLocaleString() : value.toFixed(1);
60
+ if (t < 1) rafId = requestAnimationFrame(tick);
61
+ else numEl.textContent = isInt ? target.toLocaleString() : String(target);
62
+ }
63
+ rafId = requestAnimationFrame(tick);
64
+
65
+ const timers = [];
66
+ timers.push(setTimeout(() => unitEl && unitEl.classList.add('in'), 200));
67
+ timers.push(setTimeout(() => labelEl && labelEl.classList.add('in'), 600));
68
+ timers.push(setTimeout(() => capEl && capEl.classList.add('in'), 1100));
69
+
70
+ return () => {
71
+ cancelAnimationFrame(rafId);
72
+ timers.forEach(clearTimeout);
73
+ };
74
+ },
75
+ replay(el) {
76
+ const numEl = el.querySelector('.bn-num');
77
+ if (numEl) numEl.textContent = '0';
78
+ el.querySelectorAll('.bn-unit, .bn-label, .bn-caption').forEach(n => n.classList.remove('in'));
79
+ return this.init(el);
80
+ }
81
+ };
82
+ };
83
+
84
+ function escape(s) {
85
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
86
+ }
87
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Callout — sidebar callout box with colored left border and icon.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Callout({
8
+ * section: 67,
9
+ * title: '67 · Callout',
10
+ * kind: 'tip', // 'info' | 'tip' | 'warning' | 'danger' | 'success'
11
+ * icon: 'lightbulb', // optional override; default per kind
12
+ * heading: 'Pro tip',
13
+ * body: 'Keep your specs short and your reviews shorter.',
14
+ * reveal: 'staggered' // 'instant' | 'staggered'
15
+ * }));
16
+ *
17
+ * Edit paths: heading / body
18
+ */
19
+
20
+ (function (root) {
21
+ const Stage = root.Stage = root.Stage || {};
22
+
23
+ const DEFAULTS = {
24
+ info: { icon: 'info' },
25
+ tip: { icon: 'lightbulb' },
26
+ warning: { icon: 'warning' },
27
+ danger: { icon: 'error' },
28
+ success: { icon: 'check_circle' }
29
+ };
30
+
31
+ Stage.Callout = function (opts) {
32
+ const kind = DEFAULTS[opts.kind] ? opts.kind : 'info';
33
+ const icon = opts.icon || DEFAULTS[kind].icon;
34
+ const reveal = opts.reveal || 'instant';
35
+
36
+ const slide = {
37
+ section: opts.section,
38
+ title: opts.title,
39
+ transition: opts.transition,
40
+ render(el) {
41
+ el.innerHTML = `
42
+ <div class="callout callout--${kind}" data-stage-key="Callout">
43
+ <span class="callout-icon material-symbols-outlined">${escape(icon)}</span>
44
+ <div class="callout-body">
45
+ <div class="callout-heading" data-stage-edit="heading" data-stage-key="Callout/heading">${escape(opts.heading || '')}</div>
46
+ <div class="callout-text" data-stage-edit="body" data-stage-key="Callout/body">${escape(opts.body || '')}</div>
47
+ </div>
48
+ </div>
49
+ `;
50
+ if (reveal === 'instant') {
51
+ el.querySelector('.callout')?.classList.add('in');
52
+ }
53
+ }
54
+ };
55
+
56
+ if (reveal === 'staggered') {
57
+ slide.init = function (el) {
58
+ const co = el.querySelector('.callout');
59
+ const t1 = setTimeout(() => co?.classList.add('in'), 120);
60
+ const t2 = setTimeout(() => co?.classList.add('reveal-text'), 480);
61
+ return () => { clearTimeout(t1); clearTimeout(t2); };
62
+ };
63
+ slide.replay = function (el) {
64
+ el.querySelector('.callout')?.classList.remove('in', 'reveal-text');
65
+ return this.init(el);
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);