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,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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
90
|
+
}
|
|
91
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|