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,147 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Gauge — semi-circular gauge with animated arc.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Gauge({
8
+ * section: 5,
9
+ * title: '05 · system load',
10
+ * value: 72,
11
+ * max: 100, // optional, default 100
12
+ * label: 'CPU load', // optional
13
+ * color: 'accent', // optional
14
+ * ticks: 5 // optional, marks on the arc
15
+ * }));
16
+ *
17
+ * Arc fills from 0 → value/max ratio on init.
18
+ *
19
+ * Edit paths: value / max / label
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 200x120, center (100, 100), radius 80, arc spans 180 degrees
35
+ const CX = 100;
36
+ const CY = 100;
37
+ const R = 80;
38
+
39
+ function polar(angleDeg) {
40
+ // angle 0 = left (180° from positive x), 180 = right (0°)
41
+ // Math: start at left (180°), end at right (0°) — semi-circle on top
42
+ const rad = (180 - angleDeg) * Math.PI / 180;
43
+ return { x: CX + R * Math.cos(rad), y: CY - R * Math.sin(rad) };
44
+ }
45
+
46
+ function arcPath(startDeg, endDeg) {
47
+ const s = polar(startDeg);
48
+ const e = polar(endDeg);
49
+ const large = (endDeg - startDeg) > 180 ? 1 : 0;
50
+ return `M ${s.x.toFixed(2)} ${s.y.toFixed(2)} A ${R} ${R} 0 ${large} 1 ${e.x.toFixed(2)} ${e.y.toFixed(2)}`;
51
+ }
52
+
53
+ Stage.Gauge = function (opts) {
54
+ const target = Number(opts.value) || 0;
55
+ const max = Number(opts.max) || 100;
56
+ const label = opts.label || '';
57
+ const color = opts.color || 'accent';
58
+ const ticks = Math.max(0, Math.min(20, Number(opts.ticks) || 0));
59
+ const strokeColor = COLOR_MAP[color] || COLOR_MAP.accent;
60
+
61
+ const ratio = Math.max(0, Math.min(1, target / max));
62
+
63
+ return {
64
+ section: opts.section,
65
+ title: opts.title,
66
+ transition: opts.transition,
67
+ render(el) {
68
+ const trackPath = arcPath(0, 180);
69
+ const fullArcPath = arcPath(0, 180);
70
+
71
+ // Tick marks
72
+ let tickSvg = '';
73
+ if (ticks > 0) {
74
+ const tickArr = [];
75
+ for (let i = 0; i <= ticks; i++) {
76
+ const deg = (i / ticks) * 180;
77
+ const outer = polar(deg);
78
+ const innerR = R - 10;
79
+ const rad = (180 - deg) * Math.PI / 180;
80
+ const ix = CX + innerR * Math.cos(rad);
81
+ const iy = CY - innerR * Math.sin(rad);
82
+ tickArr.push(`<line class="g-tick" x1="${outer.x.toFixed(2)}" y1="${outer.y.toFixed(2)}" x2="${ix.toFixed(2)}" y2="${iy.toFixed(2)}"/>`);
83
+ }
84
+ tickSvg = tickArr.join('');
85
+ }
86
+
87
+ el.innerHTML = `
88
+ <div class="gauge gauge--${escape(color)}" data-stage-key="Gauge">
89
+ <div class="g-svg-wrap">
90
+ <svg class="g-svg" viewBox="0 0 200 120" preserveAspectRatio="xMidYMid meet">
91
+ <path class="g-track" d="${trackPath}" fill="none" stroke="var(--dim-2)" stroke-width="14" stroke-linecap="round"/>
92
+ <path class="g-fill" d="${fullArcPath}"
93
+ data-stage-key="Gauge/arc"
94
+ fill="none"
95
+ stroke="${strokeColor}"
96
+ stroke-width="14"
97
+ stroke-linecap="round"
98
+ pathLength="100"
99
+ stroke-dasharray="0 100"/>
100
+ ${tickSvg}
101
+ </svg>
102
+ <div class="g-readout" data-stage-key="Gauge/readout">
103
+ <div class="g-num"><span class="g-num-val" data-stage-edit="value" data-target="${target}">0</span><span class="g-num-max" data-stage-edit="max">/ ${escape(String(max))}</span></div>
104
+ ${label ? `<div class="g-label" data-stage-edit="label" data-stage-key="Gauge/label">${escape(label)}</div>` : ''}
105
+ </div>
106
+ </div>
107
+ </div>
108
+ `;
109
+ },
110
+ init(el) {
111
+ const fill = el.querySelector('.g-fill');
112
+ const numEl = el.querySelector('.g-num-val');
113
+ const duration = 1400;
114
+ const start = performance.now();
115
+ const isInt = Number.isInteger(target);
116
+ let rafId;
117
+ function tick(now) {
118
+ const t = Math.min(1, (now - start) / duration);
119
+ const eased = 1 - Math.pow(1 - t, 3);
120
+ const progress = eased * ratio;
121
+ if (fill) {
122
+ fill.setAttribute('stroke-dasharray', `${(progress * 100).toFixed(2)} ${(100 - progress * 100).toFixed(2)}`);
123
+ }
124
+ if (numEl) {
125
+ const val = target * eased;
126
+ numEl.textContent = isInt ? Math.floor(val).toLocaleString() : val.toFixed(1);
127
+ }
128
+ if (t < 1) rafId = requestAnimationFrame(tick);
129
+ else if (numEl) numEl.textContent = isInt ? target.toLocaleString() : String(target);
130
+ }
131
+ rafId = requestAnimationFrame(tick);
132
+ return () => cancelAnimationFrame(rafId);
133
+ },
134
+ replay(el) {
135
+ const fill = el.querySelector('.g-fill');
136
+ const numEl = el.querySelector('.g-num-val');
137
+ if (fill) fill.setAttribute('stroke-dasharray', '0 100');
138
+ if (numEl) numEl.textContent = '0';
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,141 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Heatmap — grid of color-intensity cells (GitHub-contribution style).
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Heatmap({
8
+ * section: 5,
9
+ * title: '05 · activity',
10
+ * rows: 7, cols: 12,
11
+ * data: [
12
+ * [0, 1, 3, ...], // row 0
13
+ * [1, 2, 4, ...], // row 1
14
+ * ...
15
+ * ],
16
+ * min: 0, max: 10, // optional
17
+ * xLabels: ['Jan','Feb',...], // optional
18
+ * yLabels: ['Mon','Tue',...], // optional
19
+ * color: 'accent' // optional, default accent
20
+ * }));
21
+ *
22
+ * Cells fade in with a soft staggered sweep.
23
+ *
24
+ * Edit paths: xLabels[i], yLabels[i]
25
+ */
26
+
27
+ (function (root) {
28
+ const Stage = root.Stage = root.Stage || {};
29
+
30
+ const COLOR_MAP = {
31
+ accent: 'var(--accent)',
32
+ amber: 'var(--amber)',
33
+ blue: 'var(--blue)',
34
+ red: 'var(--red)',
35
+ fg: 'var(--fg)'
36
+ };
37
+
38
+ Stage.Heatmap = function (opts) {
39
+ const rows = Math.max(1, Number(opts.rows) || (opts.data ? opts.data.length : 1));
40
+ const cols = Math.max(1, Number(opts.cols) || (opts.data && opts.data[0] ? opts.data[0].length : 1));
41
+ const data = opts.data || [];
42
+ const xLabels = opts.xLabels || [];
43
+ const yLabels = opts.yLabels || [];
44
+ const color = opts.color || 'accent';
45
+ const baseColor = COLOR_MAP[color] || COLOR_MAP.accent;
46
+
47
+ // Find min/max if not provided
48
+ let min = Number.isFinite(opts.min) ? Number(opts.min) : Infinity;
49
+ let max = Number.isFinite(opts.max) ? Number(opts.max) : -Infinity;
50
+ if (!Number.isFinite(opts.min) || !Number.isFinite(opts.max)) {
51
+ data.forEach(row => (row || []).forEach(v => {
52
+ const n = Number(v) || 0;
53
+ if (!Number.isFinite(opts.min) && n < min) min = n;
54
+ if (!Number.isFinite(opts.max) && n > max) max = n;
55
+ }));
56
+ }
57
+ if (!Number.isFinite(min)) min = 0;
58
+ if (!Number.isFinite(max)) max = 1;
59
+ if (max === min) max = min + 1;
60
+
61
+ function intensity(v) {
62
+ const n = Number(v) || 0;
63
+ return Math.max(0, Math.min(1, (n - min) / (max - min)));
64
+ }
65
+
66
+ return {
67
+ section: opts.section,
68
+ title: opts.title,
69
+ transition: opts.transition,
70
+ render(el) {
71
+ const xLabelRow = xLabels.length
72
+ ? `<div class="hm-x-labels">
73
+ <div class="hm-corner"></div>
74
+ <div class="hm-x-row">
75
+ ${Array.from({ length: cols }, (_, c) => `
76
+ <div class="hm-x-label" data-stage-edit="xLabels[${c}]">${escape(xLabels[c] || '')}</div>
77
+ `).join('')}
78
+ </div>
79
+ </div>`
80
+ : '';
81
+
82
+ const cellRows = Array.from({ length: rows }, (_, r) => {
83
+ const yLab = yLabels[r] || '';
84
+ const cells = Array.from({ length: cols }, (_, c) => {
85
+ const value = (data[r] || [])[c];
86
+ const inten = intensity(value);
87
+ return `<div class="hm-cell"
88
+ data-r="${r}" data-c="${c}"
89
+ data-stage-key="Heatmap/cell[${r}][${c}]"
90
+ style="--hm-intensity: ${inten.toFixed(3)};"
91
+ title="${escape(String(value ?? ''))}"></div>`;
92
+ }).join('');
93
+ return `<div class="hm-row">
94
+ ${yLabels.length ? `<div class="hm-y-label" data-stage-edit="yLabels[${r}]">${escape(yLab)}</div>` : ''}
95
+ <div class="hm-cells">${cells}</div>
96
+ </div>`;
97
+ }).join('');
98
+
99
+ el.innerHTML = `
100
+ <div class="heatmap heatmap--${escape(color)}"
101
+ style="--hm-base: ${baseColor}; --hm-cols: ${cols}; --hm-rows: ${rows};"
102
+ data-stage-key="Heatmap">
103
+ ${xLabelRow}
104
+ <div class="hm-body">
105
+ ${cellRows}
106
+ </div>
107
+ <div class="hm-scale">
108
+ <span class="hm-scale-label">less</span>
109
+ <span class="hm-scale-step" style="--hm-intensity: 0.05;"></span>
110
+ <span class="hm-scale-step" style="--hm-intensity: 0.3;"></span>
111
+ <span class="hm-scale-step" style="--hm-intensity: 0.55;"></span>
112
+ <span class="hm-scale-step" style="--hm-intensity: 0.8;"></span>
113
+ <span class="hm-scale-step" style="--hm-intensity: 1;"></span>
114
+ <span class="hm-scale-label">more</span>
115
+ </div>
116
+ </div>
117
+ `;
118
+ },
119
+ init(el) {
120
+ const cells = Array.from(el.querySelectorAll('.hm-cell'));
121
+ const timers = [];
122
+ cells.forEach((cell, i) => {
123
+ const r = Number(cell.dataset.r);
124
+ const c = Number(cell.dataset.c);
125
+ // Diagonal sweep: top-left to bottom-right
126
+ const delay = 80 + (r + c) * 35;
127
+ timers.push(setTimeout(() => cell.classList.add('in'), delay));
128
+ });
129
+ return () => timers.forEach(clearTimeout);
130
+ },
131
+ replay(el) {
132
+ el.querySelectorAll('.hm-cell').forEach(c => c.classList.remove('in'));
133
+ return this.init(el);
134
+ }
135
+ };
136
+ };
137
+
138
+ function escape(s) {
139
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
140
+ }
141
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.ImageGrid — gallery of images in a grid, with optional captions.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.ImageGrid({
8
+ * section: 6,
9
+ * title: '06 · Field notes',
10
+ * columns: 3,
11
+ * images: [
12
+ * { src: 'https://picsum.photos/seed/a/600/400', alt: 'team', caption: 'team offsite' },
13
+ * { src: 'https://picsum.photos/seed/b/600/400', alt: 'console', caption: 'staging burn' },
14
+ * { src: 'https://picsum.photos/seed/c/600/400', caption: '4 a.m. ship' }
15
+ * ],
16
+ * reveal: 'cascade' // 'cascade' (fall-in w/ rotation) | 'staggered' | 'instant'
17
+ * }));
18
+ *
19
+ * Edit paths:
20
+ * images[i].src / images[i].alt / images[i].caption
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ Stage.ImageGrid = function (opts) {
27
+ const images = Array.isArray(opts.images) ? opts.images : [];
28
+ const columns = [2, 3, 4].includes(opts.columns) ? opts.columns : 3;
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="image-grid ig-cols-${columns} ig--${escape(reveal)}" data-stage-key="ImageGrid">
38
+ ${images.map((img, i) => `
39
+ <figure class="ig-cell" data-step="${i + 1}" data-stage-key="ImageGrid/cell[${i}]">
40
+ <div class="ig-frame">
41
+ <img class="ig-img"
42
+ src="${escape(img.src || '')}"
43
+ alt="${escape(img.alt || '')}"
44
+ loading="lazy"
45
+ data-stage-edit="images[${i}].src">
46
+ </div>
47
+ ${img.caption ? `<figcaption class="ig-cap" data-stage-edit="images[${i}].caption" data-stage-key="ImageGrid/cell[${i}]/caption">${escape(img.caption)}</figcaption>` : ''}
48
+ </figure>
49
+ `).join('')}
50
+ </div>
51
+ `;
52
+ if (reveal === 'instant') {
53
+ el.querySelectorAll('.ig-cell').forEach(n => n.classList.add('in'));
54
+ }
55
+ }
56
+ };
57
+
58
+ if (reveal === 'staggered' || reveal === 'cascade') {
59
+ slide.init = function (el) {
60
+ const cells = el.querySelectorAll('.ig-cell');
61
+ const timers = [];
62
+ const step = reveal === 'cascade' ? 220 : 150;
63
+ cells.forEach((n, i) => {
64
+ timers.push(setTimeout(() => n.classList.add('in'), 150 + i * step));
65
+ });
66
+ return () => timers.forEach(clearTimeout);
67
+ };
68
+ slide.replay = function (el) {
69
+ el.querySelectorAll('.ig-cell').forEach(n => n.classList.remove('in'));
70
+ return this.init(el);
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);
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.ImageText — two-column image + text.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.ImageText({
8
+ * section: 2,
9
+ * title: '02 · The view',
10
+ * image: { src: 'https://...', alt: 'Field of antennas' },
11
+ * side: 'left', // 'left' | 'right'
12
+ * heading: 'A new shape of work',
13
+ * body: 'You stop typing characters and start describing intent.',
14
+ * caption: 'Field notes, week 14', // optional
15
+ * reveal: 'staggered' // 'staggered' | 'instant'
16
+ * }));
17
+ *
18
+ * The body string is split on \n into lines for the staggered reveal.
19
+ *
20
+ * Edit paths:
21
+ * image.src / image.alt / heading / body / caption
22
+ */
23
+
24
+ (function (root) {
25
+ const Stage = root.Stage = root.Stage || {};
26
+
27
+ Stage.ImageText = function (opts) {
28
+ const image = opts.image || {};
29
+ const side = opts.side === 'right' ? 'right' : 'left';
30
+ const reveal = opts.reveal || 'instant';
31
+ const heading = opts.heading || '';
32
+ const body = opts.body || '';
33
+ const caption = opts.caption || '';
34
+ const bodyLines = String(body).split('\n').filter(Boolean);
35
+
36
+ return {
37
+ section: opts.section,
38
+ title: opts.title,
39
+ transition: opts.transition,
40
+ render(el) {
41
+ el.innerHTML = `
42
+ <div class="image-text image-text--${side}" data-stage-key="ImageText">
43
+ <figure class="it-figure" data-stage-key="ImageText/figure">
44
+ <img class="it-img"
45
+ src="${escape(image.src || '')}"
46
+ alt="${escape(image.alt || '')}"
47
+ data-stage-edit="image.src" />
48
+ <div class="it-img-frame"></div>
49
+ </figure>
50
+ <div class="it-text" data-stage-key="ImageText/text">
51
+ <h2 class="it-heading" data-stage-edit="heading" data-stage-key="ImageText/heading">${escape(heading)}</h2>
52
+ <div class="it-body" data-stage-key="ImageText/body">
53
+ ${bodyLines.map((line, i) => `
54
+ <div class="it-line" data-stage-edit="body" data-line-index="${i}">${escape(line)}</div>
55
+ `).join('')}
56
+ </div>
57
+ ${caption ? `<div class="it-caption" data-stage-edit="caption" data-stage-key="ImageText/caption">${escape(caption)}</div>` : ''}
58
+ </div>
59
+ </div>
60
+ `;
61
+ if (reveal === 'instant') {
62
+ el.querySelectorAll('.it-figure, .it-heading, .it-line, .it-caption')
63
+ .forEach(n => n.classList.add('in'));
64
+ }
65
+ },
66
+ init(el) {
67
+ if (reveal === 'instant') return () => {};
68
+ const timers = [];
69
+ const figure = el.querySelector('.it-figure');
70
+ const heading = el.querySelector('.it-heading');
71
+ const lines = el.querySelectorAll('.it-line');
72
+ const caption = el.querySelector('.it-caption');
73
+
74
+ timers.push(setTimeout(() => figure && figure.classList.add('in'), 100));
75
+ timers.push(setTimeout(() => heading && heading.classList.add('in'), 500));
76
+ lines.forEach((line, i) => {
77
+ timers.push(setTimeout(() => line.classList.add('in'), 800 + i * 220));
78
+ });
79
+ const captionDelay = 800 + lines.length * 220 + 200;
80
+ if (caption) {
81
+ timers.push(setTimeout(() => caption.classList.add('in'), captionDelay));
82
+ }
83
+ return () => timers.forEach(clearTimeout);
84
+ },
85
+ replay(el) {
86
+ el.querySelectorAll('.it-figure, .it-heading, .it-line, .it-caption')
87
+ .forEach(n => n.classList.remove('in'));
88
+ return this.init(el);
89
+ }
90
+ };
91
+ };
92
+
93
+ function escape(s) {
94
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
95
+ }
96
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.KineticText — multi-line staggered reveal.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.KineticText({
8
+ * section: 2,
9
+ * title: '02 · The shift',
10
+ * lines: [
11
+ * { text: 'You start with a sentence.', color: 'fg' },
12
+ * { text: 'You end with the sentence', color: 'dim' },
13
+ * { text: 'rewritten.', color: 'accent', pause: 800 }
14
+ * ],
15
+ * pace: 800
16
+ * }));
17
+ *
18
+ * Colors: 'fg' (default) | 'dim' | 'accent' | 'amber' | 'blue'.
19
+ * pause: ms before this specific line appears (in addition to pace).
20
+ *
21
+ * Layer-3 (inline edit): each line's text is tagged with
22
+ * data-stage-edit="lines[N].text"
23
+ * Layer-2 (element-pin notes): each line has
24
+ * data-stage-key="KineticText/line[N]"
25
+ */
26
+
27
+ (function (root) {
28
+ const Stage = root.Stage = root.Stage || {};
29
+
30
+ Stage.KineticText = function (opts) {
31
+ const lines = opts.lines || [];
32
+ const pace = opts.pace ?? 800;
33
+
34
+ return {
35
+ section: opts.section,
36
+ title: opts.title,
37
+ transition: opts.transition,
38
+ render(el) {
39
+ el.classList.add('kinetic');
40
+ el.innerHTML = '';
41
+ const wrap = document.createElement('div');
42
+ wrap.className = 'kinetic-wrap';
43
+ wrap.dataset.stageKey = 'KineticText';
44
+ lines.forEach((line, i) => {
45
+ const span = document.createElement('div');
46
+ span.className = 'line ' + (line.color || 'fg');
47
+ span.textContent = line.text;
48
+ span.dataset.stageEdit = `lines[${i}].text`;
49
+ span.dataset.stageKey = `KineticText/line[${i}]`;
50
+ wrap.appendChild(span);
51
+ });
52
+ el.appendChild(wrap);
53
+ },
54
+ init(el) {
55
+ const nodes = el.querySelectorAll('.line');
56
+ const timers = [];
57
+ let acc = 0;
58
+ nodes.forEach((n, i) => {
59
+ const line = lines[i] || {};
60
+ const delay = (line.pause ?? 0) + (i === 0 ? 200 : pace);
61
+ acc += delay;
62
+ timers.push(setTimeout(() => n.classList.add('in'), acc));
63
+ });
64
+ return () => timers.forEach(clearTimeout);
65
+ },
66
+ replay(el) {
67
+ el.querySelectorAll('.line').forEach(n => n.classList.remove('in'));
68
+ return this.init(el);
69
+ }
70
+ };
71
+ };
72
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.KPI — hero metric card with optional trend.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.KPI({
8
+ * section: 5,
9
+ * title: '05 · weekly active',
10
+ * value: 12480,
11
+ * unit: '',
12
+ * label: 'weekly active developers',
13
+ * change: { value: 12.4, direction: 'up', period: 'vs. last week' },
14
+ * color: 'accent',
15
+ * icon: 'group'
16
+ * }));
17
+ *
18
+ * On init the number counts up from 0 → value.
19
+ * Direction 'up' colors the trend green/accent, 'down' colors it red.
20
+ *
21
+ * Edit paths: value / unit / label / change.value / change.period
22
+ */
23
+
24
+ (function (root) {
25
+ const Stage = root.Stage = root.Stage || {};
26
+
27
+ Stage.KPI = function (opts) {
28
+ const target = Number(opts.value) || 0;
29
+ const unit = opts.unit || '';
30
+ const label = opts.label || '';
31
+ const change = opts.change || null;
32
+ const color = opts.color || 'accent';
33
+ const icon = opts.icon || '';
34
+
35
+ return {
36
+ section: opts.section,
37
+ title: opts.title,
38
+ transition: opts.transition,
39
+ render(el) {
40
+ const dirClass = change ? (change.direction === 'down' ? 'down' : 'up') : '';
41
+ const arrow = change
42
+ ? (change.direction === 'down' ? '▼' : '▲')
43
+ : '';
44
+ el.innerHTML = `
45
+ <div class="kpi kpi--${escape(color)}" data-stage-key="KPI">
46
+ <div class="kpi-head" data-stage-key="KPI/head">
47
+ ${icon ? `<span class="kpi-icon material-symbols-outlined">${escape(icon)}</span>` : ''}
48
+ <span class="kpi-label" data-stage-edit="label" data-stage-key="KPI/label">${escape(label)}</span>
49
+ </div>
50
+ <div class="kpi-figure" data-stage-key="KPI/figure">
51
+ <span class="kpi-num" data-stage-edit="value" data-target="${target}">0</span>${unit ? `<span class="kpi-unit" data-stage-edit="unit">${escape(unit)}</span>` : ''}
52
+ </div>
53
+ ${change ? `
54
+ <div class="kpi-change ${dirClass}" data-stage-key="KPI/change">
55
+ <span class="kpi-arrow">${arrow}</span>
56
+ <span class="kpi-change-num" data-stage-edit="change.value">${escape(String(change.value))}%</span>
57
+ <span class="kpi-change-period" data-stage-edit="change.period">${escape(change.period || '')}</span>
58
+ </div>
59
+ ` : ''}
60
+ </div>
61
+ `;
62
+ },
63
+ init(el) {
64
+ const numEl = el.querySelector('.kpi-num');
65
+ const card = el.querySelector('.kpi');
66
+ if (!numEl) return () => {};
67
+ const duration = 1200;
68
+ const start = performance.now();
69
+ const isInt = Number.isInteger(target);
70
+ let rafId;
71
+ function tick(now) {
72
+ const t = Math.min(1, (now - start) / duration);
73
+ const eased = 1 - Math.pow(1 - t, 3);
74
+ const value = target * eased;
75
+ numEl.textContent = isInt ? Math.floor(value).toLocaleString() : value.toFixed(1);
76
+ if (t < 1) rafId = requestAnimationFrame(tick);
77
+ else numEl.textContent = isInt ? target.toLocaleString() : String(target);
78
+ }
79
+ rafId = requestAnimationFrame(tick);
80
+
81
+ const timers = [];
82
+ timers.push(setTimeout(() => card && card.classList.add('in'), 80));
83
+ const changeEl = el.querySelector('.kpi-change');
84
+ if (changeEl) timers.push(setTimeout(() => changeEl.classList.add('in'), 1100));
85
+
86
+ return () => {
87
+ cancelAnimationFrame(rafId);
88
+ timers.forEach(clearTimeout);
89
+ };
90
+ },
91
+ replay(el) {
92
+ const numEl = el.querySelector('.kpi-num');
93
+ if (numEl) numEl.textContent = '0';
94
+ const card = el.querySelector('.kpi');
95
+ if (card) card.classList.remove('in');
96
+ const changeEl = el.querySelector('.kpi-change');
97
+ if (changeEl) changeEl.classList.remove('in');
98
+ return this.init(el);
99
+ }
100
+ };
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);