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,81 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Checklist — items with checkbox icons; `done: true` items render checked.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Checklist({
8
+ * section: 64,
9
+ * title: '64 · Pre-launch',
10
+ * items: [
11
+ * { text: 'Specs reviewed', done: true },
12
+ * { text: 'Tests green', done: true, body: 'CI passing on all branches' },
13
+ * { text: 'Docs updated' },
14
+ * { text: 'Comms drafted' }
15
+ * ],
16
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
17
+ * }));
18
+ *
19
+ * Style choice: done items show a filled accent checkmark + softened text
20
+ * (no strikethrough — looked cleaner). Undone items show an empty box outline.
21
+ *
22
+ * Edit paths: items[i].text / .body
23
+ */
24
+
25
+ (function (root) {
26
+ const Stage = root.Stage = root.Stage || {};
27
+
28
+ Stage.Checklist = function (opts) {
29
+ const items = opts.items || [];
30
+ const reveal = opts.reveal || 'instant';
31
+
32
+ const slide = {
33
+ section: opts.section,
34
+ title: opts.title,
35
+ transition: opts.transition,
36
+ render(el) {
37
+ el.innerHTML = `
38
+ <div class="checklist" data-stage-key="Checklist">
39
+ ${items.map((it, i) => `
40
+ <div class="checklist-item ${it.done ? 'is-done' : ''}"
41
+ data-step="${i + 1}"
42
+ data-stage-key="Checklist/item[${i}]">
43
+ <span class="checklist-box material-symbols-outlined">${it.done ? 'check_box' : 'check_box_outline_blank'}</span>
44
+ <div class="checklist-body">
45
+ <div class="checklist-text" data-stage-edit="items[${i}].text">${escape(it.text || '')}</div>
46
+ ${it.body ? `<div class="checklist-sub" data-stage-edit="items[${i}].body">${escape(it.body)}</div>` : ''}
47
+ </div>
48
+ </div>
49
+ `).join('')}
50
+ </div>
51
+ `;
52
+ if (reveal === 'instant') {
53
+ el.querySelectorAll('.checklist-item').forEach(n => n.classList.add('in'));
54
+ }
55
+ }
56
+ };
57
+
58
+ if (reveal === 'staggered') {
59
+ slide.init = function (el) {
60
+ return Stage.staggerIn(el.querySelectorAll('.checklist-item'), 130, 180);
61
+ };
62
+ slide.replay = function (el) {
63
+ el.querySelectorAll('.checklist-item').forEach(n => n.classList.remove('in'));
64
+ return this.init(el);
65
+ };
66
+ } else if (reveal === 'per-click') {
67
+ slide.steps = items.length;
68
+ slide.onStep = function (el, step) {
69
+ el.querySelectorAll('.checklist-item').forEach(n => {
70
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
71
+ });
72
+ };
73
+ }
74
+
75
+ return slide;
76
+ };
77
+
78
+ function escape(s) {
79
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
80
+ }
81
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.CodeBlock — monospace code block with optional reveal animations.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.CodeBlock({
8
+ * section: 5,
9
+ * title: '05 · the patch',
10
+ * fileName: 'src/agent.ts',
11
+ * language: 'typescript',
12
+ * code: `function agent(input) {\n return loop(input);\n}`,
13
+ * highlight: [2], // 1-based line numbers
14
+ * reveal: 'typewriter' // 'typewriter' | 'lines' | 'instant'
15
+ * }));
16
+ *
17
+ * Edit paths: code / fileName
18
+ */
19
+
20
+ (function (root) {
21
+ const Stage = root.Stage = root.Stage || {};
22
+
23
+ Stage.CodeBlock = function (opts) {
24
+ const code = String(opts.code || '');
25
+ const fileName = opts.fileName || '';
26
+ const language = opts.language || '';
27
+ const highlight = new Set((opts.highlight || []).map(Number));
28
+ const reveal = opts.reveal || 'instant';
29
+
30
+ const lines = code.split('\n');
31
+
32
+ return {
33
+ section: opts.section,
34
+ title: opts.title,
35
+ transition: opts.transition,
36
+ render(el) {
37
+ const lineHtml = lines.map((line, i) => {
38
+ const lineNum = i + 1;
39
+ const hl = highlight.has(lineNum) ? ' cb-line--highlight' : '';
40
+ return `
41
+ <div class="cb-line${hl}" data-line="${lineNum}" data-stage-key="CodeBlock/line[${i}]">
42
+ <span class="cb-line-num">${lineNum}</span>
43
+ <span class="cb-line-text">${escape(line) || '&nbsp;'}</span>
44
+ </div>
45
+ `;
46
+ }).join('');
47
+
48
+ el.innerHTML = `
49
+ <div class="codeblock" data-stage-key="CodeBlock">
50
+ ${fileName ? `
51
+ <div class="cb-head" data-stage-key="CodeBlock/head">
52
+ <span class="cb-dots"><span></span><span></span><span></span></span>
53
+ <span class="cb-file" data-stage-edit="fileName">${escape(fileName)}</span>
54
+ ${language ? `<span class="cb-lang">${escape(language)}</span>` : ''}
55
+ </div>
56
+ ` : ''}
57
+ <div class="cb-body" data-stage-edit="code">${lineHtml}</div>
58
+ </div>
59
+ `;
60
+
61
+ if (reveal === 'instant') {
62
+ el.querySelectorAll('.cb-line').forEach(n => n.classList.add('in'));
63
+ }
64
+ },
65
+ init(el) {
66
+ const lineNodes = Array.from(el.querySelectorAll('.cb-line'));
67
+ const timers = [];
68
+ const rafs = [];
69
+
70
+ if (reveal === 'instant') {
71
+ lineNodes.forEach(n => n.classList.add('in'));
72
+ return () => {};
73
+ }
74
+
75
+ if (reveal === 'lines') {
76
+ lineNodes.forEach((n, i) => {
77
+ timers.push(setTimeout(() => n.classList.add('in'), 80 + i * 90));
78
+ });
79
+ return () => timers.forEach(clearTimeout);
80
+ }
81
+
82
+ if (reveal === 'typewriter') {
83
+ // Hide all line texts, then type them sequentially char-by-char.
84
+ const originals = lineNodes.map(n => {
85
+ const txt = n.querySelector('.cb-line-text');
86
+ return txt ? lines[Number(n.dataset.line) - 1] : '';
87
+ });
88
+ lineNodes.forEach(n => {
89
+ n.classList.add('in');
90
+ const txt = n.querySelector('.cb-line-text');
91
+ if (txt) txt.textContent = '';
92
+ });
93
+
94
+ let lineIdx = 0;
95
+ let charIdx = 0;
96
+ const speed = 18; // ms/char
97
+ function step() {
98
+ if (lineIdx >= lineNodes.length) return;
99
+ const node = lineNodes[lineIdx];
100
+ const txt = node.querySelector('.cb-line-text');
101
+ const target = originals[lineIdx] || '';
102
+ if (!txt) { lineIdx++; charIdx = 0; rafs.push(requestAnimationFrame(step)); return; }
103
+ if (charIdx <= target.length) {
104
+ txt.textContent = target.slice(0, charIdx) || ' ';
105
+ charIdx++;
106
+ const id = setTimeout(step, speed);
107
+ timers.push(id);
108
+ } else {
109
+ lineIdx++;
110
+ charIdx = 0;
111
+ const id = setTimeout(step, 40);
112
+ timers.push(id);
113
+ }
114
+ }
115
+ const startId = setTimeout(step, 150);
116
+ timers.push(startId);
117
+ return () => {
118
+ timers.forEach(clearTimeout);
119
+ rafs.forEach(cancelAnimationFrame);
120
+ };
121
+ }
122
+
123
+ return () => {};
124
+ },
125
+ replay(el) {
126
+ el.querySelectorAll('.cb-line').forEach(n => n.classList.remove('in'));
127
+ // Restore text content
128
+ const lineNodes = el.querySelectorAll('.cb-line');
129
+ lineNodes.forEach((n, i) => {
130
+ const txt = n.querySelector('.cb-line-text');
131
+ if (txt) txt.innerHTML = escape(lines[i] || '') || '&nbsp;';
132
+ });
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,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.CodeDiff — git-style diff view with add/remove/context lines.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.CodeDiff({
8
+ * section: 5,
9
+ * title: '05 · the refactor',
10
+ * fileName: 'src/server.ts',
11
+ * language: 'typescript',
12
+ * lines: [
13
+ * { type: 'context', text: 'function handle(req) {' },
14
+ * { type: 'remove', text: ' return JSON.parse(req.body);' },
15
+ * { type: 'add', text: ' return safeParse(req.body);' },
16
+ * { type: 'context', text: '}' }
17
+ * ],
18
+ * reveal: 'staggered' // 'instant' | 'staggered'
19
+ * }));
20
+ *
21
+ * Edit paths: lines[i].text / fileName
22
+ */
23
+
24
+ (function (root) {
25
+ const Stage = root.Stage = root.Stage || {};
26
+
27
+ Stage.CodeDiff = function (opts) {
28
+ const fileName = opts.fileName || '';
29
+ const language = opts.language || '';
30
+ const lines = opts.lines || [];
31
+ const reveal = opts.reveal || 'instant';
32
+
33
+ // Track plus/minus counts for header summary
34
+ const adds = lines.filter(l => l.type === 'add').length;
35
+ const removes = lines.filter(l => l.type === 'remove').length;
36
+
37
+ const slide = {
38
+ section: opts.section,
39
+ title: opts.title,
40
+ transition: opts.transition,
41
+ render(el) {
42
+ const lineHtml = lines.map((line, i) => {
43
+ const type = line.type || 'context';
44
+ const marker = type === 'add' ? '+' : type === 'remove' ? '-' : ' ';
45
+ return `
46
+ <div class="cd-line cd-line--${escape(type)}"
47
+ data-i="${i}"
48
+ data-stage-key="CodeDiff/line[${i}]">
49
+ <span class="cd-marker">${marker}</span>
50
+ <span class="cd-text" data-stage-edit="lines[${i}].text">${escape(line.text || '') || '&nbsp;'}</span>
51
+ </div>
52
+ `;
53
+ }).join('');
54
+
55
+ el.innerHTML = `
56
+ <div class="codediff" data-stage-key="CodeDiff">
57
+ ${fileName ? `
58
+ <div class="cd-head" data-stage-key="CodeDiff/head">
59
+ <span class="cd-file" data-stage-edit="fileName">${escape(fileName)}</span>
60
+ ${language ? `<span class="cd-lang">${escape(language)}</span>` : ''}
61
+ <span class="cd-stats">
62
+ <span class="cd-adds">+${adds}</span>
63
+ <span class="cd-removes">-${removes}</span>
64
+ </span>
65
+ </div>
66
+ ` : ''}
67
+ <div class="cd-body">${lineHtml}</div>
68
+ </div>
69
+ `;
70
+
71
+ if (reveal === 'instant') {
72
+ el.querySelectorAll('.cd-line').forEach(n => n.classList.add('in'));
73
+ }
74
+ }
75
+ };
76
+
77
+ if (reveal === 'staggered') {
78
+ slide.init = function (el) {
79
+ const lineNodes = Array.from(el.querySelectorAll('.cd-line'));
80
+ const timers = [];
81
+ lineNodes.forEach((n, i) => {
82
+ timers.push(setTimeout(() => n.classList.add('in'), 120 + i * 130));
83
+ });
84
+ return () => timers.forEach(clearTimeout);
85
+ };
86
+ slide.replay = function (el) {
87
+ el.querySelectorAll('.cd-line').forEach(n => n.classList.remove('in'));
88
+ return this.init(el);
89
+ };
90
+ }
91
+
92
+ return slide;
93
+ };
94
+
95
+ function escape(s) {
96
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
97
+ }
98
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Compare — two-column comparison (old vs new).
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Compare({
8
+ * section: 4,
9
+ * title: 'before / after',
10
+ * left: { heading: 'OLD', items: ['type', 'compile', 'run'], style: 'strikethrough' },
11
+ * right: { heading: 'NEW', items: ['describe', 'review', 'ship'], style: 'accent' },
12
+ * reveal: 'staggered' // | 'instant' | 'per-click'
13
+ * }));
14
+ */
15
+
16
+ (function (root) {
17
+ const Stage = root.Stage = root.Stage || {};
18
+
19
+ Stage.Compare = function (opts) {
20
+ const left = opts.left || { heading: '', items: [] };
21
+ const right = opts.right || { heading: '', items: [] };
22
+ const reveal = opts.reveal || 'instant';
23
+
24
+ const slide = {
25
+ section: opts.section,
26
+ title: opts.title,
27
+ transition: opts.transition,
28
+ render(el) {
29
+ el.innerHTML = `
30
+ <div class="compare" data-stage-key="Compare">
31
+ <div class="compare-col" data-stage-key="Compare/left">
32
+ <div class="compare-h ${classFor(left.style)}" data-stage-edit="left.heading">${escape(left.heading)}</div>
33
+ <ul class="compare-list ${left.style || ''}">
34
+ ${left.items.map((it, i) => `<li data-step="${i + 1}" data-stage-edit="left.items[${i}]" data-stage-key="Compare/left/item[${i}]">${escape(it)}</li>`).join('')}
35
+ </ul>
36
+ </div>
37
+ <div class="compare-divider"></div>
38
+ <div class="compare-col" data-stage-key="Compare/right">
39
+ <div class="compare-h ${classFor(right.style)}" data-stage-edit="right.heading">${escape(right.heading)}</div>
40
+ <ul class="compare-list ${right.style || ''}">
41
+ ${right.items.map((it, i) => `<li data-step="${left.items.length + i + 1}" data-stage-edit="right.items[${i}]" data-stage-key="Compare/right/item[${i}]">${escape(it)}</li>`).join('')}
42
+ </ul>
43
+ </div>
44
+ </div>
45
+ `;
46
+ if (reveal === 'instant') {
47
+ el.querySelectorAll('li').forEach(n => n.classList.add('in'));
48
+ }
49
+ }
50
+ };
51
+
52
+ if (reveal === 'staggered') {
53
+ slide.init = function (el) {
54
+ const all = [
55
+ ...el.querySelectorAll('.compare-col:first-child li'),
56
+ ...el.querySelectorAll('.compare-col:last-child li')
57
+ ];
58
+ return Stage.staggerIn(all, 200, 200);
59
+ };
60
+ slide.replay = function (el) {
61
+ el.querySelectorAll('li').forEach(n => n.classList.remove('in'));
62
+ return this.init(el);
63
+ };
64
+ } else if (reveal === 'per-click') {
65
+ slide.steps = left.items.length + right.items.length;
66
+ slide.onStep = function (el, step) {
67
+ el.querySelectorAll('li').forEach(n => {
68
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
69
+ });
70
+ };
71
+ }
72
+
73
+ return slide;
74
+ };
75
+
76
+ function classFor(style) {
77
+ if (style === 'strikethrough') return 'old';
78
+ if (style === 'accent') return 'new';
79
+ return '';
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,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Counter — large numeric counters with optional live tick.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Counter({
8
+ * section: 4,
9
+ * title: 'live stats',
10
+ * blocks: [
11
+ * { label: 'Lines written', start: 0, perSecond: 47, color: 'accent' },
12
+ * { label: 'Bugs introduced', start: 0, perSecond: 3, color: 'amber' }
13
+ * ],
14
+ * footer: 'Both numbers are wrong.'
15
+ * }));
16
+ */
17
+
18
+ (function (root) {
19
+ const Stage = root.Stage = root.Stage || {};
20
+
21
+ Stage.Counter = function (opts) {
22
+ const blocks = opts.blocks || [];
23
+ const footer = opts.footer || '';
24
+
25
+ return {
26
+ section: opts.section,
27
+ title: opts.title,
28
+ transition: opts.transition,
29
+ render(el) {
30
+ el.innerHTML = `
31
+ <div class="counter-wrap" data-stage-key="Counter">
32
+ ${blocks.map((b, i) => `
33
+ <div class="counter-block" data-stage-key="Counter/block[${i}]">
34
+ <div class="counter-label ${b.color || ''}">
35
+ <span class="dot"></span><span data-stage-edit="blocks[${i}].label">${escape(b.label)}</span>
36
+ </div>
37
+ <div class="counter-num ${b.color || ''}" data-i="${i}">${b.start ?? 0}</div>
38
+ </div>
39
+ `).join('')}
40
+ </div>
41
+ ${footer ? `<div class="counter-foot" data-stage-edit="footer">${footer}</div>` : ''}
42
+ `;
43
+ },
44
+ init(el) {
45
+ const timers = [];
46
+ const nums = el.querySelectorAll('.counter-num');
47
+ blocks.forEach((b, i) => {
48
+ if (!b.perSecond) return;
49
+ let value = Number(b.start || 0);
50
+ const target = nums[i];
51
+ if (!target) return;
52
+ const tickMs = Math.max(20, Math.floor(1000 / Math.max(1, b.perSecond)));
53
+ const inc = b.perSecond * tickMs / 1000;
54
+ const id = setInterval(() => {
55
+ value += inc;
56
+ target.textContent = Math.floor(value).toLocaleString();
57
+ }, tickMs);
58
+ timers.push(() => clearInterval(id));
59
+ });
60
+ if (footer) {
61
+ const f = el.querySelector('.counter-foot');
62
+ const t = setTimeout(() => f && f.classList.add('visible'), 3000);
63
+ timers.push(() => clearTimeout(t));
64
+ }
65
+ return () => timers.forEach(fn => fn());
66
+ },
67
+ replay(el) {
68
+ const nums = el.querySelectorAll('.counter-num');
69
+ blocks.forEach((b, i) => { if (nums[i]) nums[i].textContent = b.start ?? 0; });
70
+ const f = el.querySelector('.counter-foot');
71
+ if (f) f.classList.remove('visible');
72
+ return this.init(el);
73
+ }
74
+ };
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,69 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.CTA — large call-to-action card.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.CTA({
8
+ * section: 66,
9
+ * title: '66 · Take action',
10
+ * headline: 'Ship the next release with confidence.',
11
+ * body: 'Start your 14-day trial. No credit card.',
12
+ * action: { label: 'Start free trial', hint: 'No setup required' },
13
+ * accent: true
14
+ * }));
15
+ *
16
+ * The button is visual only — it does not navigate.
17
+ *
18
+ * Edit paths: headline / body / action.label / action.hint
19
+ */
20
+
21
+ (function (root) {
22
+ const Stage = root.Stage = root.Stage || {};
23
+
24
+ Stage.CTA = function (opts) {
25
+ const action = opts.action || {};
26
+ const accent = opts.accent === true;
27
+
28
+ return {
29
+ section: opts.section,
30
+ title: opts.title,
31
+ transition: opts.transition,
32
+ render(el) {
33
+ el.innerHTML = `
34
+ <div class="cta ${accent ? 'cta--accent' : ''}" data-stage-key="CTA">
35
+ <div class="cta-headline" data-stage-edit="headline" data-stage-key="CTA/headline">${escape(opts.headline || '')}</div>
36
+ ${opts.body ? `<div class="cta-body" data-stage-edit="body" data-stage-key="CTA/body">${escape(opts.body)}</div>` : ''}
37
+ <div class="cta-action" data-stage-key="CTA/action">
38
+ <span class="cta-button">
39
+ <span class="cta-button-label" data-stage-edit="action.label">${escape(action.label || 'Get started')}</span>
40
+ <span class="cta-button-arrow material-symbols-outlined">arrow_forward</span>
41
+ </span>
42
+ ${action.hint ? `<span class="cta-hint" data-stage-edit="action.hint">${escape(action.hint)}</span>` : ''}
43
+ </div>
44
+ </div>
45
+ `;
46
+ },
47
+ init(el) {
48
+ const nodes = [
49
+ el.querySelector('.cta-headline'),
50
+ el.querySelector('.cta-body'),
51
+ el.querySelector('.cta-action')
52
+ ].filter(Boolean);
53
+ const timers = [];
54
+ nodes.forEach((n, i) => {
55
+ timers.push(setTimeout(() => n.classList.add('in'), 150 + i * 250));
56
+ });
57
+ return () => timers.forEach(clearTimeout);
58
+ },
59
+ replay(el) {
60
+ el.querySelectorAll('.cta-headline, .cta-body, .cta-action').forEach(n => n.classList.remove('in'));
61
+ return this.init(el);
62
+ }
63
+ };
64
+ };
65
+
66
+ function escape(s) {
67
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
68
+ }
69
+ })(typeof window !== 'undefined' ? window : globalThis);