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,336 @@
1
+ import React from "react";
2
+ import { CANVAS, useRevealStep } from "../../../core/presets.ts";
3
+ import type { SlideComponentProps } from "../../../core/types.ts";
4
+
5
+ const W = 1200, H = 560;
6
+ const CX = 600, CY = 280;
7
+
8
+ // Branch palettes.
9
+ const BLUE = { main: "#4361ee", leafFill: "#e7ecff", leafText: "#1d2a6b" };
10
+ const PURPLE = { main: "#7c3aed", leafFill: "#f0e7ff", leafText: "#3d1d6b" };
11
+ const GREEN = { main: "#059669", leafFill: "#dcf5ec", leafText: "#0b4f3a" };
12
+
13
+ // Manually laid-out organic positions.
14
+ const BRANCHES = [
15
+ {
16
+ key: "data",
17
+ palette: BLUE,
18
+ head: { label: "Data Collection", x: 260, y: 130 },
19
+ leaves: [
20
+ { label: "Traces", x: 80, y: 70 },
21
+ { label: "Logs", x: 90, y: 200 },
22
+ { label: "Metrics", x: 250, y: 40 },
23
+ ],
24
+ },
25
+ {
26
+ key: "causal",
27
+ palette: PURPLE,
28
+ head: { label: "Causal Inference", x: 940, y: 130 },
29
+ leaves: [
30
+ { label: "Graph Model", x: 1120, y: 70 },
31
+ { label: "Correlation", x: 1110, y: 210 },
32
+ ],
33
+ },
34
+ {
35
+ key: "fault",
36
+ palette: GREEN,
37
+ head: { label: "Fault Localization", x: 600, y: 480 },
38
+ leaves: [
39
+ { label: "Ranking", x: 380, y: 520 },
40
+ { label: "Validation", x: 600, y: 540 },
41
+ { label: "Reporting", x: 820, y: 520 },
42
+ ],
43
+ },
44
+ ];
45
+
46
+ // Quadratic bezier path from parent to child with a gentle bow.
47
+ function curvePath(x1: number, y1: number, x2: number, y2: number) {
48
+ const mx = (x1 + x2) / 2;
49
+ const my = (y1 + y2) / 2;
50
+ // Bow perpendicular to the line for an organic curve.
51
+ const dx = x2 - x1, dy = y2 - y1;
52
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
53
+ const nx = -dy / len, ny = dx / len;
54
+ const bow = 28;
55
+ const qx = mx + nx * bow, qy = my + ny * bow;
56
+ return "M " + x1 + " " + y1 + " Q " + qx + " " + qy + " " + x2 + " " + y2;
57
+ }
58
+
59
+ export default function DemoConceptMap({ Reveal }: SlideComponentProps) {
60
+ const rs = useRevealStep(Reveal);
61
+ const step = rs[0], ready = rs[1], instant = rs[2];
62
+
63
+ const centralRef = React.useRef<SVGGElement>(null);
64
+ const glowRef = React.useRef<SVGCircleElement>(null);
65
+ // Per-branch ref guards so each branch animates once.
66
+ const playedRef = React.useRef<Record<string, boolean>>({ central: false, data: false, causal: false, fault: false, pulse: false });
67
+
68
+ // Collect refs by branch key.
69
+ const pathRefs = React.useRef<Record<string, (SVGPathElement | null)[]>>({});
70
+ const nodeRefs = React.useRef<Record<string, (SVGGElement | null)[]>>({});
71
+
72
+ function registerPath(key: string, i: number, el: SVGPathElement | null) {
73
+ if (!pathRefs.current[key]) pathRefs.current[key] = [];
74
+ pathRefs.current[key][i] = el;
75
+ }
76
+ function registerNode(key: string, i: number, el: SVGGElement | null) {
77
+ if (!nodeRefs.current[key]) nodeRefs.current[key] = [];
78
+ nodeRefs.current[key][i] = el;
79
+ }
80
+
81
+ function showCentral(animate: boolean) {
82
+ const node = centralRef.current;
83
+ if (!node) return;
84
+ const anime = (window as any).anime;
85
+ node.style.opacity = String(1);
86
+ if (!animate || !anime || !anime.animate) {
87
+ node.style.transform = "scale(1)";
88
+ return;
89
+ }
90
+ anime.animate(node, {
91
+ scale: [0, 1],
92
+ duration: 700,
93
+ ease: "spring(1, 80, 10, 0)",
94
+ });
95
+ }
96
+
97
+ function hideCentral() {
98
+ const node = centralRef.current;
99
+ if (!node) return;
100
+ node.style.opacity = String(0);
101
+ node.style.transform = "scale(0)";
102
+ }
103
+
104
+ function showBranch(key: string, animate: boolean) {
105
+ const anime = (window as any).anime;
106
+ const paths = (pathRefs.current[key] || []).filter(Boolean) as SVGPathElement[];
107
+ const nodes = (nodeRefs.current[key] || []).filter(Boolean) as SVGGElement[];
108
+
109
+ paths.forEach(function (p) {
110
+ const len = p.getTotalLength ? p.getTotalLength() : 0;
111
+ p.style.strokeDasharray = String(len);
112
+ if (!animate || !anime || !anime.animate) {
113
+ p.style.strokeDashoffset = String(0);
114
+ p.style.opacity = String(1);
115
+ return;
116
+ }
117
+ p.style.opacity = String(1);
118
+ anime.animate(p, {
119
+ strokeDashoffset: [len, 0],
120
+ duration: 800,
121
+ ease: "inOutQuad",
122
+ });
123
+ });
124
+
125
+ nodes.forEach(function (n, i) {
126
+ n.style.opacity = String(1);
127
+ if (!animate || !anime || !anime.animate) {
128
+ n.style.transform = "scale(1)";
129
+ return;
130
+ }
131
+ anime.animate(n, {
132
+ scale: [0, 1],
133
+ duration: 600,
134
+ delay: 300 + i * 120,
135
+ ease: "spring(1, 80, 11, 0)",
136
+ });
137
+ });
138
+ }
139
+
140
+ function hideBranch(key: string) {
141
+ const paths = (pathRefs.current[key] || []).filter(Boolean) as SVGPathElement[];
142
+ const nodes = (nodeRefs.current[key] || []).filter(Boolean) as SVGGElement[];
143
+ paths.forEach(function (p) {
144
+ const len = p.getTotalLength ? p.getTotalLength() : 0;
145
+ p.style.strokeDasharray = String(len);
146
+ p.style.strokeDashoffset = String(len);
147
+ p.style.opacity = String(0);
148
+ });
149
+ nodes.forEach(function (n) {
150
+ n.style.opacity = String(0);
151
+ n.style.transform = "scale(0)";
152
+ });
153
+ }
154
+
155
+ function pulseAll() {
156
+ const anime = (window as any).anime;
157
+ if (!anime || !anime.animate) return;
158
+ const allPaths: SVGPathElement[] = [];
159
+ ["data", "causal", "fault"].forEach(function (k) {
160
+ (pathRefs.current[k] || []).filter(Boolean).forEach(function (p) { allPaths.push(p as SVGPathElement); });
161
+ });
162
+ if (allPaths.length) {
163
+ anime.animate(allPaths, {
164
+ opacity: [1, 0.4, 1],
165
+ duration: 1200,
166
+ ease: "inOutSine",
167
+ });
168
+ }
169
+ if (glowRef.current) {
170
+ anime.animate(glowRef.current, {
171
+ opacity: [0, 0.55, 0.3],
172
+ scale: [1, 1.25, 1.15],
173
+ duration: 1400,
174
+ ease: "inOutSine",
175
+ });
176
+ }
177
+ }
178
+
179
+ React.useEffect(function () {
180
+ const id = requestAnimationFrame(function () {
181
+ const played = playedRef.current;
182
+
183
+ // Central node (step 0).
184
+ if (step >= 0) {
185
+ if (instant) { showCentral(false); played.central = true; }
186
+ else if (!played.central) { played.central = true; showCentral(true); }
187
+ } else {
188
+ hideCentral();
189
+ played.central = false;
190
+ }
191
+
192
+ // Branches map to steps 1, 2, 3.
193
+ const order = [
194
+ { key: "data", req: 1 },
195
+ { key: "causal", req: 2 },
196
+ { key: "fault", req: 3 },
197
+ ];
198
+ order.forEach(function (b) {
199
+ if (step >= b.req) {
200
+ if (instant) { showBranch(b.key, false); played[b.key] = true; }
201
+ else if (!played[b.key]) { played[b.key] = true; showBranch(b.key, true); }
202
+ } else {
203
+ hideBranch(b.key);
204
+ played[b.key] = false;
205
+ }
206
+ });
207
+
208
+ // Step 4: pulse + glow.
209
+ if (step >= 4 && !instant) {
210
+ if (!played.pulse) { played.pulse = true; pulseAll(); }
211
+ } else if (step < 4) {
212
+ played.pulse = false;
213
+ if (glowRef.current) glowRef.current.style.opacity = String(0);
214
+ } else if (instant && glowRef.current) {
215
+ glowRef.current.style.opacity = String(0.3);
216
+ }
217
+ });
218
+ return function () { cancelAnimationFrame(id); };
219
+ }, [step, instant]);
220
+
221
+ return (
222
+ <svg
223
+ viewBox={"0 0 " + W + " " + H}
224
+ style={{ width: "100%", height: "100%", display: "block", fontFamily: CANVAS.fontFamily, opacity: ready ? 1 : 0, transition: "opacity 0.2s ease" }}
225
+ >
226
+ <defs>
227
+ <filter id="conceptGlow" x="-80%" y="-80%" width="260%" height="260%">
228
+ <feGaussianBlur stdDeviation="12" result="b" />
229
+ <feMerge>
230
+ <feMergeNode in="b" />
231
+ <feMergeNode in="SourceGraphic" />
232
+ </feMerge>
233
+ </filter>
234
+ </defs>
235
+
236
+ {/* Connections (drawn under nodes) */}
237
+ {BRANCHES.map(function (br) {
238
+ return (
239
+ <g key={"paths-" + br.key}>
240
+ {/* central -> head */}
241
+ <path
242
+ ref={function (el) { registerPath(br.key, 0, el); }}
243
+ d={curvePath(CX, CY, br.head.x, br.head.y)}
244
+ fill="none"
245
+ stroke={br.palette.main}
246
+ strokeWidth={4}
247
+ strokeLinecap="round"
248
+ style={{ opacity: 0 }}
249
+ />
250
+ {/* head -> leaves */}
251
+ {br.leaves.map(function (lf, i) {
252
+ return (
253
+ <path
254
+ key={i}
255
+ ref={function (el) { registerPath(br.key, i + 1, el); }}
256
+ d={curvePath(br.head.x, br.head.y, lf.x, lf.y)}
257
+ fill="none"
258
+ stroke={br.palette.main}
259
+ strokeWidth={3}
260
+ strokeLinecap="round"
261
+ style={{ opacity: 0 }}
262
+ />
263
+ );
264
+ })}
265
+ </g>
266
+ );
267
+ })}
268
+
269
+ {/* Branch nodes */}
270
+ {BRANCHES.map(function (br) {
271
+ return (
272
+ <g key={"nodes-" + br.key}>
273
+ {/* head node */}
274
+ <g
275
+ ref={function (el) { registerNode(br.key, 0, el); }}
276
+ style={{ opacity: 0, transform: "scale(0)", transformOrigin: br.head.x + "px " + br.head.y + "px", transformBox: "fill-box" }}
277
+ >
278
+ <circle cx={br.head.x} cy={br.head.y} r={30} fill="#fff" stroke={br.palette.main} strokeWidth={4} />
279
+ {twoLineLabel(br.head.label).map(function (line, li, arr) {
280
+ const dy = (li - (arr.length - 1) / 2) * 15;
281
+ return (
282
+ <text key={li} x={br.head.x} y={br.head.y + dy} textAnchor="middle" dominantBaseline="central" fontSize={14} fontWeight={800} fill={br.palette.main}>
283
+ {line}
284
+ </text>
285
+ );
286
+ })}
287
+ </g>
288
+ {/* leaf nodes */}
289
+ {br.leaves.map(function (lf, i) {
290
+ return (
291
+ <g
292
+ key={i}
293
+ ref={function (el) { registerNode(br.key, i + 1, el); }}
294
+ style={{ opacity: 0, transform: "scale(0)", transformOrigin: lf.x + "px " + lf.y + "px", transformBox: "fill-box" }}
295
+ >
296
+ <circle cx={lf.x} cy={lf.y} r={22} fill={br.palette.leafFill} stroke={br.palette.main} strokeWidth={2} />
297
+ <text x={lf.x} y={lf.y} textAnchor="middle" dominantBaseline="central" fontSize={14} fontWeight={700} fill={br.palette.leafText}>
298
+ {lf.label}
299
+ </text>
300
+ </g>
301
+ );
302
+ })}
303
+ </g>
304
+ );
305
+ })}
306
+
307
+ {/* Central node glow (behind central) */}
308
+ <circle
309
+ ref={glowRef}
310
+ cx={CX}
311
+ cy={CY}
312
+ r={45}
313
+ fill="#4361ee"
314
+ filter="url(#conceptGlow)"
315
+ style={{ opacity: 0, transformOrigin: CX + "px " + CY + "px", transformBox: "fill-box" }}
316
+ />
317
+
318
+ {/* Central node */}
319
+ <g
320
+ ref={centralRef}
321
+ style={{ opacity: 0, transform: "scale(0)", transformOrigin: CX + "px " + CY + "px", transformBox: "fill-box" }}
322
+ >
323
+ <circle cx={CX} cy={CY} r={45} fill="#4361ee" />
324
+ <text x={CX} y={CY - 8} textAnchor="middle" dominantBaseline="central" fontSize={15} fontWeight={900} fill="#fff">Root Cause</text>
325
+ <text x={CX} y={CY + 12} textAnchor="middle" dominantBaseline="central" fontSize={15} fontWeight={900} fill="#fff">Analysis</text>
326
+ </g>
327
+ </svg>
328
+ );
329
+ }
330
+
331
+ // Split a head label into at most two lines on whitespace for circle fit.
332
+ function twoLineLabel(label: string) {
333
+ const parts = label.split(" ");
334
+ if (parts.length < 2) return [label];
335
+ return [parts[0], parts.slice(1).join(" ")];
336
+ }
@@ -0,0 +1,194 @@
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
+ { label: "Users", target: 1247, suffix: "", decimals: 0, color: "#4361ee", spark: [4, 8, 6, 12, 9, 16, 14, 22] },
7
+ { label: "Uptime", target: 99.9, suffix: "%", decimals: 1, color: "#3a0ca3", spark: [18, 17, 19, 18, 20, 19, 21, 22] },
8
+ { label: "Latency", target: 42, suffix: "ms", decimals: 0, color: "#f72585", spark: [20, 16, 18, 12, 14, 9, 11, 7] },
9
+ { label: "Requests", target: 3.2, suffix: "M", decimals: 1, color: "#4cc9f0", spark: [5, 9, 7, 13, 11, 17, 15, 21] },
10
+ ];
11
+
12
+ function formatVal(v: number, decimals: number) {
13
+ const fixed = v.toFixed(decimals);
14
+ // Add thousands separators for integer-style counts.
15
+ const parts = fixed.split(".");
16
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
17
+ return parts.join(".");
18
+ }
19
+
20
+ function sparkPath(points: number[], w: number, h: number) {
21
+ const max = Math.max.apply(null, points);
22
+ const min = Math.min.apply(null, points);
23
+ const range = max - min || 1;
24
+ const step = w / (points.length - 1);
25
+ return points.map(function (p, i) {
26
+ const x = i * step;
27
+ const y = h - ((p - min) / range) * h;
28
+ return (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
29
+ }).join(" ");
30
+ }
31
+
32
+ export default function DemoCounter({ Reveal }: SlideComponentProps) {
33
+ const ref = useRevealStep(Reveal);
34
+ const step = ref[0], ready = ref[1], instant = ref[2];
35
+
36
+ const valuesState = React.useState(CARDS.map(function () { return 0; }));
37
+ const values = valuesState[0], setValues = valuesState[1];
38
+
39
+ const barRefs = React.useRef<(HTMLDivElement | null)[]>([]);
40
+ const sparkRefs = React.useRef<(SVGPathElement | null)[]>([]);
41
+
42
+ // Step 1: count numbers from 0 to target.
43
+ React.useEffect(function () {
44
+ const anime = (window as any).anime;
45
+ const visible = step >= 1;
46
+ if (!visible) {
47
+ setValues(CARDS.map(function () { return 0; }));
48
+ return;
49
+ }
50
+ if (!anime || instant) {
51
+ setValues(CARDS.map(function (c) { return c.target; }));
52
+ return;
53
+ }
54
+ const counters = CARDS.map(function () { return { v: 0 }; });
55
+ const anims = counters.map(function (counter, i) {
56
+ return anime.animate(counter, {
57
+ v: CARDS[i].target,
58
+ duration: 1600,
59
+ easing: "outCubic",
60
+ onUpdate: function () {
61
+ setValues(function (prev) {
62
+ const next = prev.slice();
63
+ next[i] = counter.v;
64
+ return next;
65
+ });
66
+ },
67
+ });
68
+ });
69
+ return function () { anims.forEach(function (a) { a.pause && a.pause(); }); };
70
+ }, [step, instant]);
71
+
72
+ // Step 2: progress bars fill.
73
+ React.useEffect(function () {
74
+ const anime = (window as any).anime;
75
+ const visible = step >= 2;
76
+ const fills = barRefs.current.filter(Boolean) as HTMLDivElement[];
77
+ if (!fills.length) return;
78
+ if (!visible) {
79
+ fills.forEach(function (el) { el.style.width = "0%"; });
80
+ return;
81
+ }
82
+ const targets = [78, 99, 58, 84];
83
+ if (!anime || instant) {
84
+ fills.forEach(function (el, i) { el.style.width = targets[i] + "%"; });
85
+ return;
86
+ }
87
+ anime.animate(fills, {
88
+ width: function (el: any, i: number) { return [0 + "%", targets[i] + "%"]; },
89
+ duration: 1100,
90
+ easing: "inOutQuad",
91
+ delay: anime.stagger(120),
92
+ });
93
+ }, [step, instant]);
94
+
95
+ // Step 3: sparklines draw via strokeDashoffset.
96
+ React.useEffect(function () {
97
+ const anime = (window as any).anime;
98
+ const visible = step >= 3;
99
+ const paths = sparkRefs.current.filter(Boolean) as SVGPathElement[];
100
+ if (!paths.length) return;
101
+ paths.forEach(function (el) {
102
+ const len = el.getTotalLength();
103
+ el.style.strokeDasharray = String(len);
104
+ el.style.strokeDashoffset = String(visible && (!anime || instant) ? 0 : len);
105
+ });
106
+ if (!visible || !anime || instant) return;
107
+ anime.animate(paths, {
108
+ strokeDashoffset: [function (el: SVGPathElement) { return el.getTotalLength(); }, 0],
109
+ duration: 1200,
110
+ easing: "outCubic",
111
+ delay: anime.stagger(150),
112
+ });
113
+ }, [step, instant]);
114
+
115
+ return (
116
+ <div
117
+ style={{
118
+ width: "100%",
119
+ height: "100%",
120
+ display: "flex",
121
+ flexDirection: "column",
122
+ justifyContent: "center",
123
+ gap: 28,
124
+ padding: "20px 40px",
125
+ boxSizing: "border-box",
126
+ fontFamily: CANVAS.fontFamily,
127
+ opacity: ready ? 1 : 0,
128
+ transition: "opacity 0.2s ease",
129
+ }}
130
+ >
131
+ <div style={{ display: "flex", gap: 24, justifyContent: "center" }}>
132
+ {CARDS.map(function (card, i) {
133
+ return (
134
+ <div
135
+ key={i}
136
+ style={{
137
+ flex: "1 1 0",
138
+ maxWidth: 250,
139
+ background: "#fff",
140
+ borderRadius: 14,
141
+ borderTop: "5px solid " + card.color,
142
+ boxShadow: "0 8px 24px rgba(49,49,49,0.12)",
143
+ padding: "24px 22px",
144
+ boxSizing: "border-box",
145
+ opacity: step >= 0 ? 1 : 0,
146
+ transform: step >= 0 ? "translateY(0)" : "translateY(20px)",
147
+ transition: instant ? "none" : "opacity 0.45s ease " + (i * 60) + "ms, transform 0.45s ease " + (i * 60) + "ms",
148
+ }}
149
+ >
150
+ <div style={{ fontSize: 40, fontWeight: 900, color: card.color, lineHeight: 1 }}>
151
+ {formatVal(values[i], card.decimals) + card.suffix}
152
+ </div>
153
+ <div style={{ fontSize: 16, fontWeight: 700, color: "#767676", marginTop: 8 }}>
154
+ {card.label}
155
+ </div>
156
+
157
+ <div
158
+ style={{
159
+ marginTop: 16,
160
+ height: 8,
161
+ borderRadius: 4,
162
+ background: "#eee",
163
+ overflow: "hidden",
164
+ }}
165
+ >
166
+ <div
167
+ ref={function (el) { barRefs.current[i] = el; }}
168
+ style={{ height: "100%", width: "0%", background: card.color, borderRadius: 4 }}
169
+ />
170
+ </div>
171
+
172
+ <svg
173
+ viewBox="0 0 200 50"
174
+ preserveAspectRatio="none"
175
+ style={{ width: "100%", height: 42, marginTop: 14, display: "block" }}
176
+ >
177
+ <path
178
+ ref={function (el) { sparkRefs.current[i] = el; }}
179
+ d={sparkPath(card.spark, 200, 44)}
180
+ fill="none"
181
+ stroke={card.color}
182
+ strokeWidth={3}
183
+ strokeLinecap="round"
184
+ strokeLinejoin="round"
185
+ transform="translate(0,3)"
186
+ />
187
+ </svg>
188
+ </div>
189
+ );
190
+ })}
191
+ </div>
192
+ </div>
193
+ );
194
+ }