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,159 @@
1
+ import React from "react";
2
+ import type { SlideComponentProps } from "../../../core/types.ts";
3
+ import { CANVAS, useRevealStep } from "../../../core/presets.ts";
4
+
5
+ const CARDS = [
6
+ { letter: "O", title: "Observation", color: "#4361ee", desc: "Gather signals from traces, logs, and metrics across the system." },
7
+ { letter: "H", title: "Hypothesis", color: "#7c3aed", desc: "Propose a candidate root cause that explains the anomaly." },
8
+ { letter: "E", title: "Experiment", color: "#f72585", desc: "Probe the suspect path with targeted tests and replays." },
9
+ { letter: "A", title: "Analysis", color: "#4cc9f0", desc: "Compare results against expected behavior to confirm or reject." },
10
+ { letter: "C", title: "Conclusion", color: "#059669", desc: "Pinpoint the fault and report the verified root cause." },
11
+ ];
12
+
13
+ // Fan-out target layout: spread across ~900px, slight upward arc.
14
+ const FAN = [
15
+ { x: -380, y: 24, rot: -8 },
16
+ { x: -190, y: 6, rot: -4 },
17
+ { x: 0, y: 0, rot: 0 },
18
+ { x: 190, y: 6, rot: 4 },
19
+ { x: 380, y: 24, rot: 8 },
20
+ ];
21
+
22
+ // Initial stacked layout: piled in center with slight offsets.
23
+ function stackPose(i: number) {
24
+ return { x: 0, y: i * 3, rot: (i - 2) * 2 };
25
+ }
26
+
27
+ export default function DemoStackedCards({ Reveal }: SlideComponentProps) {
28
+ const rs = useRevealStep(Reveal);
29
+ const step = rs[0], ready = rs[1], instant = rs[2];
30
+
31
+ const cardRefs = React.useRef<(HTMLDivElement | null)[]>([]);
32
+ // Track which cards have already fanned out so each animation plays once.
33
+ const fannedRef = React.useRef([false, false, false, false, false]);
34
+
35
+ function applyPose(node: HTMLDivElement | null, pose: { x: number; y: number; rot: number }, animate: boolean) {
36
+ if (!node) return;
37
+ const anime = (window as any).anime;
38
+ const transform =
39
+ "translate(-50%, -50%) translateX(" + pose.x + "px) translateY(" + pose.y + "px) rotate(" + pose.rot + "deg)";
40
+ if (!animate || !anime || !anime.animate) {
41
+ node.style.transform = transform;
42
+ return;
43
+ }
44
+ anime.animate(node, {
45
+ translateX: pose.x + "px",
46
+ translateY: pose.y + "px",
47
+ rotate: pose.rot + "deg",
48
+ duration: 900,
49
+ ease: "spring(1, 80, 12, 0)",
50
+ });
51
+ }
52
+
53
+ React.useEffect(function () {
54
+ const id = requestAnimationFrame(function () {
55
+ CARDS.forEach(function (_, i) {
56
+ const node = cardRefs.current[i];
57
+ if (!node) return;
58
+ const fannedOut = step >= i;
59
+
60
+ if (instant) {
61
+ // Reset path: snap to correct pose, no animation.
62
+ applyPose(node, fannedOut ? FAN[i] : stackPose(i), false);
63
+ fannedRef.current[i] = fannedOut;
64
+ return;
65
+ }
66
+
67
+ if (fannedOut && !fannedRef.current[i]) {
68
+ fannedRef.current[i] = true;
69
+ applyPose(node, FAN[i], true);
70
+ } else if (!fannedOut && fannedRef.current[i]) {
71
+ fannedRef.current[i] = false;
72
+ applyPose(node, stackPose(i), true);
73
+ }
74
+ });
75
+ });
76
+ return function () { cancelAnimationFrame(id); };
77
+ }, [step, instant]);
78
+
79
+ return (
80
+ <div
81
+ style={{
82
+ position: "relative",
83
+ width: "100%",
84
+ height: "100%",
85
+ fontFamily: CANVAS.fontFamily,
86
+ opacity: ready ? 1 : 0,
87
+ transition: "opacity 0.2s ease",
88
+ }}
89
+ >
90
+ {CARDS.map(function (card, i: number) {
91
+ const init = stackPose(i);
92
+ return (
93
+ <div
94
+ key={i}
95
+ ref={function (el) { cardRefs.current[i] = el; }}
96
+ style={{
97
+ position: "absolute",
98
+ left: "50%",
99
+ top: "50%",
100
+ width: 200,
101
+ height: 280,
102
+ marginLeft: 0,
103
+ marginTop: 0,
104
+ transform:
105
+ "translate(-50%, -50%) translateX(" + init.x + "px) translateY(" + init.y + "px) rotate(" + init.rot + "deg)",
106
+ transformOrigin: "center center",
107
+ background: "#fff",
108
+ borderRadius: 16,
109
+ boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
110
+ overflow: "hidden",
111
+ zIndex: i,
112
+ boxSizing: "border-box",
113
+ }}
114
+ >
115
+ {/* Colored top band */}
116
+ <div style={{ height: 40, background: card.color }} />
117
+
118
+ <div
119
+ style={{
120
+ padding: 18,
121
+ display: "flex",
122
+ flexDirection: "column",
123
+ alignItems: "flex-start",
124
+ gap: 14,
125
+ boxSizing: "border-box",
126
+ }}
127
+ >
128
+ {/* Icon: colored circle with letter */}
129
+ <div
130
+ style={{
131
+ width: 60,
132
+ height: 60,
133
+ borderRadius: "50%",
134
+ background: card.color,
135
+ display: "flex",
136
+ alignItems: "center",
137
+ justifyContent: "center",
138
+ color: "#fff",
139
+ fontSize: 28,
140
+ fontWeight: 900,
141
+ }}
142
+ >
143
+ {card.letter}
144
+ </div>
145
+
146
+ <div style={{ fontSize: 20, fontWeight: 800, color: "#1d1d1d" }}>
147
+ {card.title}
148
+ </div>
149
+
150
+ <div style={{ fontSize: 14, color: "#767676", lineHeight: 1.45 }}>
151
+ {card.desc}
152
+ </div>
153
+ </div>
154
+ </div>
155
+ );
156
+ })}
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,206 @@
1
+ import React from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import type { SlideComponentProps } from "../../../core/types.ts";
4
+ import { CANVAS, useRevealStep } from "../../../core/presets.ts";
5
+
6
+ const TABS = ["Overview", "Architecture", "Performance", "Security"];
7
+ const ACTIVE = "#4361ee";
8
+
9
+ function OverviewPanel() {
10
+ const items = [
11
+ "A unified platform for building and shipping fast.",
12
+ "Modular components that compose without friction.",
13
+ "Observability and metrics baked in from day one.",
14
+ "Designed for teams that move quickly.",
15
+ ];
16
+ return (
17
+ <ul style={{ margin: 0, paddingLeft: 24, display: "flex", flexDirection: "column", gap: 16 }}>
18
+ {items.map(function (t, i: number) {
19
+ return <li key={i} style={{ fontSize: 20, color: "#333", lineHeight: 1.5 }}>{t}</li>;
20
+ })}
21
+ </ul>
22
+ );
23
+ }
24
+
25
+ function ArchitecturePanel() {
26
+ const nodes = ["Client", "Gateway", "Services", "Data"];
27
+ return (
28
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 18, height: "100%" }}>
29
+ {nodes.map(function (n, i: number) {
30
+ return (
31
+ <React.Fragment key={n}>
32
+ <div
33
+ style={{
34
+ padding: "22px 28px",
35
+ borderRadius: 12,
36
+ background: "#eef1ff",
37
+ border: "2px solid " + ACTIVE,
38
+ fontSize: 20,
39
+ fontWeight: 800,
40
+ color: "#1d3557",
41
+ }}
42
+ >
43
+ {n}
44
+ </div>
45
+ {i < nodes.length - 1 ? (
46
+ <div style={{ fontSize: 28, color: ACTIVE, fontWeight: 900 }}>→</div>
47
+ ) : null}
48
+ </React.Fragment>
49
+ );
50
+ })}
51
+ </div>
52
+ );
53
+ }
54
+
55
+ function PerformancePanel() {
56
+ const bars = [
57
+ { label: "p50", value: 92, color: "#4cc9f0" },
58
+ { label: "p90", value: 74, color: "#4361ee" },
59
+ { label: "p99", value: 58, color: "#7209b7" },
60
+ { label: "max", value: 40, color: "#f72585" },
61
+ ];
62
+ return (
63
+ <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "center", gap: 40, height: "100%", paddingBottom: 10 }}>
64
+ {bars.map(function (b) {
65
+ return (
66
+ <div key={b.label} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10 }}>
67
+ <motion.div
68
+ initial={{ height: 0 }}
69
+ animate={{ height: b.value * 2.4 }}
70
+ transition={{ duration: 0.6, ease: "easeOut" }}
71
+ style={{ width: 56, background: b.color, borderRadius: "6px 6px 0 0" }}
72
+ />
73
+ <div style={{ fontSize: 16, fontWeight: 700, color: "#767676" }}>{b.label}</div>
74
+ </div>
75
+ );
76
+ })}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ function SecurityPanel() {
82
+ const checks = [
83
+ "TLS 1.3 enforced on all endpoints",
84
+ "Secrets rotated automatically every 24h",
85
+ "Role-based access control on every resource",
86
+ "Audit log streamed to immutable storage",
87
+ ];
88
+ return (
89
+ <div style={{ display: "flex", flexDirection: "column", gap: 18, justifyContent: "center", height: "100%" }}>
90
+ {checks.map(function (c, i: number) {
91
+ return (
92
+ <div key={i} style={{ display: "flex", alignItems: "center", gap: 14 }}>
93
+ <div
94
+ style={{
95
+ width: 30,
96
+ height: 30,
97
+ borderRadius: "50%",
98
+ background: "#2a9d8f",
99
+ color: "#fff",
100
+ display: "flex",
101
+ alignItems: "center",
102
+ justifyContent: "center",
103
+ fontWeight: 900,
104
+ fontSize: 18,
105
+ flex: "0 0 auto",
106
+ }}
107
+ >
108
+
109
+ </div>
110
+ <div style={{ fontSize: 20, color: "#333" }}>{c}</div>
111
+ </div>
112
+ );
113
+ })}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ const PANELS = [OverviewPanel, ArchitecturePanel, PerformancePanel, SecurityPanel];
119
+
120
+ export default function DemoTabs({ Reveal }: SlideComponentProps) {
121
+ const ref = useRevealStep(Reveal);
122
+ const step = ref[0], ready = ref[1], instant = ref[2];
123
+
124
+ // Step drives the active tab (clamped to range).
125
+ const active = Math.max(0, Math.min(step, TABS.length - 1));
126
+ const Panel = PANELS[active];
127
+
128
+ return (
129
+ <div
130
+ style={{
131
+ width: "100%",
132
+ height: "100%",
133
+ display: "flex",
134
+ flexDirection: "column",
135
+ alignItems: "center",
136
+ justifyContent: "center",
137
+ gap: 24,
138
+ fontFamily: CANVAS.fontFamily,
139
+ opacity: ready ? 1 : 0,
140
+ transition: "opacity 0.2s ease",
141
+ }}
142
+ >
143
+ <div style={{ display: "flex", gap: 8, width: 1100 }}>
144
+ {TABS.map(function (tab, i: number) {
145
+ const isActive = i === active;
146
+ return (
147
+ <div
148
+ key={tab}
149
+ style={{
150
+ position: "relative",
151
+ padding: "14px 26px",
152
+ fontSize: 20,
153
+ fontWeight: isActive ? 800 : 600,
154
+ color: isActive ? ACTIVE : "#888",
155
+ cursor: "default",
156
+ }}
157
+ >
158
+ {tab}
159
+ {isActive ? (
160
+ <motion.div
161
+ layoutId="tab-underline"
162
+ transition={instant ? { duration: 0 } : { type: "spring", stiffness: 500, damping: 32 }}
163
+ style={{
164
+ position: "absolute",
165
+ left: 12,
166
+ right: 12,
167
+ bottom: 0,
168
+ height: 4,
169
+ borderRadius: 2,
170
+ background: ACTIVE,
171
+ }}
172
+ />
173
+ ) : null}
174
+ </div>
175
+ );
176
+ })}
177
+ </div>
178
+
179
+ <div
180
+ style={{
181
+ width: 1100,
182
+ height: 400,
183
+ background: "#fff",
184
+ borderRadius: 16,
185
+ boxShadow: "0 12px 36px rgba(49,49,49,0.14)",
186
+ padding: 40,
187
+ boxSizing: "border-box",
188
+ overflow: "hidden",
189
+ }}
190
+ >
191
+ <AnimatePresence mode="wait" initial={false}>
192
+ <motion.div
193
+ key={active}
194
+ initial={instant ? false : { opacity: 0, x: 24 }}
195
+ animate={{ opacity: 1, x: 0 }}
196
+ exit={instant ? { opacity: 0 } : { opacity: 0, x: -24 }}
197
+ transition={instant ? { duration: 0 } : { duration: 0.3, ease: "easeOut" }}
198
+ style={{ width: "100%", height: "100%" }}
199
+ >
200
+ <Panel />
201
+ </motion.div>
202
+ </AnimatePresence>
203
+ </div>
204
+ </div>
205
+ );
206
+ }
@@ -0,0 +1,162 @@
1
+ import React from "react";
2
+ import type { SlideComponentProps } from "../../../core/types.ts";
3
+ import { CANVAS, SlideCanvas, useRevealStep } from "../../../core/presets.ts";
4
+
5
+ const W = 1200, H = 560;
6
+
7
+ const EVENTS = [
8
+ { label: "Design", desc: "Shape the idea", color: "#4361ee" },
9
+ { label: "Prototype", desc: "Build fast", color: "#3a0ca3" },
10
+ { label: "Develop", desc: "Ship features", color: "#7209b7" },
11
+ { label: "Test", desc: "Verify quality", color: "#f72585" },
12
+ { label: "Deploy", desc: "Go live", color: "#4cc9f0" },
13
+ { label: "Monitor", desc: "Watch metrics", color: "#4895ef" },
14
+ ];
15
+
16
+ const LINE_Y = H / 2;
17
+ const MARGIN_X = 120;
18
+ const SPAN = W - MARGIN_X * 2;
19
+
20
+ function nodeX(i: number) {
21
+ return MARGIN_X + (SPAN / (EVENTS.length - 1)) * i;
22
+ }
23
+
24
+ export default function DemoTimeline({ Reveal }: SlideComponentProps) {
25
+ // We need raw step access to drive imperative anime calls.
26
+ const ref = useRevealStep(Reveal);
27
+ const step = ref[0], instant = ref[2];
28
+
29
+ const lineRef = React.useRef<SVGLineElement>(null);
30
+ const lineDrawnRef = React.useRef(false);
31
+ const nodeRefs = React.useRef<(SVGGElement | null)[]>([]);
32
+
33
+ // Step 0: draw the connecting line (once only).
34
+ React.useEffect(function () {
35
+ const anime = (window as any).anime;
36
+ const el = lineRef.current;
37
+ if (!el) return;
38
+ const len = (el as any).getTotalLength();
39
+ el.style.strokeDasharray = String(len);
40
+ if (step < 0) {
41
+ lineDrawnRef.current = false;
42
+ el.style.strokeDashoffset = String(len);
43
+ return;
44
+ }
45
+ if (lineDrawnRef.current) return;
46
+ lineDrawnRef.current = true;
47
+ if (!anime || instant) {
48
+ el.style.strokeDashoffset = String(0);
49
+ return;
50
+ }
51
+ anime.animate(el, {
52
+ strokeDashoffset: [len, 0],
53
+ duration: 1100,
54
+ easing: "inOutQuad",
55
+ });
56
+ }, [step, instant]);
57
+
58
+ // Steps 1..6: light up nodes sequentially.
59
+ React.useEffect(function () {
60
+ const anime = (window as any).anime;
61
+ nodeRefs.current.forEach(function (g, i: number) {
62
+ if (!g) return;
63
+ const active = step >= i + 1;
64
+ if (!anime || instant) {
65
+ g.style.opacity = active ? "1" : "0";
66
+ g.style.transform = "none";
67
+ return;
68
+ }
69
+ if (active && g.dataset.shown !== "1") {
70
+ g.dataset.shown = "1";
71
+ anime.animate(g, {
72
+ opacity: [0, 1],
73
+ scale: [0.4, 1],
74
+ easing: "spring(1, 90, 12, 0)",
75
+ duration: 900,
76
+ });
77
+ } else if (!active) {
78
+ g.dataset.shown = "0";
79
+ g.style.opacity = "0";
80
+ }
81
+ });
82
+ }, [step, instant]);
83
+
84
+ return (
85
+ <SlideCanvas Reveal={Reveal} W={W} H={H}>{function (s: number, inst: boolean) { return (
86
+ <>
87
+ <line
88
+ ref={lineRef}
89
+ x1={nodeX(0)}
90
+ y1={LINE_Y}
91
+ x2={nodeX(EVENTS.length - 1)}
92
+ y2={LINE_Y}
93
+ stroke="#c9c9e8"
94
+ strokeWidth={4}
95
+ strokeLinecap="round"
96
+ />
97
+
98
+ {EVENTS.map(function (ev, i: number) {
99
+ const cx = nodeX(i);
100
+ return (
101
+ <g
102
+ key={i}
103
+ ref={function (el) { nodeRefs.current[i] = el; }}
104
+ style={{ opacity: 0, transformOrigin: cx + "px " + LINE_Y + "px" }}
105
+ >
106
+ <circle cx={cx} cy={LINE_Y} r={20} fill={ev.color} />
107
+ <circle cx={cx} cy={LINE_Y} r={20} fill="none" stroke="#fff" strokeWidth={3} />
108
+ <text
109
+ x={cx}
110
+ y={LINE_Y + 6}
111
+ textAnchor="middle"
112
+ fontSize={18}
113
+ fontWeight={900}
114
+ fill="#fff"
115
+ fontFamily={CANVAS.fontFamily}
116
+ >
117
+ {i + 1}
118
+ </text>
119
+
120
+ <text
121
+ x={cx}
122
+ y={LINE_Y - 44}
123
+ textAnchor="middle"
124
+ fontSize={20}
125
+ fontWeight={900}
126
+ fill="#313131"
127
+ fontFamily={CANVAS.fontFamily}
128
+ >
129
+ {ev.label}
130
+ </text>
131
+
132
+ <g transform={"translate(" + cx + "," + (LINE_Y + 46) + ")"}>
133
+ <rect
134
+ x={-58}
135
+ y={0}
136
+ width={116}
137
+ height={30}
138
+ rx={15}
139
+ fill={ev.color}
140
+ fillOpacity={0.12}
141
+ stroke={ev.color}
142
+ strokeWidth={1.5}
143
+ />
144
+ <text
145
+ x={0}
146
+ y={20}
147
+ textAnchor="middle"
148
+ fontSize={14}
149
+ fontWeight={700}
150
+ fill={ev.color}
151
+ fontFamily={CANVAS.fontFamily}
152
+ >
153
+ {ev.desc}
154
+ </text>
155
+ </g>
156
+ </g>
157
+ );
158
+ })}
159
+ </>
160
+ ); }}</SlideCanvas>
161
+ );
162
+ }
@@ -0,0 +1,161 @@
1
+ import React from "react";
2
+ import { treemap, hierarchy, treemapSquarify } from "d3-hierarchy";
3
+ import type { SlideComponentProps } from "../../../core/types.ts";
4
+ import { CANVAS, useRevealStep, vis } from "../../../core/presets.ts";
5
+
6
+ const W = 1200;
7
+ const H = 560;
8
+ const PAD_TOP = 8;
9
+
10
+ const CATEGORY_COLOR: Record<string, string> = {
11
+ Compute: "#4361ee",
12
+ Storage: "#f72585",
13
+ Network: "#4cc9f0",
14
+ Database: "#3a0ca3",
15
+ };
16
+
17
+ const DATA: any = {
18
+ name: "Cloud Costs",
19
+ children: [
20
+ { name: "Compute", children: [
21
+ { name: "EC2", value: 45 },
22
+ { name: "Lambda", value: 20 },
23
+ { name: "ECS", value: 15 },
24
+ ] },
25
+ { name: "Storage", children: [
26
+ { name: "S3", value: 25 },
27
+ { name: "EBS", value: 18 },
28
+ { name: "Glacier", value: 5 },
29
+ ] },
30
+ { name: "Network", children: [
31
+ { name: "CloudFront", value: 22 },
32
+ { name: "VPC", value: 12 },
33
+ { name: "Route53", value: 3 },
34
+ ] },
35
+ { name: "Database", children: [
36
+ { name: "RDS", value: 35 },
37
+ { name: "DynamoDB", value: 28 },
38
+ { name: "ElastiCache", value: 10 },
39
+ ] },
40
+ ],
41
+ };
42
+
43
+ function buildLayout() {
44
+ const root = hierarchy(DATA).sum(function (d: any) { return d.value || 0; })
45
+ .sort(function (a: any, b: any) { return b.value - a.value; });
46
+ treemap()
47
+ .tile(treemapSquarify)
48
+ .size([W, H - PAD_TOP])
49
+ .paddingOuter(6)
50
+ .paddingTop(22)
51
+ .paddingInner(3)
52
+ .round(true)(root);
53
+ return root;
54
+ }
55
+
56
+ export default function DemoTreemap({ Reveal }: SlideComponentProps) {
57
+ const ref = useRevealStep(Reveal);
58
+ const step = ref[0], ready = ref[1], instant = ref[2];
59
+
60
+ const rootData = React.useMemo(buildLayout, []);
61
+ const leaves = rootData.leaves();
62
+ const groups = rootData.children || [];
63
+
64
+ // Top-3 leaves by value for the highlight step.
65
+ const topNames = React.useMemo(function () {
66
+ return leaves.slice().sort(function (a: any, b: any) { return b.value - a.value; })
67
+ .slice(0, 3).map(function (d: any) { return d.data.name; });
68
+ }, [leaves]);
69
+
70
+ const rectRefs = React.useRef<(SVGRectElement | null)[]>([]);
71
+
72
+ // Step 0: stagger zoom-in of all rects.
73
+ React.useEffect(function () {
74
+ const rects = rectRefs.current.filter(Boolean) as SVGRectElement[];
75
+ if (!rects.length) return;
76
+ const anime = (window as any).anime;
77
+ if (step < 0) {
78
+ rects.forEach(function (el) { el.style.opacity = "0"; el.style.transform = "scale(0)"; });
79
+ return;
80
+ }
81
+ if (!anime || instant) {
82
+ rects.forEach(function (el) { el.style.opacity = "1"; el.style.transform = "scale(1)"; });
83
+ return;
84
+ }
85
+ rects.forEach(function (el) { el.style.opacity = "0"; el.style.transform = "scale(0)"; });
86
+ anime.animate(rects, {
87
+ opacity: [0, 1],
88
+ scale: [0, 1],
89
+ duration: 520,
90
+ easing: "outBack",
91
+ delay: anime.stagger(45),
92
+ });
93
+ }, [step >= 0, instant]);
94
+
95
+ return (
96
+ <svg viewBox={"0 0 " + W + " " + H}
97
+ style={{ width: "100%", height: "100%", display: "block",
98
+ opacity: ready ? 1 : 0, transition: "opacity 0.2s ease" }}>
99
+ <g>
100
+ {/* Category background headers */}
101
+ {groups.map(function (g: any, gi: number) {
102
+ const cat = g.data.name;
103
+ const w = g.x1 - g.x0, h = g.y1 - g.y0;
104
+ return (
105
+ <g key={"grp" + gi} style={vis(step, 0, instant)} transform={"translate(" + g.x0 + "," + (g.y0 + PAD_TOP) + ")"}>
106
+ <rect x={0} y={0} width={w} height={h} rx={6}
107
+ fill={CATEGORY_COLOR[cat]} opacity={0.10} />
108
+ <text x={8} y={15} fontFamily={CANVAS.fontFamily}
109
+ fontSize={14} fontWeight={800} fill={CATEGORY_COLOR[cat]}>
110
+ {cat}
111
+ </text>
112
+ </g>
113
+ );
114
+ })}
115
+
116
+ {/* Leaf cells */}
117
+ {leaves.map(function (leaf: any, i: number) {
118
+ const cat = leaf.parent.data.name;
119
+ const base = CATEGORY_COLOR[cat];
120
+ const isTop = topNames.indexOf(leaf.data.name) !== -1;
121
+ const fill = base;
122
+ let fillOpacity;
123
+ if (step >= 2) {
124
+ fillOpacity = isTop ? 1 : 0.4;
125
+ } else {
126
+ fillOpacity = 0.85;
127
+ }
128
+ const w = leaf.x1 - leaf.x0, h = leaf.y1 - leaf.y0;
129
+ const cx = w / 2, cy = h / 2;
130
+ const labelFits = w > 56 && h > 34;
131
+ return (
132
+ <g key={"leaf" + i}
133
+ transform={"translate(" + leaf.x0 + "," + (leaf.y0 + PAD_TOP) + ")"}>
134
+ <rect
135
+ ref={function (el) { rectRefs.current[i] = el; }}
136
+ x={0} y={0} width={w} height={h} rx={4}
137
+ fill={fill} fillOpacity={fillOpacity}
138
+ style={{
139
+ transformOrigin: (cx) + "px " + (cy) + "px",
140
+ transition: instant ? "none" : "fill-opacity 0.45s ease",
141
+ }}
142
+ />
143
+ <g style={vis(step, 1, instant)} pointerEvents="none">
144
+ {labelFits ? (
145
+ <>
146
+ <text x={cx} y={cy - 4} textAnchor="middle"
147
+ fontFamily={CANVAS.fontFamily} fontSize={15} fontWeight={800}
148
+ fill="#ffffff">{leaf.data.name}</text>
149
+ <text x={cx} y={cy + 15} textAnchor="middle"
150
+ fontFamily={CANVAS.fontFamily} fontSize={13} fontWeight={700}
151
+ fill="#ffffff" opacity={0.92}>{"$" + leaf.data.value}</text>
152
+ </>
153
+ ) : null}
154
+ </g>
155
+ </g>
156
+ );
157
+ })}
158
+ </g>
159
+ </svg>
160
+ );
161
+ }