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,146 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stage.Cycle — items arranged on a circle with curved arrows between them.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* Stage.register(Stage.Cycle({
|
|
8
|
+
* section: 7,
|
|
9
|
+
* title: '07 · The loop',
|
|
10
|
+
* items: [
|
|
11
|
+
* { label: 'Plan', icon: 'edit_note', color: 'accent' },
|
|
12
|
+
* { label: 'Build', icon: 'construction' },
|
|
13
|
+
* { label: 'Verify', icon: 'fact_check' },
|
|
14
|
+
* { label: 'Reflect', icon: 'psychology' }
|
|
15
|
+
* ],
|
|
16
|
+
* reveal: 'instant' // 'instant' | 'rotate' | 'per-click'
|
|
17
|
+
* }));
|
|
18
|
+
*
|
|
19
|
+
* SVG provides the circle + arcs; HTML cards float on top at polar positions.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
(function (root) {
|
|
23
|
+
const Stage = root.Stage = root.Stage || {};
|
|
24
|
+
|
|
25
|
+
Stage.Cycle = function (opts) {
|
|
26
|
+
const items = opts.items || [];
|
|
27
|
+
const reveal = opts.reveal || 'instant';
|
|
28
|
+
const n = items.length;
|
|
29
|
+
const VB = 1000;
|
|
30
|
+
const CX = VB / 2;
|
|
31
|
+
const CY = VB / 2;
|
|
32
|
+
const R = VB * 0.36;
|
|
33
|
+
const ITEM_R = VB * 0.11; // ring offset for label cards
|
|
34
|
+
|
|
35
|
+
// Start at top (-PI/2) and go clockwise.
|
|
36
|
+
function angle(i) { return -Math.PI / 2 + (i / Math.max(1, n)) * Math.PI * 2; }
|
|
37
|
+
function pt(a, r) { return { x: CX + Math.cos(a) * r, y: CY + Math.sin(a) * r }; }
|
|
38
|
+
|
|
39
|
+
// Build an arc from item i -> i+1 that sits *outside* the inner clear area,
|
|
40
|
+
// so it visually connects the two cards.
|
|
41
|
+
function arcPath(i) {
|
|
42
|
+
const a1 = angle(i);
|
|
43
|
+
const a2 = angle((i + 1) % n);
|
|
44
|
+
const p1 = pt(a1, R);
|
|
45
|
+
const p2 = pt(a2, R);
|
|
46
|
+
// Use the circle radius itself for the SVG arc command; large-arc=0, sweep=1 (clockwise).
|
|
47
|
+
return `M ${p1.x.toFixed(1)} ${p1.y.toFixed(1)} A ${R} ${R} 0 0 1 ${p2.x.toFixed(1)} ${p2.y.toFixed(1)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Arrow tip near each next item.
|
|
51
|
+
function arrowAt(i) {
|
|
52
|
+
const a = angle((i + 1) % n);
|
|
53
|
+
// Pull tip slightly back along the tangent so it lands cleanly.
|
|
54
|
+
const tipA = a - 0.18;
|
|
55
|
+
const tip = pt(tipA, R);
|
|
56
|
+
// Tangent direction (perpendicular to radius, clockwise).
|
|
57
|
+
const tx = -Math.sin(tipA);
|
|
58
|
+
const ty = Math.cos(tipA);
|
|
59
|
+
const size = VB * 0.018;
|
|
60
|
+
// Two wings off the tangent.
|
|
61
|
+
const ax = tip.x - tx * size + ty * size * 0.6;
|
|
62
|
+
const ay = tip.y - ty * size - tx * size * 0.6;
|
|
63
|
+
const bx = tip.x - tx * size - ty * size * 0.6;
|
|
64
|
+
const by = tip.y - ty * size + tx * size * 0.6;
|
|
65
|
+
return `M ${tip.x.toFixed(1)} ${tip.y.toFixed(1)} L ${ax.toFixed(1)} ${ay.toFixed(1)} M ${tip.x.toFixed(1)} ${tip.y.toFixed(1)} L ${bx.toFixed(1)} ${by.toFixed(1)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const slide = {
|
|
69
|
+
section: opts.section,
|
|
70
|
+
title: opts.title,
|
|
71
|
+
transition: opts.transition,
|
|
72
|
+
render(el) {
|
|
73
|
+
const cardsHtml = items.map((it, i) => {
|
|
74
|
+
const p = pt(angle(i), R);
|
|
75
|
+
const xPct = (p.x / VB) * 100;
|
|
76
|
+
const yPct = (p.y / VB) * 100;
|
|
77
|
+
return `
|
|
78
|
+
<div class="cyc-item ${it.color ? 'cyc-' + escape(it.color) : ''}"
|
|
79
|
+
data-step="${i + 1}"
|
|
80
|
+
style="left: ${xPct.toFixed(2)}%; top: ${yPct.toFixed(2)}%;"
|
|
81
|
+
data-stage-key="Cycle/item[${i}]">
|
|
82
|
+
${it.icon ? `<span class="cyc-icon material-symbols-outlined" data-stage-edit="items[${i}].icon">${escape(it.icon)}</span>` : ''}
|
|
83
|
+
<div class="cyc-label" data-stage-edit="items[${i}].label" data-stage-key="Cycle/item[${i}]/label">${escape(it.label || '')}</div>
|
|
84
|
+
</div>
|
|
85
|
+
`;
|
|
86
|
+
}).join('');
|
|
87
|
+
|
|
88
|
+
const arcs = [];
|
|
89
|
+
for (let i = 0; i < n; i++) {
|
|
90
|
+
arcs.push(`<path class="cyc-arc" d="${arcPath(i)}" data-step="${i + 1}"/>`);
|
|
91
|
+
arcs.push(`<path class="cyc-arrow" d="${arrowAt(i)}" data-step="${i + 1}"/>`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
el.innerHTML = `
|
|
95
|
+
<div class="cycle" data-stage-key="Cycle">
|
|
96
|
+
<div class="cyc-frame">
|
|
97
|
+
<svg class="cyc-svg" viewBox="0 0 ${VB} ${VB}" preserveAspectRatio="xMidYMid meet">
|
|
98
|
+
<circle class="cyc-ring" cx="${CX}" cy="${CY}" r="${R}"/>
|
|
99
|
+
${arcs.join('')}
|
|
100
|
+
</svg>
|
|
101
|
+
<div class="cyc-items">
|
|
102
|
+
${cardsHtml}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
if (reveal === 'instant' || reveal === 'rotate') {
|
|
108
|
+
el.querySelectorAll('.cyc-item, .cyc-arc, .cyc-arrow').forEach(n => n.classList.add('in'));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (reveal === 'rotate') {
|
|
114
|
+
slide.init = function (el) {
|
|
115
|
+
const frame = el.querySelector('.cyc-frame');
|
|
116
|
+
if (frame) {
|
|
117
|
+
frame.classList.add('rotating');
|
|
118
|
+
const id = setTimeout(() => frame.classList.remove('rotating'), 1700);
|
|
119
|
+
return () => clearTimeout(id);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
slide.replay = function (el) {
|
|
123
|
+
const frame = el.querySelector('.cyc-frame');
|
|
124
|
+
if (frame) { frame.classList.remove('rotating'); void frame.offsetWidth; }
|
|
125
|
+
return this.init(el);
|
|
126
|
+
};
|
|
127
|
+
} else if (reveal === 'per-click') {
|
|
128
|
+
slide.steps = items.length;
|
|
129
|
+
slide.onStep = function (el, step) {
|
|
130
|
+
el.querySelectorAll('.cyc-item').forEach(node => {
|
|
131
|
+
node.classList.toggle('in', Number(node.dataset.step) <= step);
|
|
132
|
+
});
|
|
133
|
+
el.querySelectorAll('.cyc-arc, .cyc-arrow').forEach(node => {
|
|
134
|
+
// Arc i connects item i -> i+1; show when both endpoints are in.
|
|
135
|
+
node.classList.toggle('in', Number(node.dataset.step) < step || (Number(node.dataset.step) === step && step === n));
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return slide;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function escape(s) {
|
|
144
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
145
|
+
}
|
|
146
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stage.Definition — dictionary-style entry.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* Stage.register(Stage.Definition({
|
|
8
|
+
* section: 5,
|
|
9
|
+
* title: '05 · Define your terms',
|
|
10
|
+
* term: 'taste',
|
|
11
|
+
* definition: 'the ability to tell a good answer from a plausible one.',
|
|
12
|
+
* etymology: 'from Old French "tast" — to touch, to feel.', // optional
|
|
13
|
+
* examples: [ // optional, 1-2
|
|
14
|
+
* 'You can rent skill. You cannot rent taste.',
|
|
15
|
+
* 'Without taste, the model writes very fast nonsense.'
|
|
16
|
+
* ]
|
|
17
|
+
* }));
|
|
18
|
+
*
|
|
19
|
+
* In examples, occurrences of the term get highlighted automatically.
|
|
20
|
+
*
|
|
21
|
+
* Edit paths:
|
|
22
|
+
* term / definition / etymology / examples[i]
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
(function (root) {
|
|
26
|
+
const Stage = root.Stage = root.Stage || {};
|
|
27
|
+
|
|
28
|
+
Stage.Definition = function (opts) {
|
|
29
|
+
const term = opts.term || '';
|
|
30
|
+
const definition = opts.definition || '';
|
|
31
|
+
const etymology = opts.etymology || '';
|
|
32
|
+
const examples = Array.isArray(opts.examples) ? opts.examples : [];
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
section: opts.section,
|
|
36
|
+
title: opts.title,
|
|
37
|
+
transition: opts.transition,
|
|
38
|
+
render(el) {
|
|
39
|
+
el.innerHTML = `
|
|
40
|
+
<article class="definition" data-stage-key="Definition">
|
|
41
|
+
<header class="df-head">
|
|
42
|
+
<h1 class="df-term" data-stage-edit="term" data-stage-key="Definition/term">${escape(term)}</h1>
|
|
43
|
+
<span class="df-pos" aria-hidden="true">n.</span>
|
|
44
|
+
</header>
|
|
45
|
+
<p class="df-def" data-stage-edit="definition" data-stage-key="Definition/definition">${escape(definition)}</p>
|
|
46
|
+
${etymology ? `<p class="df-ety" data-stage-edit="etymology" data-stage-key="Definition/etymology">${escape(etymology)}</p>` : ''}
|
|
47
|
+
${examples.length ? `
|
|
48
|
+
<ul class="df-examples">
|
|
49
|
+
${examples.map((ex, i) => `
|
|
50
|
+
<li class="df-ex" data-stage-edit="examples[${i}]" data-stage-key="Definition/example[${i}]">${highlight(ex, term)}</li>
|
|
51
|
+
`).join('')}
|
|
52
|
+
</ul>
|
|
53
|
+
` : ''}
|
|
54
|
+
</article>
|
|
55
|
+
`;
|
|
56
|
+
},
|
|
57
|
+
init(el) {
|
|
58
|
+
const term = el.querySelector('.df-term');
|
|
59
|
+
const pos = el.querySelector('.df-pos');
|
|
60
|
+
const def = el.querySelector('.df-def');
|
|
61
|
+
const ety = el.querySelector('.df-ety');
|
|
62
|
+
const exs = el.querySelectorAll('.df-ex');
|
|
63
|
+
const timers = [];
|
|
64
|
+
timers.push(setTimeout(() => term && term.classList.add('in'), 120));
|
|
65
|
+
timers.push(setTimeout(() => pos && pos.classList.add('in'), 350));
|
|
66
|
+
timers.push(setTimeout(() => def && def.classList.add('in'), 700));
|
|
67
|
+
timers.push(setTimeout(() => ety && ety.classList.add('in'), 1100));
|
|
68
|
+
exs.forEach((n, i) => {
|
|
69
|
+
timers.push(setTimeout(() => n.classList.add('in'), 1400 + i * 280));
|
|
70
|
+
});
|
|
71
|
+
return () => timers.forEach(clearTimeout);
|
|
72
|
+
},
|
|
73
|
+
replay(el) {
|
|
74
|
+
el.querySelectorAll('.df-term, .df-pos, .df-def, .df-ety, .df-ex')
|
|
75
|
+
.forEach(n => n.classList.remove('in'));
|
|
76
|
+
return this.init(el);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function highlight(raw, term) {
|
|
82
|
+
let html = escape(raw);
|
|
83
|
+
if (!term) return html;
|
|
84
|
+
const esc = escape(term);
|
|
85
|
+
const re = new RegExp(escapeRegex(esc), 'gi');
|
|
86
|
+
return html.replace(re, m => `<span class="df-mark">${m}</span>`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function escapeRegex(s) {
|
|
90
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function escape(s) {
|
|
94
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
95
|
+
}
|
|
96
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stage.DonutChart — SVG donut chart with legend.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* Stage.register(Stage.DonutChart({
|
|
8
|
+
* section: 5,
|
|
9
|
+
* title: '05 · time allocation',
|
|
10
|
+
* segments: [
|
|
11
|
+
* { label: 'Coding', value: 42, color: 'accent' },
|
|
12
|
+
* { label: 'Meetings', value: 23, color: 'amber' },
|
|
13
|
+
* { label: 'Review', value: 18, color: 'blue' },
|
|
14
|
+
* { label: 'Slack', value: 17, color: 'red' }
|
|
15
|
+
* ],
|
|
16
|
+
* centerLabel: 'time',
|
|
17
|
+
* reveal: 'animated' // 'instant' | 'animated' | 'staggered'
|
|
18
|
+
* }));
|
|
19
|
+
*
|
|
20
|
+
* Each segment grows from 0 (length-0 arc) on init.
|
|
21
|
+
*
|
|
22
|
+
* Edit paths: segments[i].label / segments[i].value / centerLabel
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
(function (root) {
|
|
26
|
+
const Stage = root.Stage = root.Stage || {};
|
|
27
|
+
|
|
28
|
+
const COLOR_MAP = {
|
|
29
|
+
accent: 'var(--accent)',
|
|
30
|
+
amber: 'var(--amber)',
|
|
31
|
+
blue: 'var(--blue)',
|
|
32
|
+
red: 'var(--red)',
|
|
33
|
+
dim: 'var(--dim)',
|
|
34
|
+
fg: 'var(--fg)'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function colorFor(name, fallbackIdx) {
|
|
38
|
+
if (COLOR_MAP[name]) return COLOR_MAP[name];
|
|
39
|
+
const cycle = ['var(--accent)', 'var(--blue)', 'var(--amber)', 'var(--red)', 'var(--dim)'];
|
|
40
|
+
return cycle[fallbackIdx % cycle.length];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Stage.DonutChart = function (opts) {
|
|
44
|
+
const segments = opts.segments || [];
|
|
45
|
+
const centerLabel = opts.centerLabel || '';
|
|
46
|
+
const reveal = opts.reveal || 'animated';
|
|
47
|
+
|
|
48
|
+
const total = segments.reduce((s, x) => s + (Number(x.value) || 0), 0) || 1;
|
|
49
|
+
// SVG circle: cx=50, cy=50, r=40, circumference = 2*pi*40 ≈ 251.327
|
|
50
|
+
const R = 40;
|
|
51
|
+
const C = 2 * Math.PI * R;
|
|
52
|
+
|
|
53
|
+
// Compute cumulative offsets so each segment is drawn at the right position
|
|
54
|
+
let cum = 0;
|
|
55
|
+
const arcs = segments.map((seg, i) => {
|
|
56
|
+
const value = Number(seg.value) || 0;
|
|
57
|
+
const fraction = value / total;
|
|
58
|
+
const length = fraction * C;
|
|
59
|
+
const offset = -cum * C; // negative because SVG draws clockwise from 3 o'clock; we rotate to 12 o'clock
|
|
60
|
+
cum += fraction;
|
|
61
|
+
return {
|
|
62
|
+
i,
|
|
63
|
+
label: seg.label || '',
|
|
64
|
+
value,
|
|
65
|
+
color: colorFor(seg.color, i),
|
|
66
|
+
length,
|
|
67
|
+
offset,
|
|
68
|
+
percent: Math.round(fraction * 100)
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const slide = {
|
|
73
|
+
section: opts.section,
|
|
74
|
+
title: opts.title,
|
|
75
|
+
transition: opts.transition,
|
|
76
|
+
render(el) {
|
|
77
|
+
const svgArcs = arcs.map(a => `
|
|
78
|
+
<circle class="donut-arc"
|
|
79
|
+
data-i="${a.i}"
|
|
80
|
+
data-stage-key="DonutChart/arc[${a.i}]"
|
|
81
|
+
cx="50" cy="50" r="${R}"
|
|
82
|
+
fill="none"
|
|
83
|
+
stroke="${a.color}"
|
|
84
|
+
stroke-width="12"
|
|
85
|
+
stroke-dasharray="0 ${C.toFixed(3)}"
|
|
86
|
+
stroke-dashoffset="${a.offset.toFixed(3)}"
|
|
87
|
+
data-length="${a.length.toFixed(3)}"
|
|
88
|
+
data-circumference="${C.toFixed(3)}"/>
|
|
89
|
+
`).join('');
|
|
90
|
+
|
|
91
|
+
const legend = arcs.map(a => `
|
|
92
|
+
<div class="donut-legend-row" data-i="${a.i}" data-stage-key="DonutChart/legend[${a.i}]">
|
|
93
|
+
<span class="donut-dot" style="--dc-color: ${a.color};"></span>
|
|
94
|
+
<span class="donut-leg-label" data-stage-edit="segments[${a.i}].label">${escape(a.label)}</span>
|
|
95
|
+
<span class="donut-leg-value" data-stage-edit="segments[${a.i}].value">${escape(String(a.value))}</span>
|
|
96
|
+
<span class="donut-leg-pct">${a.percent}%</span>
|
|
97
|
+
</div>
|
|
98
|
+
`).join('');
|
|
99
|
+
|
|
100
|
+
el.innerHTML = `
|
|
101
|
+
<div class="donut" data-stage-key="DonutChart">
|
|
102
|
+
<div class="donut-svg-wrap">
|
|
103
|
+
<svg class="donut-svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
|
|
104
|
+
<circle class="donut-track" cx="50" cy="50" r="${R}" fill="none" stroke="var(--dim-2)" stroke-width="12"/>
|
|
105
|
+
<g class="donut-arcs" transform="rotate(-90 50 50)">
|
|
106
|
+
${svgArcs}
|
|
107
|
+
</g>
|
|
108
|
+
</svg>
|
|
109
|
+
${centerLabel ? `<div class="donut-center" data-stage-edit="centerLabel" data-stage-key="DonutChart/center">${escape(centerLabel)}</div>` : ''}
|
|
110
|
+
</div>
|
|
111
|
+
<div class="donut-legend" data-stage-key="DonutChart/legend">
|
|
112
|
+
${legend}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
if (reveal === 'instant') {
|
|
118
|
+
el.querySelectorAll('.donut-arc').forEach(arc => {
|
|
119
|
+
const len = arc.dataset.length;
|
|
120
|
+
arc.setAttribute('stroke-dasharray', `${len} ${C - Number(len)}`);
|
|
121
|
+
});
|
|
122
|
+
el.querySelectorAll('.donut-legend-row').forEach(n => n.classList.add('in'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function animateArc(arc, durationMs) {
|
|
128
|
+
const target = Number(arc.dataset.length);
|
|
129
|
+
const start = performance.now();
|
|
130
|
+
let rafId;
|
|
131
|
+
function tick(now) {
|
|
132
|
+
const t = Math.min(1, (now - start) / durationMs);
|
|
133
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
134
|
+
const len = target * eased;
|
|
135
|
+
arc.setAttribute('stroke-dasharray', `${len.toFixed(3)} ${(C - len).toFixed(3)}`);
|
|
136
|
+
if (t < 1) rafId = requestAnimationFrame(tick);
|
|
137
|
+
}
|
|
138
|
+
rafId = requestAnimationFrame(tick);
|
|
139
|
+
return () => cancelAnimationFrame(rafId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (reveal === 'animated' || reveal === 'staggered') {
|
|
143
|
+
slide.init = function (el) {
|
|
144
|
+
const arcsEl = Array.from(el.querySelectorAll('.donut-arc'));
|
|
145
|
+
const rows = Array.from(el.querySelectorAll('.donut-legend-row'));
|
|
146
|
+
const cancels = [];
|
|
147
|
+
const timers = [];
|
|
148
|
+
if (reveal === 'staggered') {
|
|
149
|
+
arcsEl.forEach((arc, i) => {
|
|
150
|
+
timers.push(setTimeout(() => cancels.push(animateArc(arc, 700)), 100 + i * 320));
|
|
151
|
+
timers.push(setTimeout(() => rows[i] && rows[i].classList.add('in'), 200 + i * 320));
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
arcsEl.forEach(arc => cancels.push(animateArc(arc, 1200)));
|
|
155
|
+
rows.forEach((row, i) => {
|
|
156
|
+
timers.push(setTimeout(() => row.classList.add('in'), 200 + i * 120));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return () => {
|
|
160
|
+
cancels.forEach(c => c());
|
|
161
|
+
timers.forEach(clearTimeout);
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
slide.replay = function (el) {
|
|
165
|
+
el.querySelectorAll('.donut-arc').forEach(arc => {
|
|
166
|
+
arc.setAttribute('stroke-dasharray', `0 ${C}`);
|
|
167
|
+
});
|
|
168
|
+
el.querySelectorAll('.donut-legend-row').forEach(n => n.classList.remove('in'));
|
|
169
|
+
return this.init(el);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return slide;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function escape(s) {
|
|
177
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
178
|
+
}
|
|
179
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stage.FullImage — full-bleed background image with optional text overlay.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* Stage.register(Stage.FullImage({
|
|
8
|
+
* section: 4,
|
|
9
|
+
* title: '04 · The terrain',
|
|
10
|
+
* image: { src: 'https://...', alt: 'A wide horizon' },
|
|
11
|
+
* overlay: {
|
|
12
|
+
* position: 'bottom-left', // 'center' | 'bottom-left' | 'top' | 'bottom-right'
|
|
13
|
+
* headline: 'Most slides should be quiet.',
|
|
14
|
+
* body: 'Let the picture speak first.' // optional
|
|
15
|
+
* }
|
|
16
|
+
* }));
|
|
17
|
+
*
|
|
18
|
+
* If `overlay` is omitted, the image gets a subtle ken-burns drift on init.
|
|
19
|
+
*
|
|
20
|
+
* Edit paths:
|
|
21
|
+
* image.src / image.alt / overlay.headline / overlay.body
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
(function (root) {
|
|
25
|
+
const Stage = root.Stage = root.Stage || {};
|
|
26
|
+
|
|
27
|
+
Stage.FullImage = function (opts) {
|
|
28
|
+
const image = opts.image || {};
|
|
29
|
+
const overlay = opts.overlay;
|
|
30
|
+
const position = overlay && overlay.position ? overlay.position : 'center';
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
section: opts.section,
|
|
34
|
+
title: opts.title,
|
|
35
|
+
transition: opts.transition,
|
|
36
|
+
render(el) {
|
|
37
|
+
el.classList.add('full-image-host');
|
|
38
|
+
el.innerHTML = `
|
|
39
|
+
<div class="full-image" data-stage-key="FullImage">
|
|
40
|
+
<div class="fi-bg"
|
|
41
|
+
style="background-image: url('${escape(image.src || '')}')"
|
|
42
|
+
role="img"
|
|
43
|
+
aria-label="${escape(image.alt || '')}"
|
|
44
|
+
data-stage-edit="image.src"></div>
|
|
45
|
+
${overlay ? `
|
|
46
|
+
<div class="fi-overlay fi-overlay--${position}" data-stage-key="FullImage/overlay">
|
|
47
|
+
<div class="fi-scrim fi-scrim--${position}"></div>
|
|
48
|
+
<div class="fi-overlay-inner">
|
|
49
|
+
<div class="fi-headline" data-stage-edit="overlay.headline" data-stage-key="FullImage/headline">${escape(overlay.headline || '')}</div>
|
|
50
|
+
${overlay.body ? `<div class="fi-body" data-stage-edit="overlay.body" data-stage-key="FullImage/body">${escape(overlay.body)}</div>` : ''}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
` : ''}
|
|
54
|
+
</div>
|
|
55
|
+
`;
|
|
56
|
+
},
|
|
57
|
+
init(el) {
|
|
58
|
+
const bg = el.querySelector('.fi-bg');
|
|
59
|
+
const headline = el.querySelector('.fi-headline');
|
|
60
|
+
const body = el.querySelector('.fi-body');
|
|
61
|
+
const timers = [];
|
|
62
|
+
timers.push(setTimeout(() => bg && bg.classList.add('in'), 50));
|
|
63
|
+
if (!overlay) {
|
|
64
|
+
// ken-burns on the bg
|
|
65
|
+
timers.push(setTimeout(() => bg && bg.classList.add('drift'), 400));
|
|
66
|
+
}
|
|
67
|
+
if (headline) timers.push(setTimeout(() => headline.classList.add('in'), 600));
|
|
68
|
+
if (body) timers.push(setTimeout(() => body.classList.add('in'), 900));
|
|
69
|
+
return () => timers.forEach(clearTimeout);
|
|
70
|
+
},
|
|
71
|
+
replay(el) {
|
|
72
|
+
el.querySelectorAll('.fi-bg, .fi-headline, .fi-body')
|
|
73
|
+
.forEach(n => { n.classList.remove('in'); n.classList.remove('drift'); });
|
|
74
|
+
return this.init(el);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function escape(s) {
|
|
80
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
81
|
+
}
|
|
82
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stage.Funnel — stacked trapezoids, narrowing top-to-bottom.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* Stage.register(Stage.Funnel({
|
|
8
|
+
* section: 8,
|
|
9
|
+
* title: '08 · Down to it',
|
|
10
|
+
* stages: [
|
|
11
|
+
* { label: 'Awareness', value: '10k', body: 'first touch' },
|
|
12
|
+
* { label: 'Trial', value: '2.4k' },
|
|
13
|
+
* { label: 'Adoption', value: '600' },
|
|
14
|
+
* { label: 'Champions', value: '42', color: 'accent', body: 'outcome' }
|
|
15
|
+
* ],
|
|
16
|
+
* reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
|
|
17
|
+
* }));
|
|
18
|
+
*
|
|
19
|
+
* Bottom is treated as the outcome and is implicitly accent-colored
|
|
20
|
+
* unless its own `color` is set.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
(function (root) {
|
|
24
|
+
const Stage = root.Stage = root.Stage || {};
|
|
25
|
+
|
|
26
|
+
Stage.Funnel = function (opts) {
|
|
27
|
+
const stages = opts.stages || [];
|
|
28
|
+
const reveal = opts.reveal || 'instant';
|
|
29
|
+
const n = stages.length;
|
|
30
|
+
|
|
31
|
+
// Top widest (100%) -> bottom narrowest (~40%).
|
|
32
|
+
function topWidth(i) {
|
|
33
|
+
if (n <= 1) return 100;
|
|
34
|
+
const min = 38;
|
|
35
|
+
const max = 100;
|
|
36
|
+
const t = i / n; // before this layer
|
|
37
|
+
return max - (max - min) * t;
|
|
38
|
+
}
|
|
39
|
+
function bottomWidth(i) {
|
|
40
|
+
if (n <= 1) return 38;
|
|
41
|
+
const min = 38;
|
|
42
|
+
const max = 100;
|
|
43
|
+
const t = (i + 1) / n;
|
|
44
|
+
return max - (max - min) * t;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const slide = {
|
|
48
|
+
section: opts.section,
|
|
49
|
+
title: opts.title,
|
|
50
|
+
transition: opts.transition,
|
|
51
|
+
render(el) {
|
|
52
|
+
el.innerHTML = `
|
|
53
|
+
<div class="funnel" data-stage-key="Funnel">
|
|
54
|
+
${stages.map((stage, i) => {
|
|
55
|
+
const tw = topWidth(i);
|
|
56
|
+
const bw = bottomWidth(i);
|
|
57
|
+
const isOutcome = i === n - 1;
|
|
58
|
+
const colorClass = stage.color
|
|
59
|
+
? 'fnc-' + escape(stage.color)
|
|
60
|
+
: (isOutcome ? 'fnc-accent' : '');
|
|
61
|
+
// Build a CSS clip-path polygon trapezoid (percentages within the layer).
|
|
62
|
+
const leftTop = ((100 - tw) / 2).toFixed(2);
|
|
63
|
+
const rightTop = (100 - (100 - tw) / 2).toFixed(2);
|
|
64
|
+
const leftBot = ((100 - bw) / 2).toFixed(2);
|
|
65
|
+
const rightBot = (100 - (100 - bw) / 2).toFixed(2);
|
|
66
|
+
const clip = `polygon(${leftTop}% 0%, ${rightTop}% 0%, ${rightBot}% 100%, ${leftBot}% 100%)`;
|
|
67
|
+
return `
|
|
68
|
+
<div class="fn-stage ${colorClass} ${isOutcome ? 'fn-outcome' : ''}"
|
|
69
|
+
data-step="${i + 1}"
|
|
70
|
+
data-stage-key="Funnel/stage[${i}]">
|
|
71
|
+
<div class="fn-shape" style="clip-path: ${clip}; -webkit-clip-path: ${clip};"></div>
|
|
72
|
+
<div class="fn-content" style="--fn-pad: ${((100 - Math.min(tw, bw)) / 2).toFixed(2)}%;">
|
|
73
|
+
<div class="fn-label" data-stage-edit="stages[${i}].label" data-stage-key="Funnel/stage[${i}]/label">${escape(stage.label || '')}</div>
|
|
74
|
+
${stage.value ? `<div class="fn-value" data-stage-edit="stages[${i}].value" data-stage-key="Funnel/stage[${i}]/value">${escape(stage.value)}</div>` : ''}
|
|
75
|
+
${stage.body ? `<div class="fn-body" data-stage-edit="stages[${i}].body" data-stage-key="Funnel/stage[${i}]/body">${escape(stage.body)}</div>` : ''}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
`;
|
|
79
|
+
}).join('')}
|
|
80
|
+
</div>
|
|
81
|
+
`;
|
|
82
|
+
if (reveal === 'instant') {
|
|
83
|
+
el.querySelectorAll('.fn-stage').forEach(n => n.classList.add('in'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (reveal === 'staggered') {
|
|
89
|
+
slide.init = function (el) {
|
|
90
|
+
return Stage.staggerIn(el.querySelectorAll('.fn-stage'), 220, 200);
|
|
91
|
+
};
|
|
92
|
+
slide.replay = function (el) {
|
|
93
|
+
el.querySelectorAll('.fn-stage').forEach(n => n.classList.remove('in'));
|
|
94
|
+
return this.init(el);
|
|
95
|
+
};
|
|
96
|
+
} else if (reveal === 'per-click') {
|
|
97
|
+
slide.steps = stages.length;
|
|
98
|
+
slide.onStep = function (el, step) {
|
|
99
|
+
el.querySelectorAll('.fn-stage').forEach(n => {
|
|
100
|
+
n.classList.toggle('in', Number(n.dataset.step) <= step);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return slide;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function escape(s) {
|
|
109
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
110
|
+
}
|
|
111
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|