create-slide-deck 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 (105) hide show
  1. package/dist/index.js +119 -0
  2. package/package.json +36 -0
  3. package/template-full/README.md +99 -0
  4. package/template-full/package.json +47 -0
  5. package/template-full/src/reveal/components/auto-layout.ts +229 -0
  6. package/template-full/src/reveal/components/charts.tsx +213 -0
  7. package/template-full/src/reveal/core/blocks.ts +172 -0
  8. package/template-full/src/reveal/core/deck-init.ts +60 -0
  9. package/template-full/src/reveal/core/design.ts +46 -0
  10. package/template-full/src/reveal/core/layout.ts +187 -0
  11. package/template-full/src/reveal/core/mount-registry.ts +41 -0
  12. package/template-full/src/reveal/core/presets.ts +189 -0
  13. package/template-full/src/reveal/core/runtime.ts +141 -0
  14. package/template-full/src/reveal/core/types.ts +114 -0
  15. package/template-full/src/reveal/data/algorithms.ts +78 -0
  16. package/template-full/src/reveal/data/benchmark.ts +79 -0
  17. package/template-full/src/reveal/decks/demo-showcase/components/demo-arc-progress.tsx +153 -0
  18. package/template-full/src/reveal/decks/demo-showcase/components/demo-before-after.tsx +164 -0
  19. package/template-full/src/reveal/decks/demo-showcase/components/demo-bigtext.tsx +70 -0
  20. package/template-full/src/reveal/decks/demo-showcase/components/demo-card-flip.tsx +118 -0
  21. package/template-full/src/reveal/decks/demo-showcase/components/demo-chat-bubbles.tsx +257 -0
  22. package/template-full/src/reveal/decks/demo-showcase/components/demo-code.tsx +136 -0
  23. package/template-full/src/reveal/decks/demo-showcase/components/demo-concept-map.tsx +336 -0
  24. package/template-full/src/reveal/decks/demo-showcase/components/demo-counter.tsx +194 -0
  25. package/template-full/src/reveal/decks/demo-showcase/components/demo-cover.tsx +188 -0
  26. package/template-full/src/reveal/decks/demo-showcase/components/demo-dark-dashboard.tsx +166 -0
  27. package/template-full/src/reveal/decks/demo-showcase/components/demo-eval-matrix.tsx +191 -0
  28. package/template-full/src/reveal/decks/demo-showcase/components/demo-force-graph.tsx +169 -0
  29. package/template-full/src/reveal/decks/demo-showcase/components/demo-fullbleed-bars.tsx +109 -0
  30. package/template-full/src/reveal/decks/demo-showcase/components/demo-fullbleed-flow.tsx +177 -0
  31. package/template-full/src/reveal/decks/demo-showcase/components/demo-heatmap.tsx +135 -0
  32. package/template-full/src/reveal/decks/demo-showcase/components/demo-icon-wall.tsx +143 -0
  33. package/template-full/src/reveal/decks/demo-showcase/components/demo-math.tsx +103 -0
  34. package/template-full/src/reveal/decks/demo-showcase/components/demo-number-morph.tsx +126 -0
  35. package/template-full/src/reveal/decks/demo-showcase/components/demo-path.tsx +185 -0
  36. package/template-full/src/reveal/decks/demo-showcase/components/demo-radar.tsx +124 -0
  37. package/template-full/src/reveal/decks/demo-showcase/components/demo-rough.tsx +169 -0
  38. package/template-full/src/reveal/decks/demo-showcase/components/demo-sankey.tsx +144 -0
  39. package/template-full/src/reveal/decks/demo-showcase/components/demo-screenshot-annotate.tsx +181 -0
  40. package/template-full/src/reveal/decks/demo-showcase/components/demo-stacked-cards.tsx +159 -0
  41. package/template-full/src/reveal/decks/demo-showcase/components/demo-tabs.tsx +206 -0
  42. package/template-full/src/reveal/decks/demo-showcase/components/demo-timeline.tsx +162 -0
  43. package/template-full/src/reveal/decks/demo-showcase/components/demo-treemap.tsx +161 -0
  44. package/template-full/src/reveal/decks/demo-showcase/components/demo-zoom-focus.tsx +223 -0
  45. package/template-full/src/reveal/decks/demo-showcase/components/registry.ts +63 -0
  46. package/template-full/src/reveal/decks/demo-showcase/demo.css +237 -0
  47. package/template-full/src/reveal/decks/demo-showcase/index.html +24 -0
  48. package/template-full/src/reveal/decks/demo-showcase/main.ts +7 -0
  49. package/template-full/src/reveal/decks/demo-showcase/slides.ts +271 -0
  50. package/template-full/src/reveal/decks/fse26-rca/components/aws-cascade.tsx +295 -0
  51. package/template-full/src/reveal/decks/fse26-rca/components/bench-compare.tsx +64 -0
  52. package/template-full/src/reveal/decks/fse26-rca/components/bench-deficiency.tsx +104 -0
  53. package/template-full/src/reveal/decks/fse26-rca/components/bench-loop.tsx +402 -0
  54. package/template-full/src/reveal/decks/fse26-rca/components/bench-needs.tsx +78 -0
  55. package/template-full/src/reveal/decks/fse26-rca/components/closing-takeaway.tsx +165 -0
  56. package/template-full/src/reveal/decks/fse26-rca/components/cloud-incidents.tsx +88 -0
  57. package/template-full/src/reveal/decks/fse26-rca/components/failure-modes.tsx +59 -0
  58. package/template-full/src/reveal/decks/fse26-rca/components/fault-heatmap.tsx +85 -0
  59. package/template-full/src/reveal/decks/fse26-rca/components/hierarchy-tree.tsx +93 -0
  60. package/template-full/src/reveal/decks/fse26-rca/components/incident-hard.tsx +72 -0
  61. package/template-full/src/reveal/decks/fse26-rca/components/rca-pipeline.tsx +193 -0
  62. package/template-full/src/reveal/decks/fse26-rca/components/registry.ts +37 -0
  63. package/template-full/src/reveal/decks/fse26-rca/components/simple-rca.tsx +216 -0
  64. package/template-full/src/reveal/decks/fse26-rca/components/sota-collapse.tsx +63 -0
  65. package/template-full/src/reveal/decks/fse26-rca/components/srca-results.tsx +115 -0
  66. package/template-full/src/reveal/decks/fse26-rca/images/aws-outage-2025-deployflow.png +0 -0
  67. package/template-full/src/reveal/decks/fse26-rca/images/aws-post-event-summary.png +0 -0
  68. package/template-full/src/reveal/decks/fse26-rca/images/bbc-crowdstrike.png +0 -0
  69. package/template-full/src/reveal/decks/fse26-rca/images/cnn-meta-outage-2021.png +0 -0
  70. package/template-full/src/reveal/decks/fse26-rca/images/cover.png +0 -0
  71. package/template-full/src/reveal/decks/fse26-rca/images/nyt-facebook-2021.png +0 -0
  72. package/template-full/src/reveal/decks/fse26-rca/images/qr-repo.png +0 -0
  73. package/template-full/src/reveal/decks/fse26-rca/images/verge-crowdstrike-2024.png +0 -0
  74. package/template-full/src/reveal/decks/fse26-rca/images/wiki-meta-outage-2021.png +0 -0
  75. package/template-full/src/reveal/decks/fse26-rca/index.html +30 -0
  76. package/template-full/src/reveal/decks/fse26-rca/main.ts +8 -0
  77. package/template-full/src/reveal/decks/fse26-rca/slides.ts +175 -0
  78. package/template-full/src/reveal/env.d.ts +38 -0
  79. package/template-full/src/reveal/theme.css +762 -0
  80. package/template-full/src/reveal/tools/dev.mjs +120 -0
  81. package/template-full/src/reveal/tools/export-pdf.mjs +86 -0
  82. package/template-full/src/reveal/tools/preview.mjs +132 -0
  83. package/template-full/tsconfig.json +19 -0
  84. package/template-full/vite.config.ts +95 -0
  85. package/template-minimal/package.json +42 -0
  86. package/template-minimal/src/reveal/components/auto-layout.ts +229 -0
  87. package/template-minimal/src/reveal/components/charts.tsx +213 -0
  88. package/template-minimal/src/reveal/core/blocks.ts +172 -0
  89. package/template-minimal/src/reveal/core/deck-init.ts +60 -0
  90. package/template-minimal/src/reveal/core/design.ts +46 -0
  91. package/template-minimal/src/reveal/core/layout.ts +187 -0
  92. package/template-minimal/src/reveal/core/mount-registry.ts +41 -0
  93. package/template-minimal/src/reveal/core/presets.ts +189 -0
  94. package/template-minimal/src/reveal/core/runtime.ts +141 -0
  95. package/template-minimal/src/reveal/core/types.ts +114 -0
  96. package/template-minimal/src/reveal/data/.gitkeep +0 -0
  97. package/template-minimal/src/reveal/decks/my-deck/components/example-component.tsx +28 -0
  98. package/template-minimal/src/reveal/decks/my-deck/components/registry.ts +9 -0
  99. package/template-minimal/src/reveal/decks/my-deck/index.html +14 -0
  100. package/template-minimal/src/reveal/decks/my-deck/main.ts +5 -0
  101. package/template-minimal/src/reveal/decks/my-deck/slides.ts +34 -0
  102. package/template-minimal/src/reveal/env.d.ts +38 -0
  103. package/template-minimal/src/reveal/theme.css +762 -0
  104. package/template-minimal/tsconfig.json +19 -0
  105. package/template-minimal/vite.config.ts +95 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Generic empty-slot finder for slide annotations.
3
+ *
4
+ * Given a canvas size, a list of occupied rectangles, and a desired slot size,
5
+ * finds the best position that:
6
+ * 1. Does not overlap any occupied rect (with margin)
7
+ * 2. Maximizes distance from occupied rects (use empty space well)
8
+ * 3. Optionally stays close to a "near" point (e.g. the active node)
9
+ *
10
+ * Usage:
11
+ * const pos = findEmptySlot({
12
+ * canvasW: 1200, canvasH: 700,
13
+ * occupied: [ {x:10, y:10, w:200, h:150}, ... ],
14
+ * slotW: 500, slotH: 160,
15
+ * margin: 16,
16
+ * near: { x: 300, y: 100 }, // optional: prefer positions near this point
17
+ * });
18
+ * // pos = { x, y }
19
+ */
20
+
21
+ export interface Rect {
22
+ x: number;
23
+ y: number;
24
+ w: number;
25
+ h: number;
26
+ }
27
+
28
+ export interface Point {
29
+ x: number;
30
+ y: number;
31
+ }
32
+
33
+ export interface FindEmptySlotOpts {
34
+ canvasW: number;
35
+ canvasH: number;
36
+ occupied: Rect[];
37
+ slotW: number;
38
+ slotH: number;
39
+ margin?: number;
40
+ near?: Point | null;
41
+ step?: number;
42
+ }
43
+
44
+ export interface FindLargestSlotOpts {
45
+ canvasW: number;
46
+ canvasH: number;
47
+ occupied: Rect[];
48
+ maxW?: number;
49
+ maxH?: number;
50
+ margin?: number;
51
+ step?: number;
52
+ near?: Point | null;
53
+ }
54
+
55
+ interface CascadeNode {
56
+ step: number;
57
+ id: string;
58
+ col: number;
59
+ row: number;
60
+ }
61
+
62
+ export interface NodeOccupiedRectsOpts {
63
+ nx: (col: number) => number;
64
+ ny: (row: number) => number;
65
+ iconR: number;
66
+ labelH?: number;
67
+ }
68
+
69
+ function rectsOverlap(a: Rect, b: Rect, gap: number): boolean {
70
+ return !(a.x + a.w + gap <= b.x || b.x + b.w + gap <= a.x ||
71
+ a.y + a.h + gap <= b.y || b.y + b.h + gap <= a.y);
72
+ }
73
+
74
+ function rectMinDist(slot: Rect, rect: Rect): number {
75
+ const dx = Math.max(rect.x - (slot.x + slot.w), slot.x - (rect.x + rect.w), 0);
76
+ const dy = Math.max(rect.y - (slot.y + slot.h), slot.y - (rect.y + rect.h), 0);
77
+ return Math.sqrt(dx * dx + dy * dy);
78
+ }
79
+
80
+ export function findEmptySlot({
81
+ canvasW,
82
+ canvasH,
83
+ occupied,
84
+ slotW,
85
+ slotH,
86
+ margin = 16,
87
+ near = null,
88
+ step = 16,
89
+ }: FindEmptySlotOpts): Point {
90
+ let bestPos: Point = { x: margin, y: canvasH - slotH - margin };
91
+ let bestScore = -Infinity;
92
+
93
+ for (let y = margin; y + slotH <= canvasH - margin; y += step) {
94
+ for (let x = margin; x + slotW <= canvasW - margin; x += step) {
95
+ const slot: Rect = { x: x, y: y, w: slotW, h: slotH };
96
+
97
+ let overlaps = false;
98
+ for (let i = 0; i < occupied.length; i++) {
99
+ if (rectsOverlap(slot, occupied[i], margin)) {
100
+ overlaps = true;
101
+ break;
102
+ }
103
+ }
104
+ if (overlaps) continue;
105
+
106
+ let minDist = Infinity;
107
+ for (let j = 0; j < occupied.length; j++) {
108
+ const d = rectMinDist(slot, occupied[j]);
109
+ if (d < minDist) minDist = d;
110
+ }
111
+
112
+ // Small bonus for not being jammed against a node, but cap it low
113
+ let score = Math.min(minDist, 30) * 0.5;
114
+
115
+ // Primary: stay as close as possible to the active node
116
+ if (near) {
117
+ const dx = (x + slotW / 2) - near.x;
118
+ const dy = (y + slotH / 2) - near.y;
119
+ score -= Math.sqrt(dx * dx + dy * dy) * 0.8;
120
+ }
121
+
122
+ // Slight preference for horizontally centered positions
123
+ const cxDist = Math.abs(x + slotW / 2 - canvasW / 2);
124
+ score -= cxDist * 0.02;
125
+
126
+ if (score > bestScore) {
127
+ bestScore = score;
128
+ bestPos = { x: x, y: y };
129
+ }
130
+ }
131
+ }
132
+
133
+ return bestPos;
134
+ }
135
+
136
+ /**
137
+ * Find the largest empty rectangle in the canvas that doesn't overlap any occupied rect.
138
+ * Returns { x, y, w, h }. If maxW/maxH are provided, the result is capped.
139
+ */
140
+ export function findLargestSlot({
141
+ canvasW, canvasH, occupied,
142
+ maxW, maxH,
143
+ margin = 14,
144
+ step = 18,
145
+ near = null,
146
+ }: FindLargestSlotOpts): Rect {
147
+ maxW = maxW || (canvasW - 2 * margin);
148
+ maxH = maxH || (canvasH - 2 * margin);
149
+
150
+ let best: Rect | null = null;
151
+ let bestScore = -Infinity;
152
+
153
+ for (let y = margin; y < canvasH - margin - 60; y += step) {
154
+ for (let x = margin; x < canvasW - margin - 60; x += step) {
155
+ let inside = false;
156
+ for (let i = 0; i < occupied.length; i++) {
157
+ const r = occupied[i];
158
+ if (x >= r.x - margin && x < r.x + r.w + margin &&
159
+ y >= r.y - margin && y < r.y + r.h + margin) {
160
+ inside = true; break;
161
+ }
162
+ }
163
+ if (inside) continue;
164
+
165
+ let w = Math.min(canvasW - margin - x, maxW);
166
+ for (let i = 0; i < occupied.length; i++) {
167
+ const r = occupied[i];
168
+ const rl = r.x - margin;
169
+ if (rl > x && rl < x + w &&
170
+ y < r.y + r.h + margin && y + maxH > r.y - margin) {
171
+ w = Math.min(w, rl - x);
172
+ }
173
+ }
174
+
175
+ let h = Math.min(canvasH - margin - y, maxH);
176
+ for (let i = 0; i < occupied.length; i++) {
177
+ const r = occupied[i];
178
+ const rt = r.y - margin;
179
+ if (rt > y && rt < y + h &&
180
+ x < r.x + r.w + margin && x + w > r.x - margin) {
181
+ h = Math.min(h, rt - y);
182
+ }
183
+ }
184
+
185
+ if (w < 60 || h < 60) continue;
186
+
187
+ const area = w * h;
188
+ let score = area;
189
+ if (near) {
190
+ const dx = (x + w / 2) - near.x;
191
+ const dy = (y + h / 2) - near.y;
192
+ score -= Math.sqrt(dx * dx + dy * dy) * 50;
193
+ }
194
+
195
+ if (score > bestScore) {
196
+ bestScore = score;
197
+ best = { x: x, y: y, w: w, h: h };
198
+ }
199
+ }
200
+ }
201
+
202
+ return best || { x: margin, y: margin, w: maxW, h: maxH };
203
+ }
204
+
205
+ /**
206
+ * Build occupied rectangles from cascade nodes that are visible at a given step.
207
+ * Each node becomes a bounding rect covering its icon circle + label area.
208
+ */
209
+ export function nodeOccupiedRects(
210
+ nodes: CascadeNode[],
211
+ visibleStep: number,
212
+ { nx, ny, iconR, labelH = 50 }: NodeOccupiedRectsOpts,
213
+ ): Rect[] {
214
+ const rects: Rect[] = [];
215
+ for (let i = 0; i < nodes.length; i++) {
216
+ const n = nodes[i];
217
+ if (n.step > visibleStep) continue;
218
+ const cx = nx(n.col);
219
+ const cy = ny(n.row);
220
+ const r = (n.id === "dns" || n.id === "users") ? iconR + 4 : iconR;
221
+ rects.push({
222
+ x: cx - r - 12,
223
+ y: cy - r - 30,
224
+ w: 2 * (r + 12),
225
+ h: 2 * r + 30 + labelH,
226
+ });
227
+ }
228
+ return rects;
229
+ }
@@ -0,0 +1,213 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ import { CANVAS, MIN_FONT } from "../core/presets.ts";
4
+
5
+ /**
6
+ * Reusable SVG chart primitives.
7
+ * Each is a plain function returning an SVG sub-tree (not a full component).
8
+ */
9
+
10
+ export interface HorizontalBarItem {
11
+ label: string;
12
+ value: number;
13
+ maxValue?: number;
14
+ color?: string;
15
+ bold?: boolean;
16
+ extra?: ReactNode;
17
+ }
18
+
19
+ export interface HorizontalBarsProps {
20
+ items?: HorizontalBarItem[];
21
+ x?: number;
22
+ y?: number;
23
+ barX?: number;
24
+ barMaxW?: number;
25
+ rowH?: number;
26
+ labelFont?: number;
27
+ valueFont?: number;
28
+ fontFamily?: string;
29
+ }
30
+
31
+ export interface BubbleMatrixRow {
32
+ label: string;
33
+ values?: number[];
34
+ }
35
+
36
+ export interface BubbleMatrixProps {
37
+ rows?: BubbleMatrixRow[];
38
+ columns?: string[];
39
+ x?: number;
40
+ y?: number;
41
+ colW?: number;
42
+ rowH?: number;
43
+ maxR?: number;
44
+ colorFn?: (value: number) => string;
45
+ fontFamily?: string;
46
+ headerFont?: number;
47
+ labelFont?: number;
48
+ }
49
+
50
+ export interface StatStackItem {
51
+ value: string | number;
52
+ label: string;
53
+ }
54
+
55
+ export interface StatStackProps {
56
+ items?: StatStackItem[];
57
+ cx?: number;
58
+ y?: number;
59
+ gap?: number;
60
+ valueFont?: number;
61
+ labelFont?: number;
62
+ color?: string;
63
+ fontFamily?: string;
64
+ }
65
+
66
+ /**
67
+ * Labeled horizontal bar chart.
68
+ * items: { label, value, maxValue, color, bold, extra }[] (extra: optional JSX after the bar)
69
+ */
70
+ export function HorizontalBars(props: HorizontalBarsProps): ReactNode {
71
+ const items = props.items || [];
72
+ const x = props.x || 0;
73
+ const y = props.y || 0;
74
+ const barX = props.barX != null ? props.barX : x + 120;
75
+ const barMaxW = props.barMaxW || 240;
76
+ const rowH = props.rowH || 32;
77
+ const labelFont = Math.max(props.labelFont || 13, MIN_FONT - 1);
78
+ const valueFont = Math.max(props.valueFont || 13, MIN_FONT - 1);
79
+ const fontFamily = props.fontFamily || CANVAS.fontFamily;
80
+
81
+ return items.map(function (item, i) {
82
+ const ry = y + i * rowH;
83
+ const cy = ry + rowH / 2;
84
+ const maxValue = item.maxValue || 1;
85
+ const barW = (item.value / maxValue) * barMaxW;
86
+ const weight = item.bold ? 900 : 700;
87
+
88
+ return React.createElement(
89
+ "g",
90
+ { key: i },
91
+ React.createElement(
92
+ "text",
93
+ {
94
+ x: barX - 8, y: cy, textAnchor: "end", dominantBaseline: "middle",
95
+ fontSize: labelFont, fontWeight: weight, fill: item.color || "#313131",
96
+ fontFamily: fontFamily,
97
+ },
98
+ item.label
99
+ ),
100
+ React.createElement("rect", {
101
+ x: barX, y: ry + 5, width: barW, height: rowH - 12, rx: 3,
102
+ fill: item.color || "#00A6D6", fillOpacity: 0.18,
103
+ stroke: item.color || "#00A6D6", strokeWidth: item.bold ? 2 : 1,
104
+ }),
105
+ React.createElement(
106
+ "text",
107
+ {
108
+ x: barX + barW + 6, y: cy, dominantBaseline: "middle",
109
+ fontSize: valueFont, fontWeight: 900, fill: item.color || "#313131",
110
+ fontFamily: fontFamily,
111
+ },
112
+ item.value
113
+ ),
114
+ item.extra != null
115
+ ? React.createElement("g", { transform: "translate(0," + cy + ")" }, item.extra)
116
+ : null
117
+ );
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Grid of sized/colored bubbles.
123
+ * rows: { label, values: number[] }[]
124
+ * columns: string[] headers
125
+ */
126
+ export function BubbleMatrix(props: BubbleMatrixProps): ReactNode {
127
+ const rows = props.rows || [];
128
+ const columns = props.columns || [];
129
+ const x = props.x || 0;
130
+ const y = props.y || 0;
131
+ const colW = props.colW || 50;
132
+ const rowH = props.rowH || 17;
133
+ const maxR = props.maxR || 7;
134
+ const colorFn = props.colorFn || function () { return "#00A6D6"; };
135
+ const fontFamily = props.fontFamily || CANVAS.fontFamily;
136
+ const headerFont = Math.max(props.headerFont || 12, MIN_FONT - 2);
137
+ const labelFont = Math.max(props.labelFont || 12.5, MIN_FONT - 2);
138
+
139
+ function colX(i: number) { return x + colW * (i + 0.5); }
140
+
141
+ const headers = columns.map(function (c, i) {
142
+ return React.createElement(
143
+ "text",
144
+ {
145
+ key: "h" + i, x: colX(i), y: y - 10, textAnchor: "middle",
146
+ fontSize: headerFont, fontWeight: 800, fill: "#313131", fontFamily: fontFamily,
147
+ },
148
+ c
149
+ );
150
+ });
151
+
152
+ const body = rows.map(function (r, ri) {
153
+ const ry = y + ri * rowH;
154
+ const cy = ry + rowH / 2;
155
+ const label = React.createElement(
156
+ "text",
157
+ {
158
+ key: "l", x: x - 14, y: cy, textAnchor: "end", dominantBaseline: "middle",
159
+ fontSize: labelFont, fontWeight: 700, fill: "#313131", fontFamily: fontFamily,
160
+ },
161
+ r.label
162
+ );
163
+ const bubbles = (r.values || []).map(function (v, ci) {
164
+ const rad = v <= 0 ? 3 : 4 + v * (maxR - 4);
165
+ return React.createElement("circle", {
166
+ key: ci, cx: colX(ci), cy: cy, r: rad,
167
+ fill: colorFn(v), fillOpacity: v <= 0 ? 0.5 : 0.9,
168
+ });
169
+ });
170
+ return React.createElement("g", { key: ri }, label, bubbles);
171
+ });
172
+
173
+ return React.createElement("g", null, headers, body);
174
+ }
175
+
176
+ /**
177
+ * Large stat numbers stacked vertically.
178
+ * items: { value, label }[]
179
+ */
180
+ export function StatStack(props: StatStackProps): ReactNode {
181
+ const items = props.items || [];
182
+ const cx = props.cx || 0;
183
+ const y = props.y || 0;
184
+ const gap = props.gap || 62;
185
+ const valueFont = props.valueFont || 36;
186
+ const labelFont = Math.max(props.labelFont || 16, MIN_FONT);
187
+ const color = props.color || "#76B82A";
188
+ const fontFamily = props.fontFamily || CANVAS.fontFamily;
189
+
190
+ return items.map(function (item, i) {
191
+ const iy = y + i * gap;
192
+ return React.createElement(
193
+ "g",
194
+ { key: i },
195
+ React.createElement(
196
+ "text",
197
+ {
198
+ x: cx, y: iy, textAnchor: "middle",
199
+ fontSize: valueFont, fontWeight: 900, fill: color, fontFamily: fontFamily,
200
+ },
201
+ item.value
202
+ ),
203
+ React.createElement(
204
+ "text",
205
+ {
206
+ x: cx, y: iy + labelFont + 4, textAnchor: "middle",
207
+ fontSize: labelFont, fontWeight: 700, fill: "#555", fontFamily: fontFamily,
208
+ },
209
+ item.label
210
+ )
211
+ );
212
+ });
213
+ }
@@ -0,0 +1,172 @@
1
+ import type { Block, BlockOptions, Slide, SlideOptions, StepNote, LayerName } from "./types.ts";
2
+ import { attrsToHtml, esc, style } from "./design.ts";
3
+
4
+ const LAYER_Z: Record<LayerName, number> = {
5
+ background: 0,
6
+ connector: 1,
7
+ content: 2,
8
+ overlay: 3,
9
+ chrome: 4,
10
+ };
11
+
12
+ export function fragmentMeta(
13
+ fragment: boolean | number | null | undefined,
14
+ animation = "fade-in",
15
+ ): { classes: string[]; attrs: Record<string, number | undefined> } {
16
+ if (fragment === null || fragment === undefined || fragment === false) {
17
+ return { classes: [], attrs: {} };
18
+ }
19
+ const classes = ["fragment", animation].filter(Boolean);
20
+ const attrs = Number.isInteger(fragment) ? { "data-fragment-index": fragment as number } : {};
21
+ return { classes, attrs };
22
+ }
23
+
24
+ export function createBlock({
25
+ id,
26
+ kind = "html",
27
+ html = "",
28
+ src,
29
+ alt = "",
30
+ x,
31
+ y,
32
+ w,
33
+ h,
34
+ layer = "content",
35
+ fragment = null,
36
+ animation = "fade-in",
37
+ className = "",
38
+ attrs = {},
39
+ css = {},
40
+ styleText = "",
41
+ flow = false,
42
+ mount = null,
43
+ }: BlockOptions = {}): Block {
44
+ return {
45
+ id,
46
+ kind,
47
+ html,
48
+ src,
49
+ alt,
50
+ x,
51
+ y,
52
+ w,
53
+ h,
54
+ layer,
55
+ fragment,
56
+ animation,
57
+ className,
58
+ attrs,
59
+ css,
60
+ styleText,
61
+ flow,
62
+ mount,
63
+ };
64
+ }
65
+
66
+ export function htmlBlock(options: BlockOptions = {}): Block {
67
+ return createBlock({ kind: "html", ...options });
68
+ }
69
+
70
+ export function svgBlock(options: BlockOptions = {}): Block {
71
+ return createBlock({ kind: "svg", ...options });
72
+ }
73
+
74
+ export function imageBlock(options: BlockOptions = {}): Block {
75
+ return createBlock({ kind: "image", ...options });
76
+ }
77
+
78
+ export function mountBlock(options: BlockOptions = {}): Block {
79
+ return createBlock({ kind: "mount", ...options });
80
+ }
81
+
82
+ function blockInner(block: Block): string {
83
+ if (block.kind === "image") {
84
+ return `<img class="block-image" src="${esc(block.src ?? "")}" alt="${esc(block.alt ?? "")}">`;
85
+ }
86
+
87
+ if (block.kind === "mount") {
88
+ const mountAttrs = attrsToHtml({
89
+ "data-mount-library": block.mount?.library,
90
+ "data-mount-export": block.mount?.exportName,
91
+ "data-mount-props": block.mount?.props ? JSON.stringify(block.mount.props) : undefined,
92
+ });
93
+ return `<div class="block-mount" ${mountAttrs}>${block.html ?? ""}</div>`;
94
+ }
95
+
96
+ return block.html ?? "";
97
+ }
98
+
99
+ export function renderBlock(block: Block | string): string {
100
+ if (typeof block === "string") return block;
101
+
102
+ const fragment = fragmentMeta(block.fragment, block.animation);
103
+ const classes = [
104
+ block.flow ? "slide-flow-block" : "slide-positioned-block",
105
+ "slide-block",
106
+ `block-${block.kind}`,
107
+ `layer-${block.layer}`,
108
+ ...fragment.classes,
109
+ block.className,
110
+ ].filter(Boolean);
111
+
112
+ const positionStyle: Record<string, string | number | undefined> = block.flow
113
+ ? {}
114
+ : {
115
+ left: block.x,
116
+ top: block.y,
117
+ width: block.w,
118
+ height: block.h,
119
+ "z-index": LAYER_Z[block.layer] ?? LAYER_Z.content,
120
+ };
121
+ const styleAttr = [style({ ...positionStyle, ...block.css }), block.styleText]
122
+ .filter(Boolean)
123
+ .join(";");
124
+
125
+ const attrs = attrsToHtml({
126
+ "data-block-id": block.id,
127
+ "data-block-kind": block.kind,
128
+ "data-layer": block.layer,
129
+ ...block.attrs,
130
+ ...fragment.attrs,
131
+ });
132
+ const attrText = attrs ? ` ${attrs}` : "";
133
+ const styleHtml = styleAttr ? ` style="${esc(styleAttr)}"` : "";
134
+
135
+ return `<div class="${classes.map(esc).join(" ")}"${attrText}${styleHtml}>${blockInner(block)}</div>`;
136
+ }
137
+
138
+ export function renderBlocks(blocks: (Block | string)[] = []): string {
139
+ return blocks.flat().filter(Boolean).map(renderBlock).join("\n");
140
+ }
141
+
142
+ export function stepNote({ step, visible, say, why, next }: StepNote): string {
143
+ return [
144
+ `Step: ${step}`,
145
+ `Visible: ${visible}`,
146
+ `Say: ${say}`,
147
+ why ? `Why: ${why}` : "",
148
+ next ? `Next: ${next}` : "",
149
+ ].filter(Boolean).join("\n");
150
+ }
151
+
152
+ export function renderNotes(notes: string | (string | StepNote)[] = ""): string {
153
+ if (Array.isArray(notes)) {
154
+ return notes.map((note) => (typeof note === "string" ? note : stepNote(note))).join("\n\n");
155
+ }
156
+ return notes;
157
+ }
158
+
159
+ export function defineSlide({
160
+ className = "native-slide",
161
+ attrs = {},
162
+ html = "",
163
+ blocks = [],
164
+ notes = "",
165
+ }: SlideOptions = {}): Slide {
166
+ return {
167
+ className,
168
+ attrs,
169
+ html: [html, renderBlocks(blocks)].filter(Boolean).join("\n"),
170
+ notes: renderNotes(notes),
171
+ };
172
+ }
@@ -0,0 +1,60 @@
1
+ import Reveal from "reveal.js";
2
+ import RevealNotes from "reveal.js/plugin/notes";
3
+ import "reveal.js/reveal.css";
4
+ import * as anime from "animejs";
5
+ import "../theme.css";
6
+ import { installRuntime } from "./runtime.ts";
7
+ import type { Slide, ComponentRegistry, DeckConfig } from "./types.ts";
8
+
9
+ function injectSlides(slides: Slide[]): void {
10
+ const container = document.querySelector(".slides");
11
+ if (!container) return;
12
+ slides.forEach((slide) => {
13
+ const section = document.createElement("section");
14
+ if (slide.className) section.className = slide.className;
15
+ if (slide.attrs) {
16
+ Object.keys(slide.attrs).forEach((k) => {
17
+ const v = slide.attrs![k];
18
+ if (v !== undefined && v !== null) section.setAttribute(k, String(v));
19
+ });
20
+ }
21
+ section.innerHTML = slide.html;
22
+ if (slide.notes) {
23
+ const aside = document.createElement("aside");
24
+ aside.className = "notes";
25
+ aside.textContent = slide.notes;
26
+ section.appendChild(aside);
27
+ }
28
+ container.appendChild(section);
29
+ });
30
+ }
31
+
32
+ export async function initDeck<C extends ComponentRegistry>(config: DeckConfig<C>): Promise<void> {
33
+ const slides = typeof config.buildSlides === "function"
34
+ ? await config.buildSlides()
35
+ : config.buildSlides;
36
+ injectSlides(slides);
37
+
38
+ const deck = new (Reveal as any)({
39
+ width: 1280,
40
+ height: 720,
41
+ margin: 0.04,
42
+ minScale: 0.2,
43
+ maxScale: 2.0,
44
+ controls: true,
45
+ controlsTutorial: true,
46
+ progress: true,
47
+ slideNumber: "c/t",
48
+ overview: true,
49
+ keyboard: true,
50
+ hash: true,
51
+ fragmentInURL: true,
52
+ center: false,
53
+ transition: "none",
54
+ backgroundTransition: "none",
55
+ plugins: [RevealNotes],
56
+ });
57
+
58
+ await deck.initialize();
59
+ installRuntime(deck, anime, config.components);
60
+ }