stagecraft 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/AGENT.md +792 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -0
  4. package/bin/cli.js +51 -0
  5. package/bin/export.js +137 -0
  6. package/bin/init.js +52 -0
  7. package/bin/lib/edit-ops.js +405 -0
  8. package/bin/serve.js +278 -0
  9. package/dist/stagecraft.bundle.css +4443 -0
  10. package/dist/stagecraft.bundle.js +7621 -0
  11. package/dist/themes/brand.bundle.css +5262 -0
  12. package/dist/themes/neon.bundle.css +5289 -0
  13. package/dist/themes/paper.bundle.css +5276 -0
  14. package/dist/themes/phosphor.bundle.css +4443 -0
  15. package/dist/themes/shopware.bundle.css +5850 -0
  16. package/examples/closing-card.js +74 -0
  17. package/examples/orchestration-graph.js +156 -0
  18. package/examples/terminal-log.js +109 -0
  19. package/examples/token-stream.js +96 -0
  20. package/examples/whoami.js +90 -0
  21. package/package.json +41 -0
  22. package/src/components/activity-list.js +75 -0
  23. package/src/components/agenda.js +79 -0
  24. package/src/components/bar-chart.js +162 -0
  25. package/src/components/before-after.js +135 -0
  26. package/src/components/bento.js +73 -0
  27. package/src/components/big-number.js +87 -0
  28. package/src/components/callout.js +75 -0
  29. package/src/components/checklist.js +81 -0
  30. package/src/components/code-block.js +141 -0
  31. package/src/components/code-diff.js +98 -0
  32. package/src/components/compare.js +85 -0
  33. package/src/components/counter.js +80 -0
  34. package/src/components/cta.js +69 -0
  35. package/src/components/cycle.js +146 -0
  36. package/src/components/definition.js +96 -0
  37. package/src/components/donut-chart.js +179 -0
  38. package/src/components/full-image.js +82 -0
  39. package/src/components/funnel.js +111 -0
  40. package/src/components/gauge.js +147 -0
  41. package/src/components/heatmap.js +141 -0
  42. package/src/components/image-grid.js +80 -0
  43. package/src/components/image-text.js +96 -0
  44. package/src/components/kinetic-text.js +72 -0
  45. package/src/components/kpi.js +106 -0
  46. package/src/components/line-chart.js +215 -0
  47. package/src/components/manifesto.js +104 -0
  48. package/src/components/marquee.js +63 -0
  49. package/src/components/matrix2x2.js +151 -0
  50. package/src/components/pillars.js +80 -0
  51. package/src/components/pricing.js +90 -0
  52. package/src/components/process-flow.js +133 -0
  53. package/src/components/progress.js +136 -0
  54. package/src/components/punchline.js +82 -0
  55. package/src/components/pyramid.js +107 -0
  56. package/src/components/qanda.js +60 -0
  57. package/src/components/quote.js +70 -0
  58. package/src/components/roadmap.js +130 -0
  59. package/src/components/section-card.js +45 -0
  60. package/src/components/shift-arrow.js +41 -0
  61. package/src/components/spark-line.js +147 -0
  62. package/src/components/spotlight.js +85 -0
  63. package/src/components/statement.js +106 -0
  64. package/src/components/stats.js +91 -0
  65. package/src/components/steps.js +83 -0
  66. package/src/components/swot.js +110 -0
  67. package/src/components/team-grid.js +87 -0
  68. package/src/components/testimonial.js +99 -0
  69. package/src/components/timeline.js +91 -0
  70. package/src/components/tip.js +63 -0
  71. package/src/components/venn.js +198 -0
  72. package/src/edit-mode.js +1256 -0
  73. package/src/engine.js +823 -0
  74. package/src/helpers.js +169 -0
  75. package/src/transitions.js +101 -0
  76. package/starter/index.html +40 -0
  77. package/starter/slides/00-title.js +12 -0
  78. package/starter/stagecraft.config.js +8 -0
  79. package/themes/brand/base.css +4 -0
  80. package/themes/brand/components-business.css +173 -0
  81. package/themes/brand/components-chart.css +65 -0
  82. package/themes/brand/components-content.css +126 -0
  83. package/themes/brand/components-data.css +162 -0
  84. package/themes/brand/components-diagram.css +115 -0
  85. package/themes/brand/components-layout.css +112 -0
  86. package/themes/brand/components.css +46 -0
  87. package/themes/brand/manifest.json +20 -0
  88. package/themes/brand/tokens.css +20 -0
  89. package/themes/brand/transitions.css +4 -0
  90. package/themes/neon/base.css +10 -0
  91. package/themes/neon/components-business.css +189 -0
  92. package/themes/neon/components-chart.css +70 -0
  93. package/themes/neon/components-content.css +112 -0
  94. package/themes/neon/components-data.css +160 -0
  95. package/themes/neon/components-diagram.css +109 -0
  96. package/themes/neon/components-layout.css +87 -0
  97. package/themes/neon/components.css +87 -0
  98. package/themes/neon/manifest.json +21 -0
  99. package/themes/neon/tokens.css +17 -0
  100. package/themes/neon/transitions.css +13 -0
  101. package/themes/paper/base.css +9 -0
  102. package/themes/paper/components-business.css +196 -0
  103. package/themes/paper/components-chart.css +74 -0
  104. package/themes/paper/components-content.css +108 -0
  105. package/themes/paper/components-data.css +168 -0
  106. package/themes/paper/components-diagram.css +89 -0
  107. package/themes/paper/components-layout.css +105 -0
  108. package/themes/paper/components.css +60 -0
  109. package/themes/paper/manifest.json +10 -0
  110. package/themes/paper/tokens.css +21 -0
  111. package/themes/paper/transitions.css +11 -0
  112. package/themes/phosphor/base.css +511 -0
  113. package/themes/phosphor/components-business.css +818 -0
  114. package/themes/phosphor/components-chart.css +415 -0
  115. package/themes/phosphor/components-content.css +530 -0
  116. package/themes/phosphor/components-data.css +824 -0
  117. package/themes/phosphor/components-diagram.css +427 -0
  118. package/themes/phosphor/components-layout.css +450 -0
  119. package/themes/phosphor/components.css +223 -0
  120. package/themes/phosphor/manifest.json +11 -0
  121. package/themes/phosphor/tokens.css +17 -0
  122. package/themes/phosphor/transitions.css +213 -0
  123. package/themes/shopware/base.css +94 -0
  124. package/themes/shopware/components-business.css +344 -0
  125. package/themes/shopware/components-chart.css +121 -0
  126. package/themes/shopware/components-content.css +169 -0
  127. package/themes/shopware/components-data.css +219 -0
  128. package/themes/shopware/components-diagram.css +129 -0
  129. package/themes/shopware/components-layout.css +166 -0
  130. package/themes/shopware/components.css +83 -0
  131. package/themes/shopware/manifest.json +21 -0
  132. package/themes/shopware/tokens.css +68 -0
  133. package/themes/shopware/transitions.css +22 -0
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Steps — numbered tutorial steps (big numeral + label + body).
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Steps({
8
+ * section: 65,
9
+ * title: '65 · How it works',
10
+ * orientation: 'horizontal', // 'horizontal' | 'vertical'
11
+ * steps: [
12
+ * { number: '01', label: 'Install', body: 'Run npx stagecraft init.', icon: 'download' },
13
+ * { number: '02', label: 'Write', body: 'Add slides to slides/.', icon: 'edit' },
14
+ * { number: '03', label: 'Present', body: 'Open in any browser.', icon: 'present_to_all' }
15
+ * ],
16
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
17
+ * }));
18
+ *
19
+ * Steps differ from ProcessFlow: ProcessFlow is about *flow* (arrows between
20
+ * states); Steps is about *teaching* (numeral + explanation per step).
21
+ *
22
+ * Edit paths: steps[i].number / .label / .body / .icon
23
+ */
24
+
25
+ (function (root) {
26
+ const Stage = root.Stage = root.Stage || {};
27
+
28
+ Stage.Steps = function (opts) {
29
+ const stepsList = opts.steps || [];
30
+ const reveal = opts.reveal || 'instant';
31
+ const orientation = opts.orientation === 'vertical' ? 'vertical' : 'horizontal';
32
+
33
+ const slide = {
34
+ section: opts.section,
35
+ title: opts.title,
36
+ transition: opts.transition,
37
+ render(el) {
38
+ el.innerHTML = `
39
+ <div class="steps steps--${orientation}" data-stage-key="Steps">
40
+ ${stepsList.map((s, i) => `
41
+ <div class="step-card" data-step="${i + 1}" data-stage-key="Steps/step[${i}]">
42
+ <div class="step-numeral" data-stage-edit="steps[${i}].number">${escape(s.number || String(i + 1).padStart(2, '0'))}</div>
43
+ <div class="step-content">
44
+ <div class="step-head">
45
+ ${s.icon ? `<span class="step-icon material-symbols-outlined" data-stage-edit="steps[${i}].icon">${escape(s.icon)}</span>` : ''}
46
+ <div class="step-label" data-stage-edit="steps[${i}].label">${escape(s.label || '')}</div>
47
+ </div>
48
+ ${s.body ? `<div class="step-body" data-stage-edit="steps[${i}].body">${escape(s.body)}</div>` : ''}
49
+ </div>
50
+ </div>
51
+ `).join('')}
52
+ </div>
53
+ `;
54
+ if (reveal === 'instant') {
55
+ el.querySelectorAll('.step-card').forEach(n => n.classList.add('in'));
56
+ }
57
+ }
58
+ };
59
+
60
+ if (reveal === 'staggered') {
61
+ slide.init = function (el) {
62
+ return Stage.staggerIn(el.querySelectorAll('.step-card'), 200, 220);
63
+ };
64
+ slide.replay = function (el) {
65
+ el.querySelectorAll('.step-card').forEach(n => n.classList.remove('in'));
66
+ return this.init(el);
67
+ };
68
+ } else if (reveal === 'per-click') {
69
+ slide.steps = stepsList.length;
70
+ slide.onStep = function (el, step) {
71
+ el.querySelectorAll('.step-card').forEach(n => {
72
+ n.classList.toggle('in', Number(n.dataset.step) <= step);
73
+ });
74
+ };
75
+ }
76
+
77
+ return slide;
78
+ };
79
+
80
+ function escape(s) {
81
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
82
+ }
83
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.SWOT — 2x2 SWOT grid (Strengths / Weaknesses / Opportunities / Threats).
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.SWOT({
8
+ * section: 5,
9
+ * title: '05 · SWOT',
10
+ * strengths: ['fast iteration', 'strong brand', 'tier-1 talent'],
11
+ * weaknesses: ['legacy code', 'fragmented data'],
12
+ * opportunities: ['agentic tooling', 'developer SaaS reset'],
13
+ * threats: ['foundation model race', 'commoditisation'],
14
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
15
+ * }));
16
+ *
17
+ * S = accent · W = amber · O = blue · T = red.
18
+ *
19
+ * Edit paths: strengths[i] / weaknesses[i] / opportunities[i] / threats[i]
20
+ */
21
+
22
+ (function (root) {
23
+ const Stage = root.Stage = root.Stage || {};
24
+
25
+ Stage.SWOT = function (opts) {
26
+ const strengths = opts.strengths || [];
27
+ const weaknesses = opts.weaknesses || [];
28
+ const opportunities = opts.opportunities || [];
29
+ const threats = opts.threats || [];
30
+ const reveal = opts.reveal || 'instant';
31
+
32
+ const cells = [
33
+ { key: 'strengths', label: 'Strengths', short: 'S', color: 'accent', items: strengths },
34
+ { key: 'weaknesses', label: 'Weaknesses', short: 'W', color: 'amber', items: weaknesses },
35
+ { key: 'opportunities', label: 'Opportunities', short: 'O', color: 'blue', items: opportunities },
36
+ { key: 'threats', label: 'Threats', short: 'T', color: 'red', items: threats }
37
+ ];
38
+
39
+ const slide = {
40
+ section: opts.section,
41
+ title: opts.title,
42
+ transition: opts.transition,
43
+ render(el) {
44
+ const cellHtml = cells.map((cell, idx) => {
45
+ const items = cell.items.map((t, i) => `
46
+ <li class="swot-item" data-stage-edit="${cell.key}[${i}]" data-stage-key="SWOT/${cell.key}/item[${i}]">${escape(t)}</li>
47
+ `).join('');
48
+ return `
49
+ <div class="swot-cell swot-cell--${cell.color}"
50
+ data-quad-index="${idx}"
51
+ data-stage-key="SWOT/${cell.key}">
52
+ <div class="swot-head">
53
+ <span class="swot-letter">${cell.short}</span>
54
+ <span class="swot-title">${escape(cell.label)}</span>
55
+ </div>
56
+ <ul class="swot-list">${items}</ul>
57
+ </div>
58
+ `;
59
+ }).join('');
60
+
61
+ el.innerHTML = `
62
+ <div class="swot" data-stage-key="SWOT">
63
+ ${cellHtml}
64
+ </div>
65
+ `;
66
+
67
+ if (reveal === 'instant') {
68
+ el.querySelectorAll('.swot-cell, .swot-item').forEach(n => n.classList.add('in'));
69
+ }
70
+ }
71
+ };
72
+
73
+ if (reveal === 'staggered') {
74
+ slide.init = function (el) {
75
+ const cellNodes = Array.from(el.querySelectorAll('.swot-cell'));
76
+ const timers = [];
77
+ cellNodes.forEach((cell, i) => {
78
+ timers.push(setTimeout(() => {
79
+ cell.classList.add('in');
80
+ const items = cell.querySelectorAll('.swot-item');
81
+ items.forEach((it, j) => {
82
+ timers.push(setTimeout(() => it.classList.add('in'), 200 + j * 120));
83
+ });
84
+ }, 200 + i * 350));
85
+ });
86
+ return () => timers.forEach(clearTimeout);
87
+ };
88
+ slide.replay = function (el) {
89
+ el.querySelectorAll('.swot-cell, .swot-item').forEach(n => n.classList.remove('in'));
90
+ return this.init(el);
91
+ };
92
+ } else if (reveal === 'per-click') {
93
+ slide.steps = cells.length;
94
+ slide.onStep = function (el, step) {
95
+ el.querySelectorAll('.swot-cell').forEach(n => {
96
+ const idx = Number(n.dataset.quadIndex);
97
+ const on = idx < step;
98
+ n.classList.toggle('in', on);
99
+ n.querySelectorAll('.swot-item').forEach(it => it.classList.toggle('in', on));
100
+ });
101
+ };
102
+ }
103
+
104
+ return slide;
105
+ };
106
+
107
+ function escape(s) {
108
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
109
+ }
110
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.TeamGrid — grid of people cards with photo, name, role, bio, socials.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.TeamGrid({
8
+ * section: 62,
9
+ * title: '62 · The team',
10
+ * columns: 3,
11
+ * people: [
12
+ * { name: 'Avery Chen', role: 'CEO',
13
+ * photo: 'https://picsum.photos/seed/avery/400/400',
14
+ * bio: 'Ex-Stripe. Loves type systems.',
15
+ * social: { linkedin: '#', twitter: '#', github: '#' } }
16
+ * ],
17
+ * reveal: 'staggered' // 'instant' | 'staggered'
18
+ * }));
19
+ *
20
+ * Edit paths: people[i].name / .role / .bio
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ const SOCIAL_ICONS = {
27
+ linkedin: 'work',
28
+ twitter: 'alternate_email',
29
+ github: 'code'
30
+ };
31
+
32
+ Stage.TeamGrid = function (opts) {
33
+ const people = opts.people || [];
34
+ const reveal = opts.reveal || 'instant';
35
+ const columns = Math.min(4, Math.max(2, opts.columns || 3));
36
+
37
+ const slide = {
38
+ section: opts.section,
39
+ title: opts.title,
40
+ transition: opts.transition,
41
+ render(el) {
42
+ el.innerHTML = `
43
+ <div class="team-grid team-grid--cols-${columns}" data-stage-key="TeamGrid">
44
+ ${people.map((p, i) => `
45
+ <div class="team-card" data-step="${i + 1}" data-stage-key="TeamGrid/person[${i}]">
46
+ <div class="team-photo-wrap">
47
+ ${p.photo
48
+ ? `<img class="team-photo" src="${escape(p.photo)}" alt="${escape(p.name || '')}">`
49
+ : `<span class="team-photo team-photo--placeholder material-symbols-outlined">person</span>`}
50
+ </div>
51
+ <div class="team-name" data-stage-edit="people[${i}].name">${escape(p.name || '')}</div>
52
+ <div class="team-role" data-stage-edit="people[${i}].role">${escape(p.role || '')}</div>
53
+ ${p.bio ? `<div class="team-bio" data-stage-edit="people[${i}].bio">${escape(p.bio)}</div>` : ''}
54
+ ${p.social ? `
55
+ <div class="team-social" data-stage-key="TeamGrid/person[${i}]/social">
56
+ ${Object.entries(p.social).filter(([, v]) => v).map(([k]) => `
57
+ <span class="team-social-icon material-symbols-outlined" title="${escape(k)}">${SOCIAL_ICONS[k] || 'link'}</span>
58
+ `).join('')}
59
+ </div>
60
+ ` : ''}
61
+ </div>
62
+ `).join('')}
63
+ </div>
64
+ `;
65
+ if (reveal === 'instant') {
66
+ el.querySelectorAll('.team-card').forEach(n => n.classList.add('in'));
67
+ }
68
+ }
69
+ };
70
+
71
+ if (reveal === 'staggered') {
72
+ slide.init = function (el) {
73
+ return Stage.staggerIn(el.querySelectorAll('.team-card'), 120, 180);
74
+ };
75
+ slide.replay = function (el) {
76
+ el.querySelectorAll('.team-card').forEach(n => n.classList.remove('in'));
77
+ return this.init(el);
78
+ };
79
+ }
80
+
81
+ return slide;
82
+ };
83
+
84
+ function escape(s) {
85
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
86
+ }
87
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Testimonial — single large quote with photo + attribution.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Testimonial({
8
+ * section: 61,
9
+ * title: '61 · Testimonial',
10
+ * quote: 'It cut our review cycles in half.',
11
+ * author: {
12
+ * name: 'Avery Chen',
13
+ * role: 'Staff Engineer',
14
+ * company: 'Acme Co',
15
+ * photo: 'https://picsum.photos/seed/avery/400/400',
16
+ * logo: 'https://example.com/logo.svg'
17
+ * },
18
+ * reveal: 'staggered' // 'instant' | 'staggered'
19
+ * }));
20
+ *
21
+ * Edit paths: quote / author.name / author.role / author.company
22
+ */
23
+
24
+ (function (root) {
25
+ const Stage = root.Stage = root.Stage || {};
26
+
27
+ Stage.Testimonial = function (opts) {
28
+ const author = opts.author || {};
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="testimonial" data-stage-key="Testimonial">
38
+ ${author.photo ? `
39
+ <div class="testimonial-photo-wrap" data-stage-key="Testimonial/photo">
40
+ <img class="testimonial-photo" src="${escape(author.photo)}" alt="${escape(author.name || '')}">
41
+ </div>
42
+ ` : ''}
43
+ <div class="testimonial-body">
44
+ <div class="testimonial-mark material-symbols-outlined">format_quote</div>
45
+ <blockquote class="testimonial-quote"
46
+ data-stage-edit="quote"
47
+ data-stage-key="Testimonial/quote">${escape(opts.quote || '')}</blockquote>
48
+ <div class="testimonial-meta" data-stage-key="Testimonial/meta">
49
+ <div class="testimonial-author" data-stage-edit="author.name">${escape(author.name || '')}</div>
50
+ <div class="testimonial-affil">
51
+ ${author.role ? `<span data-stage-edit="author.role">${escape(author.role)}</span>` : ''}
52
+ ${author.role && author.company ? `<span class="testimonial-sep">·</span>` : ''}
53
+ ${author.company ? `<span class="testimonial-company" data-stage-edit="author.company">${escape(author.company)}</span>` : ''}
54
+ </div>
55
+ ${author.logo ? `
56
+ <img class="testimonial-logo" src="${escape(author.logo)}" alt="${escape(author.company || 'company')}">
57
+ ` : ''}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ `;
62
+ if (reveal === 'instant') {
63
+ el.querySelectorAll('[data-anim]').forEach(n => n.classList.add('in'));
64
+ el.querySelector('.testimonial')?.classList.add('in');
65
+ }
66
+ }
67
+ };
68
+
69
+ if (reveal === 'staggered') {
70
+ slide.init = function (el) {
71
+ const t = el.querySelector('.testimonial');
72
+ const nodes = [
73
+ el.querySelector('.testimonial-photo-wrap'),
74
+ el.querySelector('.testimonial-mark'),
75
+ el.querySelector('.testimonial-quote'),
76
+ el.querySelector('.testimonial-meta')
77
+ ].filter(Boolean);
78
+ const timers = [];
79
+ timers.push(setTimeout(() => t?.classList.add('in'), 80));
80
+ nodes.forEach((n, i) => {
81
+ timers.push(setTimeout(() => n.classList.add('in'), 220 + i * 240));
82
+ });
83
+ return () => timers.forEach(clearTimeout);
84
+ };
85
+ slide.replay = function (el) {
86
+ el.querySelector('.testimonial')?.classList.remove('in');
87
+ el.querySelectorAll('.testimonial-photo-wrap, .testimonial-mark, .testimonial-quote, .testimonial-meta')
88
+ .forEach(n => n.classList.remove('in'));
89
+ return this.init(el);
90
+ };
91
+ }
92
+
93
+ return slide;
94
+ };
95
+
96
+ function escape(s) {
97
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
98
+ }
99
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Timeline — horizontal (default) or vertical milestone track.
5
+ *
6
+ * Usage:
7
+ * Stage.register(Stage.Timeline({
8
+ * section: 5,
9
+ * title: '05 · The arc',
10
+ * orientation: 'horizontal', // 'horizontal' | 'vertical'
11
+ * events: [
12
+ * { date: '2022', heading: 'Copilot', body: 'autocomplete', icon: 'flash_on' },
13
+ * { date: '2023', heading: 'ChatGPT', body: 'conversation', icon: 'chat' },
14
+ * { date: '2024', heading: 'Agents', body: 'autonomy', icon: 'memory' }
15
+ * ],
16
+ * reveal: 'staggered' // 'instant' | 'staggered' | 'per-click'
17
+ * }));
18
+ *
19
+ * Edit paths:
20
+ * events[i].date / .heading / .body / .icon
21
+ */
22
+
23
+ (function (root) {
24
+ const Stage = root.Stage = root.Stage || {};
25
+
26
+ Stage.Timeline = function (opts) {
27
+ const events = opts.events || [];
28
+ const reveal = opts.reveal || 'instant';
29
+ const orientation = opts.orientation === 'vertical' ? 'vertical' : 'horizontal';
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="timeline timeline--${orientation}" data-stage-key="Timeline">
38
+ <div class="timeline-track"></div>
39
+ <div class="timeline-events">
40
+ ${events.map((ev, i) => `
41
+ <div class="tl-event ${ev.color ? 'tc-' + escape(ev.color) : ''}" data-step="${i + 1}" data-stage-key="Timeline/event[${i}]">
42
+ <div class="tl-date" data-stage-edit="events[${i}].date" data-stage-key="Timeline/event[${i}]/date">${escape(ev.date || '')}</div>
43
+ <div class="tl-dot">
44
+ ${ev.icon ? `<span class="tl-icon material-symbols-outlined" data-stage-edit="events[${i}].icon">${escape(ev.icon)}</span>` : ''}
45
+ </div>
46
+ <div class="tl-text">
47
+ <div class="tl-heading" data-stage-edit="events[${i}].heading" data-stage-key="Timeline/event[${i}]/heading">${escape(ev.heading || '')}</div>
48
+ ${ev.body ? `<div class="tl-body" data-stage-edit="events[${i}].body" data-stage-key="Timeline/event[${i}]/body">${escape(ev.body)}</div>` : ''}
49
+ </div>
50
+ </div>
51
+ `).join('')}
52
+ </div>
53
+ </div>
54
+ `;
55
+ if (reveal === 'instant') {
56
+ el.querySelectorAll('.tl-event').forEach(n => n.classList.add('in', 'active'));
57
+ }
58
+ }
59
+ };
60
+
61
+ if (reveal === 'staggered') {
62
+ slide.init = function (el) {
63
+ const nodes = el.querySelectorAll('.tl-event');
64
+ const timers = [];
65
+ nodes.forEach((n, i) => {
66
+ timers.push(setTimeout(() => { n.classList.add('in'); n.classList.add('active'); }, 200 + i * 280));
67
+ });
68
+ return () => timers.forEach(clearTimeout);
69
+ };
70
+ slide.replay = function (el) {
71
+ el.querySelectorAll('.tl-event').forEach(n => n.classList.remove('in', 'active'));
72
+ return this.init(el);
73
+ };
74
+ } else if (reveal === 'per-click') {
75
+ slide.steps = events.length;
76
+ slide.onStep = function (el, step) {
77
+ el.querySelectorAll('.tl-event').forEach(n => {
78
+ const idx = Number(n.dataset.step);
79
+ n.classList.toggle('in', idx <= step);
80
+ n.classList.toggle('active', idx === step);
81
+ });
82
+ };
83
+ }
84
+
85
+ return slide;
86
+ };
87
+
88
+ function escape(s) {
89
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
90
+ }
91
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage.Tip — compact single-paragraph emphasis box.
5
+ *
6
+ * Use when you want to highlight one thought (not a whole section).
7
+ * For multi-line callouts with heading + body, use Stage.Callout.
8
+ *
9
+ * Usage:
10
+ * Stage.register(Stage.Tip({
11
+ * section: 68,
12
+ * title: '68 · Tip',
13
+ * kind: 'tip', // 'info' | 'tip' | 'warning' | 'danger' | 'success'
14
+ * icon: 'lightbulb', // optional
15
+ * body: 'Think before you commit. Commit before you think too much.'
16
+ * }));
17
+ *
18
+ * Edit path: body
19
+ */
20
+
21
+ (function (root) {
22
+ const Stage = root.Stage = root.Stage || {};
23
+
24
+ const DEFAULTS = {
25
+ info: { icon: 'info' },
26
+ tip: { icon: 'lightbulb' },
27
+ warning: { icon: 'warning' },
28
+ danger: { icon: 'error' },
29
+ success: { icon: 'check_circle' }
30
+ };
31
+
32
+ Stage.Tip = function (opts) {
33
+ const kind = DEFAULTS[opts.kind] ? opts.kind : 'tip';
34
+ const icon = opts.icon || DEFAULTS[kind].icon;
35
+
36
+ return {
37
+ section: opts.section,
38
+ title: opts.title,
39
+ transition: opts.transition,
40
+ render(el) {
41
+ el.innerHTML = `
42
+ <div class="tip tip--${kind}" data-stage-key="Tip">
43
+ <span class="tip-icon material-symbols-outlined">${escape(icon)}</span>
44
+ <p class="tip-body" data-stage-edit="body" data-stage-key="Tip/body">${escape(opts.body || '')}</p>
45
+ </div>
46
+ `;
47
+ },
48
+ init(el) {
49
+ const tip = el.querySelector('.tip');
50
+ const t = setTimeout(() => tip?.classList.add('in'), 100);
51
+ return () => clearTimeout(t);
52
+ },
53
+ replay(el) {
54
+ el.querySelector('.tip')?.classList.remove('in');
55
+ return this.init(el);
56
+ }
57
+ };
58
+ };
59
+
60
+ function escape(s) {
61
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
62
+ }
63
+ })(typeof window !== 'undefined' ? window : globalThis);