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.
- package/AGENT.md +792 -0
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/bin/cli.js +51 -0
- package/bin/export.js +137 -0
- package/bin/init.js +52 -0
- package/bin/lib/edit-ops.js +405 -0
- package/bin/serve.js +278 -0
- package/dist/stagecraft.bundle.css +4443 -0
- package/dist/stagecraft.bundle.js +7621 -0
- package/dist/themes/brand.bundle.css +5262 -0
- package/dist/themes/neon.bundle.css +5289 -0
- package/dist/themes/paper.bundle.css +5276 -0
- package/dist/themes/phosphor.bundle.css +4443 -0
- package/dist/themes/shopware.bundle.css +5850 -0
- package/examples/closing-card.js +74 -0
- package/examples/orchestration-graph.js +156 -0
- package/examples/terminal-log.js +109 -0
- package/examples/token-stream.js +96 -0
- package/examples/whoami.js +90 -0
- package/package.json +41 -0
- package/src/components/activity-list.js +75 -0
- package/src/components/agenda.js +79 -0
- package/src/components/bar-chart.js +162 -0
- package/src/components/before-after.js +135 -0
- package/src/components/bento.js +73 -0
- package/src/components/big-number.js +87 -0
- package/src/components/callout.js +75 -0
- package/src/components/checklist.js +81 -0
- package/src/components/code-block.js +141 -0
- package/src/components/code-diff.js +98 -0
- package/src/components/compare.js +85 -0
- package/src/components/counter.js +80 -0
- package/src/components/cta.js +69 -0
- package/src/components/cycle.js +146 -0
- package/src/components/definition.js +96 -0
- package/src/components/donut-chart.js +179 -0
- package/src/components/full-image.js +82 -0
- package/src/components/funnel.js +111 -0
- package/src/components/gauge.js +147 -0
- package/src/components/heatmap.js +141 -0
- package/src/components/image-grid.js +80 -0
- package/src/components/image-text.js +96 -0
- package/src/components/kinetic-text.js +72 -0
- package/src/components/kpi.js +106 -0
- package/src/components/line-chart.js +215 -0
- package/src/components/manifesto.js +104 -0
- package/src/components/marquee.js +63 -0
- package/src/components/matrix2x2.js +151 -0
- package/src/components/pillars.js +80 -0
- package/src/components/pricing.js +90 -0
- package/src/components/process-flow.js +133 -0
- package/src/components/progress.js +136 -0
- package/src/components/punchline.js +82 -0
- package/src/components/pyramid.js +107 -0
- package/src/components/qanda.js +60 -0
- package/src/components/quote.js +70 -0
- package/src/components/roadmap.js +130 -0
- package/src/components/section-card.js +45 -0
- package/src/components/shift-arrow.js +41 -0
- package/src/components/spark-line.js +147 -0
- package/src/components/spotlight.js +85 -0
- package/src/components/statement.js +106 -0
- package/src/components/stats.js +91 -0
- package/src/components/steps.js +83 -0
- package/src/components/swot.js +110 -0
- package/src/components/team-grid.js +87 -0
- package/src/components/testimonial.js +99 -0
- package/src/components/timeline.js +91 -0
- package/src/components/tip.js +63 -0
- package/src/components/venn.js +198 -0
- package/src/edit-mode.js +1256 -0
- package/src/engine.js +823 -0
- package/src/helpers.js +169 -0
- package/src/transitions.js +101 -0
- package/starter/index.html +40 -0
- package/starter/slides/00-title.js +12 -0
- package/starter/stagecraft.config.js +8 -0
- package/themes/brand/base.css +4 -0
- package/themes/brand/components-business.css +173 -0
- package/themes/brand/components-chart.css +65 -0
- package/themes/brand/components-content.css +126 -0
- package/themes/brand/components-data.css +162 -0
- package/themes/brand/components-diagram.css +115 -0
- package/themes/brand/components-layout.css +112 -0
- package/themes/brand/components.css +46 -0
- package/themes/brand/manifest.json +20 -0
- package/themes/brand/tokens.css +20 -0
- package/themes/brand/transitions.css +4 -0
- package/themes/neon/base.css +10 -0
- package/themes/neon/components-business.css +189 -0
- package/themes/neon/components-chart.css +70 -0
- package/themes/neon/components-content.css +112 -0
- package/themes/neon/components-data.css +160 -0
- package/themes/neon/components-diagram.css +109 -0
- package/themes/neon/components-layout.css +87 -0
- package/themes/neon/components.css +87 -0
- package/themes/neon/manifest.json +21 -0
- package/themes/neon/tokens.css +17 -0
- package/themes/neon/transitions.css +13 -0
- package/themes/paper/base.css +9 -0
- package/themes/paper/components-business.css +196 -0
- package/themes/paper/components-chart.css +74 -0
- package/themes/paper/components-content.css +108 -0
- package/themes/paper/components-data.css +168 -0
- package/themes/paper/components-diagram.css +89 -0
- package/themes/paper/components-layout.css +105 -0
- package/themes/paper/components.css +60 -0
- package/themes/paper/manifest.json +10 -0
- package/themes/paper/tokens.css +21 -0
- package/themes/paper/transitions.css +11 -0
- package/themes/phosphor/base.css +511 -0
- package/themes/phosphor/components-business.css +818 -0
- package/themes/phosphor/components-chart.css +415 -0
- package/themes/phosphor/components-content.css +530 -0
- package/themes/phosphor/components-data.css +824 -0
- package/themes/phosphor/components-diagram.css +427 -0
- package/themes/phosphor/components-layout.css +450 -0
- package/themes/phosphor/components.css +223 -0
- package/themes/phosphor/manifest.json +11 -0
- package/themes/phosphor/tokens.css +17 -0
- package/themes/phosphor/transitions.css +213 -0
- package/themes/shopware/base.css +94 -0
- package/themes/shopware/components-business.css +344 -0
- package/themes/shopware/components-chart.css +121 -0
- package/themes/shopware/components-content.css +169 -0
- package/themes/shopware/components-data.css +219 -0
- package/themes/shopware/components-diagram.css +129 -0
- package/themes/shopware/components-layout.css +166 -0
- package/themes/shopware/components.css +83 -0
- package/themes/shopware/manifest.json +21 -0
- package/themes/shopware/tokens.css +68 -0
- 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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) || ' '}</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] || '') || ' ';
|
|
132
|
+
});
|
|
133
|
+
return this.init(el);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function escape(s) {
|
|
139
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 || '') || ' '}</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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
68
|
+
}
|
|
69
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|