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,130 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Roadmap — horizontal timeline with swimlanes.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Roadmap({
8
+ * section: 5,
9
+ * title: '05 · 2026 roadmap',
10
+ * months: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
11
+ * lanes: [
12
+ * { label: 'Platform', color: 'accent', bars: [
13
+ * { start: 0, end: 3, label: 'rewrite core' },
14
+ * { start: 4, end: 7, label: 'observability' }
15
+ * ]},
16
+ * { label: 'Product', color: 'blue', bars: [
17
+ * { start: 2, end: 6, label: 'collab editor' }
18
+ * ]}
19
+ * ],
20
+ * reveal: 'staggered' // 'instant' | 'staggered'
21
+ * }));
22
+ *
23
+ * `start` and `end` are indices into `months` (or arbitrary numbers if no months).
24
+ *
25
+ * Edit paths: lanes[i].label, lanes[i].bars[j].label, months[k]
26
+ */
27
+
28
+ (function (root) {
29
+ const Stage = root.Stage = root.Stage || {};
30
+
31
+ Stage.Roadmap = function (opts) {
32
+ const lanes = opts.lanes || [];
33
+ const months = opts.months || [];
34
+ const reveal = opts.reveal || 'staggered';
35
+
36
+ // Determine min/max for normalisation
37
+ let lo = months.length ? 0 : Infinity;
38
+ let hi = months.length ? months.length : -Infinity;
39
+ if (!months.length) {
40
+ lanes.forEach(lane => (lane.bars || []).forEach(b => {
41
+ const s = Number(b.start);
42
+ const e = Number(b.end);
43
+ if (Number.isFinite(s) && s < lo) lo = s;
44
+ if (Number.isFinite(e) && e > hi) hi = e;
45
+ }));
46
+ }
47
+ if (!Number.isFinite(lo)) lo = 0;
48
+ if (!Number.isFinite(hi)) hi = 1;
49
+ const span = (hi - lo) || 1;
50
+
51
+ function pct(v) {
52
+ return ((Number(v) - lo) / span) * 100;
53
+ }
54
+
55
+ const slide = {
56
+ section: opts.section,
57
+ title: opts.title,
58
+ transition: opts.transition,
59
+ render(el) {
60
+ // Month headers
61
+ const monthCells = months.length
62
+ ? months.map((m, i) => `<div class="rm-month" data-stage-edit="months[${i}]">${escape(m)}</div>`).join('')
63
+ : '';
64
+
65
+ const laneRows = lanes.map((lane, i) => {
66
+ const colorClass = lane.color ? `rm-${escape(lane.color)}` : '';
67
+ const bars = (lane.bars || []).map((b, j) => {
68
+ const left = pct(b.start);
69
+ const right = pct(b.end);
70
+ const w = Math.max(0, right - left);
71
+ return `<div class="rm-bar ${colorClass}"
72
+ data-i="${i}" data-j="${j}"
73
+ data-stage-key="Roadmap/lane[${i}]/bar[${j}]"
74
+ style="--rm-left: ${left.toFixed(2)}%; --rm-width: ${w.toFixed(2)}%;">
75
+ <span class="rm-bar-label" data-stage-edit="lanes[${i}].bars[${j}].label">${escape(b.label || '')}</span>
76
+ </div>`;
77
+ }).join('');
78
+ return `<div class="rm-lane" data-stage-key="Roadmap/lane[${i}]">
79
+ <div class="rm-lane-label ${colorClass}" data-stage-edit="lanes[${i}].label">${escape(lane.label || '')}</div>
80
+ <div class="rm-lane-track">${bars}</div>
81
+ </div>`;
82
+ }).join('');
83
+
84
+ el.innerHTML = `
85
+ <div class="roadmap" data-stage-key="Roadmap">
86
+ ${months.length ? `
87
+ <div class="rm-header">
88
+ <div class="rm-lane-label-spacer"></div>
89
+ <div class="rm-months" style="--rm-cols: ${months.length};">${monthCells}</div>
90
+ </div>
91
+ ` : ''}
92
+ <div class="rm-body">
93
+ ${laneRows}
94
+ </div>
95
+ </div>
96
+ `;
97
+
98
+ if (reveal === 'instant') {
99
+ el.querySelectorAll('.rm-bar, .rm-lane').forEach(n => n.classList.add('in'));
100
+ }
101
+ }
102
+ };
103
+
104
+ if (reveal === 'staggered') {
105
+ slide.init = function (el) {
106
+ const lanesEl = Array.from(el.querySelectorAll('.rm-lane'));
107
+ const barsEl = Array.from(el.querySelectorAll('.rm-bar'));
108
+ const timers = [];
109
+ lanesEl.forEach((lane, i) => {
110
+ timers.push(setTimeout(() => lane.classList.add('in'), 150 + i * 180));
111
+ });
112
+ barsEl.forEach((bar, i) => {
113
+ const laneIdx = Number(bar.dataset.i);
114
+ timers.push(setTimeout(() => bar.classList.add('in'), 400 + laneIdx * 180 + Number(bar.dataset.j) * 120));
115
+ });
116
+ return () => timers.forEach(clearTimeout);
117
+ };
118
+ slide.replay = function (el) {
119
+ el.querySelectorAll('.rm-bar, .rm-lane').forEach(n => n.classList.remove('in'));
120
+ return this.init(el);
121
+ };
122
+ }
123
+
124
+ return slide;
125
+ };
126
+
127
+ function escape(s) {
128
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
129
+ }
130
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.SectionCard — section divider with number + title + tag.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.SectionCard({
8
+ * section: 2,
9
+ * number: '02',
10
+ * title: 'What changed',
11
+ * tag: 'the shift in cost'
12
+ * }));
13
+ */
14
+
15
+ (function (root) {
16
+ const Stage = root.Stage = root.Stage || {};
17
+
18
+ Stage.SectionCard = function (opts) {
19
+ return {
20
+ section: opts.section,
21
+ title: opts.title || `${opts.number} · ${opts.titleText || ''}`.trim(),
22
+ transition: opts.transition,
23
+ render(el) {
24
+ el.innerHTML = `
25
+ <div class="section-card stagger" data-stage-key="SectionCard">
26
+ <div class="sec-rule">
27
+ <div class="sec-line"></div>
28
+ <div class="sec-num" data-stage-edit="number" data-stage-key="SectionCard/number">${escape(opts.number || '')}</div>
29
+ <div class="sec-line"></div>
30
+ </div>
31
+ <div class="sec-title" data-stage-edit="title" data-stage-key="SectionCard/title">${escape(opts.title || '')}</div>
32
+ ${opts.tag ? `<div class="sec-tag" data-stage-edit="tag" data-stage-key="SectionCard/tag">${escape(opts.tag)}</div>` : ''}
33
+ </div>
34
+ `;
35
+ },
36
+ init(el) {
37
+ return Stage.staggerIn(el.querySelectorAll('.section-card > *'), 200, 100);
38
+ }
39
+ };
40
+ };
41
+
42
+ function escape(s) {
43
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
44
+ }
45
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.ShiftArrow — "from → to" mental-model shift pattern.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.ShiftArrow({
8
+ * section: 6,
9
+ * title: 'the shift',
10
+ * from: 'writing code',
11
+ * to: 'reviewing code'
12
+ * }));
13
+ */
14
+
15
+ (function (root) {
16
+ const Stage = root.Stage = root.Stage || {};
17
+
18
+ Stage.ShiftArrow = function (opts) {
19
+ return {
20
+ section: opts.section,
21
+ title: opts.title,
22
+ transition: opts.transition,
23
+ render(el) {
24
+ el.innerHTML = `
25
+ <div class="shift-line stagger" data-stage-key="ShiftArrow">
26
+ <div class="shift-from" data-stage-edit="from" data-stage-key="ShiftArrow/from">${escape(opts.from || '')}</div>
27
+ <div class="shift-arrow">→</div>
28
+ <div class="shift-to" data-stage-edit="to" data-stage-key="ShiftArrow/to">${escape(opts.to || '')}</div>
29
+ </div>
30
+ `;
31
+ },
32
+ init(el) {
33
+ return Stage.staggerIn(el.querySelectorAll('.shift-line > *'), 350, 200);
34
+ }
35
+ };
36
+ };
37
+
38
+ function escape(s) {
39
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
40
+ }
41
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.SparkLine — hero number with an inline trend sparkline.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.SparkLine({
8
+ * section: 5,
9
+ * title: '05 · daily commits',
10
+ * value: 247,
11
+ * label: 'commits today',
12
+ * points: [42, 51, 68, 95, 130, 178, 247],
13
+ * color: 'accent',
14
+ * period: 'last 7 days'
15
+ * }));
16
+ *
17
+ * Big number counts up; sparkline draws on init.
18
+ *
19
+ * Edit paths: value / label / period
20
+ */
21
+
22
+ (function (root) {
23
+ const Stage = root.Stage = root.Stage || {};
24
+
25
+ const COLOR_MAP = {
26
+ accent: 'var(--accent)',
27
+ amber: 'var(--amber)',
28
+ blue: 'var(--blue)',
29
+ red: 'var(--red)',
30
+ dim: 'var(--dim)',
31
+ fg: 'var(--fg)'
32
+ };
33
+
34
+ // viewBox 200x60
35
+ const VB_W = 200;
36
+ const VB_H = 60;
37
+ const PAD = 4;
38
+
39
+ Stage.SparkLine = function (opts) {
40
+ const target = Number(opts.value) || 0;
41
+ const label = opts.label || '';
42
+ const points = (opts.points || []).map(p => Number(p) || 0);
43
+ const color = opts.color || 'accent';
44
+ const period = opts.period || '';
45
+ const stroke = COLOR_MAP[color] || COLOR_MAP.accent;
46
+
47
+ const pMax = points.length ? Math.max(...points) : 1;
48
+ const pMin = points.length ? Math.min(...points) : 0;
49
+ const range = pMax - pMin || 1;
50
+
51
+ function xAt(i, n) {
52
+ if (n <= 1) return VB_W / 2;
53
+ return PAD + (i / (n - 1)) * (VB_W - 2 * PAD);
54
+ }
55
+ function yAt(v) {
56
+ return VB_H - PAD - ((v - pMin) / range) * (VB_H - 2 * PAD);
57
+ }
58
+
59
+ const path = points.length
60
+ ? points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${xAt(i, points.length).toFixed(2)} ${yAt(p).toFixed(2)}`).join(' ')
61
+ : '';
62
+ const areaPath = points.length
63
+ ? `${path} L ${xAt(points.length - 1, points.length).toFixed(2)} ${VB_H - PAD} L ${xAt(0, points.length).toFixed(2)} ${VB_H - PAD} Z`
64
+ : '';
65
+
66
+ return {
67
+ section: opts.section,
68
+ title: opts.title,
69
+ transition: opts.transition,
70
+ render(el) {
71
+ el.innerHTML = `
72
+ <div class="sparkline sparkline--${escape(color)}" data-stage-key="SparkLine">
73
+ <div class="sl-figure">
74
+ <span class="sl-num" data-stage-edit="value" data-target="${target}">0</span>
75
+ </div>
76
+ <div class="sl-label" data-stage-edit="label" data-stage-key="SparkLine/label">${escape(label)}</div>
77
+ <div class="sl-svg-wrap">
78
+ <svg class="sl-svg" viewBox="0 0 ${VB_W} ${VB_H}" preserveAspectRatio="none">
79
+ ${areaPath ? `<path class="sl-area" d="${areaPath}" fill="${stroke}" fill-opacity="0.14"/>` : ''}
80
+ ${path ? `<path class="sl-line" d="${path}" stroke="${stroke}" fill="none"/>` : ''}
81
+ ${points.length ? `<circle class="sl-tip" cx="${xAt(points.length - 1, points.length).toFixed(2)}" cy="${yAt(points[points.length - 1]).toFixed(2)}" r="3" fill="${stroke}"/>` : ''}
82
+ </svg>
83
+ </div>
84
+ ${period ? `<div class="sl-period" data-stage-edit="period">${escape(period)}</div>` : ''}
85
+ </div>
86
+ `;
87
+ },
88
+ init(el) {
89
+ const numEl = el.querySelector('.sl-num');
90
+ const line = el.querySelector('.sl-line');
91
+ const area = el.querySelector('.sl-area');
92
+ const tip = el.querySelector('.sl-tip');
93
+ const duration = 1200;
94
+ const start = performance.now();
95
+ const isInt = Number.isInteger(target);
96
+ let rafId;
97
+
98
+ function tick(now) {
99
+ const t = Math.min(1, (now - start) / duration);
100
+ const eased = 1 - Math.pow(1 - t, 3);
101
+ const val = target * eased;
102
+ if (numEl) numEl.textContent = isInt ? Math.floor(val).toLocaleString() : val.toFixed(1);
103
+ if (t < 1) rafId = requestAnimationFrame(tick);
104
+ else if (numEl) numEl.textContent = isInt ? target.toLocaleString() : String(target);
105
+ }
106
+ rafId = requestAnimationFrame(tick);
107
+
108
+ const timers = [];
109
+ if (line) {
110
+ const length = line.getTotalLength ? line.getTotalLength() : 500;
111
+ line.style.strokeDasharray = `${length}`;
112
+ line.style.strokeDashoffset = `${length}`;
113
+ // eslint-disable-next-line no-unused-expressions
114
+ line.getBoundingClientRect();
115
+ timers.push(setTimeout(() => {
116
+ line.style.transition = 'stroke-dashoffset 1300ms cubic-bezier(0.16, 1, 0.3, 1)';
117
+ line.style.strokeDashoffset = '0';
118
+ }, 200));
119
+ }
120
+ if (area) timers.push(setTimeout(() => area.classList.add('in'), 800));
121
+ if (tip) timers.push(setTimeout(() => tip.classList.add('in'), 1400));
122
+
123
+ return () => {
124
+ cancelAnimationFrame(rafId);
125
+ timers.forEach(clearTimeout);
126
+ };
127
+ },
128
+ replay(el) {
129
+ const numEl = el.querySelector('.sl-num');
130
+ if (numEl) numEl.textContent = '0';
131
+ const line = el.querySelector('.sl-line');
132
+ if (line) {
133
+ line.style.transition = 'none';
134
+ const length = line.getTotalLength ? line.getTotalLength() : 500;
135
+ line.style.strokeDasharray = `${length}`;
136
+ line.style.strokeDashoffset = `${length}`;
137
+ }
138
+ el.querySelectorAll('.sl-area, .sl-tip').forEach(n => n.classList.remove('in'));
139
+ return this.init(el);
140
+ }
141
+ };
142
+ };
143
+
144
+ function escape(s) {
145
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
146
+ }
147
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Spotlight — one focal item with dimmer supporting items beneath.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Spotlight({
8
+ * section: 7,
9
+ * title: '07 · The one thing',
10
+ * focus: {
11
+ * icon: 'auto_awesome',
12
+ * heading: 'Taste',
13
+ * body: 'The scarce part. Still yours to bring.'
14
+ * },
15
+ * context: ['speed', 'scale', 'cost', 'tooling'],
16
+ * reveal: 'staggered' // 'staggered' | 'instant'
17
+ * }));
18
+ *
19
+ * Edit paths:
20
+ * focus.heading / focus.body / focus.icon / context[i]
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ Stage.Spotlight = function (opts) {
27
+ const focus = opts.focus || {};
28
+ const context = Array.isArray(opts.context) ? opts.context : [];
29
+ const reveal = opts.reveal || 'instant';
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="spotlight" data-stage-key="Spotlight">
38
+ <div class="sp-focus" data-stage-key="Spotlight/focus">
39
+ ${focus.icon ? `<span class="sp-icon material-symbols-outlined" data-stage-edit="focus.icon">${escape(focus.icon)}</span>` : ''}
40
+ <div class="sp-focus-text">
41
+ <div class="sp-heading" data-stage-edit="focus.heading" data-stage-key="Spotlight/focus/heading">${escape(focus.heading || '')}</div>
42
+ ${focus.body ? `<div class="sp-body" data-stage-edit="focus.body" data-stage-key="Spotlight/focus/body">${escape(focus.body)}</div>` : ''}
43
+ </div>
44
+ </div>
45
+ ${context.length ? `
46
+ <div class="sp-context" data-stage-key="Spotlight/context">
47
+ ${context.map((c, i) => `
48
+ <div class="sp-ctx-item" data-step="${i + 1}" data-stage-edit="context[${i}]" data-stage-key="Spotlight/context[${i}]">${escape(c)}</div>
49
+ `).join('')}
50
+ </div>
51
+ ` : ''}
52
+ </div>
53
+ `;
54
+ if (reveal === 'instant') {
55
+ const focusEl = el.querySelector('.sp-focus');
56
+ if (focusEl) focusEl.classList.add('in');
57
+ el.querySelectorAll('.sp-ctx-item').forEach(n => n.classList.add('in'));
58
+ }
59
+ }
60
+ };
61
+
62
+ if (reveal === 'staggered') {
63
+ slide.init = function (el) {
64
+ const focusEl = el.querySelector('.sp-focus');
65
+ const ctx = el.querySelectorAll('.sp-ctx-item');
66
+ const timers = [];
67
+ timers.push(setTimeout(() => focusEl && focusEl.classList.add('in'), 150));
68
+ ctx.forEach((n, i) => {
69
+ timers.push(setTimeout(() => n.classList.add('in'), 700 + i * 140));
70
+ });
71
+ return () => timers.forEach(clearTimeout);
72
+ };
73
+ slide.replay = function (el) {
74
+ el.querySelectorAll('.sp-focus, .sp-ctx-item').forEach(n => n.classList.remove('in'));
75
+ return this.init(el);
76
+ };
77
+ }
78
+
79
+ return slide;
80
+ };
81
+
82
+ function escape(s) {
83
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
84
+ }
85
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Statement — one massive declarative sentence, centered.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Statement({
8
+ * section: 1,
9
+ * title: '01 · Thesis',
10
+ * text: 'We are not in the business of making widgets. We are in the business of trust.',
11
+ * emphasis: ['trust'], // substrings within text get accent color
12
+ * color: 'accent' // 'accent' | 'amber' | 'blue' — controls emphasis color
13
+ * }));
14
+ *
15
+ * On init the text reveals word by word.
16
+ *
17
+ * Edit paths:
18
+ * text (the whole sentence; emphasis is structural, not inline-editable)
19
+ */
20
+
21
+ (function (root) {
22
+ const Stage = root.Stage = root.Stage || {};
23
+
24
+ Stage.Statement = function (opts) {
25
+ const text = opts.text || '';
26
+ const emphasis = Array.isArray(opts.emphasis) ? opts.emphasis : [];
27
+ const color = opts.color || 'accent';
28
+
29
+ return {
30
+ section: opts.section,
31
+ title: opts.title,
32
+ transition: opts.transition,
33
+ render(el) {
34
+ el.classList.add('statement-host');
35
+ // Build emphasized HTML, then split by whitespace into word spans so we
36
+ // can stagger reveal. We escape first, then wrap emphasis spans.
37
+ const emphasized = emphasizeHTML(text, emphasis, color);
38
+ const words = splitWords(emphasized);
39
+
40
+ el.innerHTML = `
41
+ <div class="statement" data-stage-key="Statement">
42
+ <p class="statement-text" data-stage-edit="text" data-stage-key="Statement/text">${
43
+ words.map((w, i) => `<span class="st-word" data-i="${i}">${w}</span>`).join(' ')
44
+ }</p>
45
+ </div>
46
+ `;
47
+ },
48
+ init(el) {
49
+ const words = el.querySelectorAll('.st-word');
50
+ const timers = [];
51
+ words.forEach((w, i) => {
52
+ timers.push(setTimeout(() => w.classList.add('in'), 120 + i * 90));
53
+ });
54
+ return () => timers.forEach(clearTimeout);
55
+ },
56
+ replay(el) {
57
+ el.querySelectorAll('.st-word').forEach(n => n.classList.remove('in'));
58
+ return this.init(el);
59
+ }
60
+ };
61
+ };
62
+
63
+ // Build escaped HTML where each emphasis substring becomes
64
+ // <span class="st-em st-em--accent">...</span>.
65
+ function emphasizeHTML(raw, emphasis, color) {
66
+ let html = escape(raw);
67
+ // Sort longest first so substrings don't shadow longer matches.
68
+ const sorted = [...emphasis].filter(Boolean).sort((a, b) => b.length - a.length);
69
+ for (const phrase of sorted) {
70
+ const esc = escape(phrase);
71
+ const re = new RegExp(escapeRegex(esc), 'g');
72
+ html = html.replace(re, `<span class="st-em st-em--${escape(color)}">${esc}</span>`);
73
+ }
74
+ return html;
75
+ }
76
+
77
+ // Split into word tokens, preserving emphasis tags intact (don't split inside <span>).
78
+ function splitWords(html) {
79
+ // Tokenize: keep tag-blocks atomic by splitting on plain whitespace
80
+ // but only outside tags. Simple state machine.
81
+ const tokens = [];
82
+ let buf = '';
83
+ let depth = 0;
84
+ for (let i = 0; i < html.length; i++) {
85
+ const c = html[i];
86
+ if (c === '<') depth++;
87
+ if (c === ' ' && depth === 0) {
88
+ if (buf) tokens.push(buf);
89
+ buf = '';
90
+ } else {
91
+ buf += c;
92
+ }
93
+ if (c === '>') depth--;
94
+ }
95
+ if (buf) tokens.push(buf);
96
+ return tokens;
97
+ }
98
+
99
+ function escapeRegex(s) {
100
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
101
+ }
102
+
103
+ function escape(s) {
104
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
105
+ }
106
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Stats — grid of statistic mini-cards.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Stats({
8
+ * section: 6,
9
+ * title: '06 · The numbers',
10
+ * blocks: [
11
+ * { number: 92, unit: '%', label: 'cost-per-token drop', color: 'accent' },
12
+ * { number: 7, unit: 'x', label: 'context window growth', color: 'amber' },
13
+ * { number: 3, unit: 'd', label: 'avg PR turnaround', color: 'blue' }
14
+ * ],
15
+ * columns: 3 // optional, defaults to blocks.length up to 4
16
+ * }));
17
+ *
18
+ * On init each number counts up from 0 → target, staggered by ~200ms.
19
+ *
20
+ * Edit paths: blocks[i].number / blocks[i].unit / blocks[i].label
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ Stage.Stats = function (opts) {
27
+ const blocks = opts.blocks || [];
28
+ const columns = Math.min(4, opts.columns || Math.min(4, blocks.length || 1));
29
+
30
+ return {
31
+ section: opts.section,
32
+ title: opts.title,
33
+ transition: opts.transition,
34
+ render(el) {
35
+ el.innerHTML = `
36
+ <div class="stats stats--cols-${columns}" data-stage-key="Stats">
37
+ ${blocks.map((b, i) => `
38
+ <div class="stat ${b.color ? 'stat--' + b.color : ''}" data-stage-key="Stats/block[${i}]">
39
+ <div class="stat-figure">
40
+ <span class="stat-num" data-stage-edit="blocks[${i}].number" data-target="${Number(b.number) || 0}">0</span>${b.unit ? `<span class="stat-unit" data-stage-edit="blocks[${i}].unit">${escape(b.unit)}</span>` : ''}
41
+ </div>
42
+ <div class="stat-label" data-stage-edit="blocks[${i}].label">${escape(b.label || '')}</div>
43
+ </div>
44
+ `).join('')}
45
+ </div>
46
+ `;
47
+ },
48
+ init(el) {
49
+ const nums = el.querySelectorAll('.stat-num');
50
+ const cards = el.querySelectorAll('.stat');
51
+ const duration = 1200;
52
+ const stagger = 200;
53
+ const rafs = [];
54
+ const timers = [];
55
+ cards.forEach((card, i) => {
56
+ timers.push(setTimeout(() => card.classList.add('in'), 100 + i * stagger));
57
+ });
58
+ nums.forEach((numEl, i) => {
59
+ const target = Number(numEl.dataset.target) || 0;
60
+ const isInt = Number.isInteger(target);
61
+ const begin = performance.now() + 200 + i * stagger;
62
+ let rafId;
63
+ function tick(now) {
64
+ if (now < begin) { rafId = requestAnimationFrame(tick); rafs.push(rafId); return; }
65
+ const t = Math.min(1, (now - begin) / duration);
66
+ const eased = 1 - Math.pow(1 - t, 3);
67
+ const value = target * eased;
68
+ numEl.textContent = isInt ? Math.floor(value).toLocaleString() : value.toFixed(1);
69
+ if (t < 1) { rafId = requestAnimationFrame(tick); rafs.push(rafId); }
70
+ else numEl.textContent = isInt ? target.toLocaleString() : String(target);
71
+ }
72
+ rafId = requestAnimationFrame(tick);
73
+ rafs.push(rafId);
74
+ });
75
+ return () => {
76
+ rafs.forEach(cancelAnimationFrame);
77
+ timers.forEach(clearTimeout);
78
+ };
79
+ },
80
+ replay(el) {
81
+ el.querySelectorAll('.stat-num').forEach(n => n.textContent = '0');
82
+ el.querySelectorAll('.stat').forEach(n => n.classList.remove('in'));
83
+ return this.init(el);
84
+ }
85
+ };
86
+ };
87
+
88
+ function escape(s) {
89
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
90
+ }
91
+ })(typeof window !== 'undefined' ? window : globalThis);