@reslide-dev/core 0.2.0 → 0.2.2

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.
@@ -0,0 +1,1779 @@
1
+ import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
2
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/ClickNavigation.tsx
4
+ /**
5
+ * Invisible click zones on the left/right edges of the slide.
6
+ * Clicking the left ~15% goes to the previous slide,
7
+ * clicking the right ~15% goes to the next slide.
8
+ * Shows a subtle arrow indicator on hover.
9
+ */
10
+ function ClickNavigation({ onPrev, onNext, disabled }) {
11
+ if (disabled) return null;
12
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(NavZone, {
13
+ direction: "prev",
14
+ onClick: onPrev
15
+ }), /* @__PURE__ */ jsx(NavZone, {
16
+ direction: "next",
17
+ onClick: onNext
18
+ })] });
19
+ }
20
+ function NavZone({ direction, onClick }) {
21
+ const [hovered, setHovered] = useState(false);
22
+ const isPrev = direction === "prev";
23
+ return /* @__PURE__ */ jsx("button", {
24
+ type: "button",
25
+ onClick: useCallback((e) => {
26
+ e.stopPropagation();
27
+ onClick();
28
+ }, [onClick]),
29
+ onMouseEnter: () => setHovered(true),
30
+ onMouseLeave: () => setHovered(false),
31
+ "aria-label": isPrev ? "Previous slide" : "Next slide",
32
+ style: {
33
+ position: "absolute",
34
+ top: 0,
35
+ bottom: 0,
36
+ [isPrev ? "left" : "right"]: 0,
37
+ width: "15%",
38
+ background: "none",
39
+ border: "none",
40
+ cursor: "pointer",
41
+ zIndex: 20,
42
+ display: "flex",
43
+ alignItems: "center",
44
+ justifyContent: isPrev ? "flex-start" : "flex-end",
45
+ padding: "0 1.5rem",
46
+ opacity: hovered ? 1 : 0,
47
+ transition: "opacity 0.2s ease"
48
+ },
49
+ children: /* @__PURE__ */ jsx("svg", {
50
+ width: "32",
51
+ height: "32",
52
+ viewBox: "0 0 24 24",
53
+ fill: "none",
54
+ stroke: "currentColor",
55
+ strokeWidth: "2",
56
+ strokeLinecap: "round",
57
+ strokeLinejoin: "round",
58
+ style: {
59
+ color: "var(--slide-text, #1a1a1a)",
60
+ opacity: .4,
61
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.2))",
62
+ transform: isPrev ? "none" : "rotate(180deg)"
63
+ },
64
+ children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" })
65
+ })
66
+ });
67
+ }
68
+ //#endregion
69
+ //#region src/context.ts
70
+ const DeckContext = createContext(null);
71
+ function useDeck() {
72
+ const ctx = useContext(DeckContext);
73
+ if (!ctx) throw new Error("useDeck must be used within a <Deck> component");
74
+ return ctx;
75
+ }
76
+ //#endregion
77
+ //#region src/DrawingLayer.tsx
78
+ /**
79
+ * Canvas-based freehand drawing overlay for presentations.
80
+ * Drawings are stored per slide and persist across navigation.
81
+ * Toggle with `d` key, clear current slide with `c` key.
82
+ */
83
+ function DrawingLayer({ active, currentSlide, color = "#ef4444", width = 3 }) {
84
+ const canvasRef = useRef(null);
85
+ const [isDrawing, setIsDrawing] = useState(false);
86
+ const lastPoint = useRef(null);
87
+ const drawingsRef = useRef(/* @__PURE__ */ new Map());
88
+ const prevSlideRef = useRef(currentSlide);
89
+ const getCanvasSize = useCallback(() => {
90
+ const canvas = canvasRef.current;
91
+ if (!canvas) return {
92
+ w: 0,
93
+ h: 0
94
+ };
95
+ return {
96
+ w: canvas.width,
97
+ h: canvas.height
98
+ };
99
+ }, []);
100
+ const saveCurrentSlide = useCallback((slideIndex) => {
101
+ const canvas = canvasRef.current;
102
+ const ctx = canvas?.getContext("2d");
103
+ if (!ctx || !canvas) return;
104
+ const { w, h } = getCanvasSize();
105
+ if (w === 0 || h === 0) return;
106
+ const imageData = ctx.getImageData(0, 0, w, h);
107
+ if (imageData.data.some((_, i) => i % 4 === 3 && imageData.data[i] > 0)) drawingsRef.current.set(slideIndex, imageData);
108
+ else drawingsRef.current.delete(slideIndex);
109
+ }, [getCanvasSize]);
110
+ const restoreSlide = useCallback((slideIndex) => {
111
+ const canvas = canvasRef.current;
112
+ const ctx = canvas?.getContext("2d");
113
+ if (!ctx || !canvas) return;
114
+ const { w, h } = getCanvasSize();
115
+ ctx.clearRect(0, 0, w, h);
116
+ const saved = drawingsRef.current.get(slideIndex);
117
+ if (saved && saved.width === w && saved.height === h) ctx.putImageData(saved, 0, 0);
118
+ }, [getCanvasSize]);
119
+ useEffect(() => {
120
+ if (prevSlideRef.current !== currentSlide) {
121
+ saveCurrentSlide(prevSlideRef.current);
122
+ restoreSlide(currentSlide);
123
+ prevSlideRef.current = currentSlide;
124
+ }
125
+ }, [
126
+ currentSlide,
127
+ saveCurrentSlide,
128
+ restoreSlide
129
+ ]);
130
+ const getPoint = useCallback((e) => {
131
+ const canvas = canvasRef.current;
132
+ const rect = canvas.getBoundingClientRect();
133
+ return {
134
+ x: (e.clientX - rect.left) * (canvas.width / rect.width),
135
+ y: (e.clientY - rect.top) * (canvas.height / rect.height)
136
+ };
137
+ }, []);
138
+ const startDraw = useCallback((e) => {
139
+ if (!active) return;
140
+ setIsDrawing(true);
141
+ lastPoint.current = getPoint(e);
142
+ }, [active, getPoint]);
143
+ const draw = useCallback((e) => {
144
+ if (!isDrawing || !active) return;
145
+ const ctx = canvasRef.current?.getContext("2d");
146
+ if (!ctx || !lastPoint.current) return;
147
+ const point = getPoint(e);
148
+ ctx.beginPath();
149
+ ctx.moveTo(lastPoint.current.x, lastPoint.current.y);
150
+ ctx.lineTo(point.x, point.y);
151
+ ctx.strokeStyle = color;
152
+ ctx.lineWidth = width;
153
+ ctx.lineCap = "round";
154
+ ctx.lineJoin = "round";
155
+ ctx.stroke();
156
+ lastPoint.current = point;
157
+ }, [
158
+ isDrawing,
159
+ active,
160
+ color,
161
+ width,
162
+ getPoint
163
+ ]);
164
+ const stopDraw = useCallback(() => {
165
+ setIsDrawing(false);
166
+ lastPoint.current = null;
167
+ }, []);
168
+ useEffect(() => {
169
+ const canvas = canvasRef.current;
170
+ if (!canvas) return;
171
+ const resize = () => {
172
+ const rect = canvas.parentElement?.getBoundingClientRect();
173
+ if (!rect) return;
174
+ const newWidth = rect.width * window.devicePixelRatio;
175
+ const newHeight = rect.height * window.devicePixelRatio;
176
+ if (canvas.width !== newWidth || canvas.height !== newHeight) {
177
+ saveCurrentSlide(currentSlide);
178
+ drawingsRef.current.clear();
179
+ canvas.width = newWidth;
180
+ canvas.height = newHeight;
181
+ }
182
+ };
183
+ resize();
184
+ window.addEventListener("resize", resize);
185
+ return () => window.removeEventListener("resize", resize);
186
+ }, [currentSlide, saveCurrentSlide]);
187
+ useEffect(() => {
188
+ if (!active) return;
189
+ function handleKey(e) {
190
+ if (e.key === "c") {
191
+ const canvas = canvasRef.current;
192
+ const ctx = canvas?.getContext("2d");
193
+ if (ctx && canvas) {
194
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
195
+ drawingsRef.current.delete(currentSlide);
196
+ }
197
+ }
198
+ }
199
+ window.addEventListener("keydown", handleKey);
200
+ return () => window.removeEventListener("keydown", handleKey);
201
+ }, [active, currentSlide]);
202
+ return /* @__PURE__ */ jsx("canvas", {
203
+ ref: canvasRef,
204
+ onMouseDown: startDraw,
205
+ onMouseMove: draw,
206
+ onMouseUp: stopDraw,
207
+ onMouseLeave: stopDraw,
208
+ style: {
209
+ position: "absolute",
210
+ inset: 0,
211
+ width: "100%",
212
+ height: "100%",
213
+ cursor: active ? "crosshair" : "default",
214
+ pointerEvents: active ? "auto" : "none",
215
+ zIndex: 50
216
+ }
217
+ });
218
+ }
219
+ //#endregion
220
+ //#region src/use-presenter.ts
221
+ const CHANNEL_NAME = "reslide-presenter";
222
+ /**
223
+ * Hook for syncing presentation state across windows via BroadcastChannel.
224
+ * The main presentation window broadcasts state changes and listens for
225
+ * navigation commands from the presenter window.
226
+ */
227
+ function usePresenterSync(currentSlide, clickStep, handlers) {
228
+ const channelRef = useRef(null);
229
+ useEffect(() => {
230
+ if (typeof BroadcastChannel === "undefined") return;
231
+ const channel = new BroadcastChannel(CHANNEL_NAME);
232
+ channelRef.current = channel;
233
+ if (handlers) channel.onmessage = (e) => {
234
+ if (e.data.type === "navigate") switch (e.data.action) {
235
+ case "next":
236
+ handlers.next();
237
+ break;
238
+ case "prev":
239
+ handlers.prev();
240
+ break;
241
+ case "goTo":
242
+ if (e.data.slideIndex != null) handlers.goTo(e.data.slideIndex);
243
+ break;
244
+ }
245
+ };
246
+ return () => {
247
+ channel.close();
248
+ channelRef.current = null;
249
+ };
250
+ }, [handlers]);
251
+ useEffect(() => {
252
+ channelRef.current?.postMessage({
253
+ type: "sync",
254
+ currentSlide,
255
+ clickStep
256
+ });
257
+ }, [currentSlide, clickStep]);
258
+ }
259
+ /**
260
+ * Hook for the presenter window to listen for sync messages and send
261
+ * navigation commands back to the main window.
262
+ */
263
+ function usePresenterChannel(onSync) {
264
+ const channelRef = useRef(null);
265
+ const onSyncRef = useRef(onSync);
266
+ onSyncRef.current = onSync;
267
+ useEffect(() => {
268
+ if (typeof BroadcastChannel === "undefined") return;
269
+ const channel = new BroadcastChannel(CHANNEL_NAME);
270
+ channelRef.current = channel;
271
+ channel.onmessage = (e) => {
272
+ if (e.data.type === "sync") onSyncRef.current(e.data);
273
+ };
274
+ return () => {
275
+ channel.close();
276
+ channelRef.current = null;
277
+ };
278
+ }, []);
279
+ return {
280
+ next: useCallback(() => {
281
+ channelRef.current?.postMessage({
282
+ type: "navigate",
283
+ action: "next"
284
+ });
285
+ }, []),
286
+ prev: useCallback(() => {
287
+ channelRef.current?.postMessage({
288
+ type: "navigate",
289
+ action: "prev"
290
+ });
291
+ }, []),
292
+ goTo: useCallback((index) => {
293
+ channelRef.current?.postMessage({
294
+ type: "navigate",
295
+ action: "goTo",
296
+ slideIndex: index
297
+ });
298
+ }, [])
299
+ };
300
+ }
301
+ /**
302
+ * Opens the presenter window at the /presenter route.
303
+ */
304
+ function openPresenterWindow() {
305
+ const url = new URL(window.location.href);
306
+ url.searchParams.set("presenter", "true");
307
+ window.open(url.toString(), "reslide-presenter", "width=1024,height=768,menubar=no,toolbar=no");
308
+ }
309
+ /**
310
+ * Check if the current window is the presenter view.
311
+ */
312
+ function isPresenterView() {
313
+ if (typeof window === "undefined") return false;
314
+ return new URLSearchParams(window.location.search).get("presenter") === "true";
315
+ }
316
+ //#endregion
317
+ //#region src/NavigationBar.tsx
318
+ /**
319
+ * Slidev-style navigation bar that appears on hover at the bottom of the presentation.
320
+ * Provides buttons for prev/next, overview, fullscreen, presenter, and drawing modes.
321
+ */
322
+ function NavigationBar({ isDrawing, onToggleDrawing }) {
323
+ const { currentSlide, totalSlides, clickStep, totalClickSteps, isOverview, isFullscreen, next, prev, toggleOverview, toggleFullscreen } = useDeck();
324
+ const [visible, setVisible] = useState(false);
325
+ const timerRef = useRef(null);
326
+ const barRef = useRef(null);
327
+ const showBar = useCallback(() => {
328
+ setVisible(true);
329
+ if (timerRef.current) clearTimeout(timerRef.current);
330
+ timerRef.current = setTimeout(() => setVisible(false), 3e3);
331
+ }, []);
332
+ useEffect(() => {
333
+ function handleMouseMove(e) {
334
+ const threshold = window.innerHeight - 80;
335
+ if (e.clientY > threshold) showBar();
336
+ }
337
+ window.addEventListener("mousemove", handleMouseMove);
338
+ return () => window.removeEventListener("mousemove", handleMouseMove);
339
+ }, [showBar]);
340
+ const handleMouseEnter = () => {
341
+ if (timerRef.current) clearTimeout(timerRef.current);
342
+ setVisible(true);
343
+ };
344
+ const handleMouseLeave = () => {
345
+ timerRef.current = setTimeout(() => setVisible(false), 1500);
346
+ };
347
+ return /* @__PURE__ */ jsxs("div", {
348
+ ref: barRef,
349
+ className: "reslide-navbar",
350
+ onMouseEnter: handleMouseEnter,
351
+ onMouseLeave: handleMouseLeave,
352
+ style: {
353
+ position: "absolute",
354
+ bottom: 0,
355
+ left: "50%",
356
+ transform: `translateX(-50%) translateY(${visible ? "0" : "100%"})`,
357
+ display: "flex",
358
+ alignItems: "center",
359
+ gap: "0.25rem",
360
+ padding: "0.375rem 0.75rem",
361
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
362
+ backdropFilter: "blur(8px)",
363
+ borderRadius: "0.5rem 0.5rem 0 0",
364
+ transition: "transform 0.25s ease, opacity 0.25s ease",
365
+ opacity: visible ? 1 : 0,
366
+ zIndex: 200,
367
+ color: "#e2e8f0",
368
+ fontSize: "0.8125rem",
369
+ fontFamily: "system-ui, sans-serif",
370
+ fontVariantNumeric: "tabular-nums",
371
+ pointerEvents: visible ? "auto" : "none"
372
+ },
373
+ children: [
374
+ /* @__PURE__ */ jsx(NavButton, {
375
+ onClick: prev,
376
+ title: "Previous (←)",
377
+ disabled: isOverview,
378
+ children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "left" })
379
+ }),
380
+ /* @__PURE__ */ jsxs("span", {
381
+ style: {
382
+ padding: "0 0.5rem",
383
+ userSelect: "none",
384
+ whiteSpace: "nowrap"
385
+ },
386
+ children: [
387
+ currentSlide + 1,
388
+ " / ",
389
+ totalSlides,
390
+ clickStep > 0 && totalClickSteps > 0 && /* @__PURE__ */ jsxs("span", {
391
+ style: {
392
+ opacity: .6,
393
+ marginLeft: "0.25rem"
394
+ },
395
+ children: [
396
+ "(",
397
+ clickStep,
398
+ "/",
399
+ totalClickSteps,
400
+ ")"
401
+ ]
402
+ })
403
+ ]
404
+ }),
405
+ /* @__PURE__ */ jsx(NavButton, {
406
+ onClick: next,
407
+ title: "Next (→ / Space)",
408
+ disabled: isOverview,
409
+ children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "right" })
410
+ }),
411
+ /* @__PURE__ */ jsx(Divider, {}),
412
+ /* @__PURE__ */ jsx(NavButton, {
413
+ onClick: toggleOverview,
414
+ title: "Overview (Esc)",
415
+ active: isOverview,
416
+ children: /* @__PURE__ */ jsx(GridIcon, {})
417
+ }),
418
+ /* @__PURE__ */ jsx(NavButton, {
419
+ onClick: toggleFullscreen,
420
+ title: "Fullscreen (f)",
421
+ active: isFullscreen,
422
+ children: /* @__PURE__ */ jsx(FullscreenIcon, { expanded: isFullscreen })
423
+ }),
424
+ /* @__PURE__ */ jsx(NavButton, {
425
+ onClick: openPresenterWindow,
426
+ title: "Presenter (p)",
427
+ children: /* @__PURE__ */ jsx(PresenterIcon, {})
428
+ }),
429
+ /* @__PURE__ */ jsx(NavButton, {
430
+ onClick: onToggleDrawing,
431
+ title: "Drawing (d)",
432
+ active: isDrawing,
433
+ children: /* @__PURE__ */ jsx(PenIcon, {})
434
+ })
435
+ ]
436
+ });
437
+ }
438
+ function NavButton({ children, onClick, title, active, disabled }) {
439
+ return /* @__PURE__ */ jsx("button", {
440
+ type: "button",
441
+ onClick,
442
+ title,
443
+ disabled,
444
+ style: {
445
+ display: "flex",
446
+ alignItems: "center",
447
+ justifyContent: "center",
448
+ width: "2rem",
449
+ height: "2rem",
450
+ background: active ? "rgba(255,255,255,0.2)" : "none",
451
+ border: "none",
452
+ borderRadius: "0.25rem",
453
+ cursor: disabled ? "default" : "pointer",
454
+ color: active ? "#fff" : "#cbd5e1",
455
+ opacity: disabled ? .4 : 1,
456
+ transition: "background 0.15s, color 0.15s",
457
+ padding: 0
458
+ },
459
+ onMouseEnter: (e) => {
460
+ if (!disabled) e.currentTarget.style.background = "rgba(255,255,255,0.15)";
461
+ },
462
+ onMouseLeave: (e) => {
463
+ e.currentTarget.style.background = active ? "rgba(255,255,255,0.2)" : "none";
464
+ },
465
+ children
466
+ });
467
+ }
468
+ function Divider() {
469
+ return /* @__PURE__ */ jsx("div", { style: {
470
+ width: 1,
471
+ height: "1.25rem",
472
+ backgroundColor: "rgba(255,255,255,0.2)",
473
+ margin: "0 0.25rem"
474
+ } });
475
+ }
476
+ function ArrowIcon({ direction }) {
477
+ return /* @__PURE__ */ jsx("svg", {
478
+ width: "16",
479
+ height: "16",
480
+ viewBox: "0 0 24 24",
481
+ fill: "none",
482
+ stroke: "currentColor",
483
+ strokeWidth: "2",
484
+ strokeLinecap: "round",
485
+ strokeLinejoin: "round",
486
+ children: direction === "left" ? /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) : /* @__PURE__ */ jsx("polyline", { points: "9 6 15 12 9 18" })
487
+ });
488
+ }
489
+ function GridIcon() {
490
+ return /* @__PURE__ */ jsxs("svg", {
491
+ width: "16",
492
+ height: "16",
493
+ viewBox: "0 0 24 24",
494
+ fill: "none",
495
+ stroke: "currentColor",
496
+ strokeWidth: "2",
497
+ strokeLinecap: "round",
498
+ strokeLinejoin: "round",
499
+ children: [
500
+ /* @__PURE__ */ jsx("rect", {
501
+ x: "3",
502
+ y: "3",
503
+ width: "7",
504
+ height: "7"
505
+ }),
506
+ /* @__PURE__ */ jsx("rect", {
507
+ x: "14",
508
+ y: "3",
509
+ width: "7",
510
+ height: "7"
511
+ }),
512
+ /* @__PURE__ */ jsx("rect", {
513
+ x: "3",
514
+ y: "14",
515
+ width: "7",
516
+ height: "7"
517
+ }),
518
+ /* @__PURE__ */ jsx("rect", {
519
+ x: "14",
520
+ y: "14",
521
+ width: "7",
522
+ height: "7"
523
+ })
524
+ ]
525
+ });
526
+ }
527
+ function FullscreenIcon({ expanded }) {
528
+ return /* @__PURE__ */ jsx("svg", {
529
+ width: "16",
530
+ height: "16",
531
+ viewBox: "0 0 24 24",
532
+ fill: "none",
533
+ stroke: "currentColor",
534
+ strokeWidth: "2",
535
+ strokeLinecap: "round",
536
+ strokeLinejoin: "round",
537
+ children: expanded ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
538
+ /* @__PURE__ */ jsx("polyline", { points: "4 14 10 14 10 20" }),
539
+ /* @__PURE__ */ jsx("polyline", { points: "20 10 14 10 14 4" }),
540
+ /* @__PURE__ */ jsx("line", {
541
+ x1: "14",
542
+ y1: "10",
543
+ x2: "21",
544
+ y2: "3"
545
+ }),
546
+ /* @__PURE__ */ jsx("line", {
547
+ x1: "3",
548
+ y1: "21",
549
+ x2: "10",
550
+ y2: "14"
551
+ })
552
+ ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
553
+ /* @__PURE__ */ jsx("polyline", { points: "15 3 21 3 21 9" }),
554
+ /* @__PURE__ */ jsx("polyline", { points: "9 21 3 21 3 15" }),
555
+ /* @__PURE__ */ jsx("line", {
556
+ x1: "21",
557
+ y1: "3",
558
+ x2: "14",
559
+ y2: "10"
560
+ }),
561
+ /* @__PURE__ */ jsx("line", {
562
+ x1: "3",
563
+ y1: "21",
564
+ x2: "10",
565
+ y2: "14"
566
+ })
567
+ ] })
568
+ });
569
+ }
570
+ function PresenterIcon() {
571
+ return /* @__PURE__ */ jsxs("svg", {
572
+ width: "16",
573
+ height: "16",
574
+ viewBox: "0 0 24 24",
575
+ fill: "none",
576
+ stroke: "currentColor",
577
+ strokeWidth: "2",
578
+ strokeLinecap: "round",
579
+ strokeLinejoin: "round",
580
+ children: [
581
+ /* @__PURE__ */ jsx("rect", {
582
+ x: "2",
583
+ y: "3",
584
+ width: "20",
585
+ height: "14",
586
+ rx: "2"
587
+ }),
588
+ /* @__PURE__ */ jsx("line", {
589
+ x1: "8",
590
+ y1: "21",
591
+ x2: "16",
592
+ y2: "21"
593
+ }),
594
+ /* @__PURE__ */ jsx("line", {
595
+ x1: "12",
596
+ y1: "17",
597
+ x2: "12",
598
+ y2: "21"
599
+ })
600
+ ]
601
+ });
602
+ }
603
+ function PenIcon() {
604
+ return /* @__PURE__ */ jsxs("svg", {
605
+ width: "16",
606
+ height: "16",
607
+ viewBox: "0 0 24 24",
608
+ fill: "none",
609
+ stroke: "currentColor",
610
+ strokeWidth: "2",
611
+ strokeLinecap: "round",
612
+ strokeLinejoin: "round",
613
+ children: [
614
+ /* @__PURE__ */ jsx("path", { d: "M12 19l7-7 3 3-7 7-3-3z" }),
615
+ /* @__PURE__ */ jsx("path", { d: "M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" }),
616
+ /* @__PURE__ */ jsx("path", { d: "M2 2l7.586 7.586" }),
617
+ /* @__PURE__ */ jsx("circle", {
618
+ cx: "11",
619
+ cy: "11",
620
+ r: "2"
621
+ })
622
+ ]
623
+ });
624
+ }
625
+ //#endregion
626
+ //#region src/slide-context.ts
627
+ const SlideIndexContext = createContext(null);
628
+ function useSlideIndex() {
629
+ const index = useContext(SlideIndexContext);
630
+ if (index == null) throw new Error("useSlideIndex must be used within a <Slide> component");
631
+ return index;
632
+ }
633
+ //#endregion
634
+ //#region src/PrintView.tsx
635
+ /**
636
+ * Renders all slides vertically for print/PDF export.
637
+ * Use with @media print CSS to generate PDFs via browser print.
638
+ */
639
+ function PrintView({ children }) {
640
+ return /* @__PURE__ */ jsx("div", {
641
+ className: "reslide-print-view",
642
+ children: Children.toArray(children).map((slide, i) => /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
643
+ value: i,
644
+ children: slide
645
+ }, i))
646
+ });
647
+ }
648
+ //#endregion
649
+ //#region src/ProgressBar.tsx
650
+ function ProgressBar() {
651
+ const { currentSlide, totalSlides, clickStep, totalClickSteps } = useDeck();
652
+ const slideProgress = totalSlides <= 1 ? 1 : currentSlide / (totalSlides - 1);
653
+ const clickFraction = totalSlides <= 1 ? 0 : totalClickSteps > 0 ? clickStep / totalClickSteps * (1 / (totalSlides - 1)) : 0;
654
+ return /* @__PURE__ */ jsx("div", {
655
+ className: "reslide-progress-bar",
656
+ style: {
657
+ position: "absolute",
658
+ top: 0,
659
+ left: 0,
660
+ width: "100%",
661
+ height: "3px",
662
+ zIndex: 100
663
+ },
664
+ children: /* @__PURE__ */ jsx("div", { style: {
665
+ height: "100%",
666
+ width: `${Math.min(slideProgress + clickFraction, 1) * 100}%`,
667
+ backgroundColor: "var(--slide-accent, #3b82f6)",
668
+ transition: "width 0.3s ease"
669
+ } })
670
+ });
671
+ }
672
+ //#endregion
673
+ //#region src/SlideNumber.tsx
674
+ /**
675
+ * Displays the current slide number in the bottom-right corner.
676
+ * Automatically reads state from DeckContext.
677
+ */
678
+ function SlideNumber() {
679
+ const { currentSlide, totalSlides } = useDeck();
680
+ return /* @__PURE__ */ jsxs("div", {
681
+ className: "reslide-slide-number",
682
+ style: {
683
+ position: "absolute",
684
+ bottom: "0.75rem",
685
+ right: "1rem",
686
+ fontSize: "0.75rem",
687
+ fontFamily: "system-ui, sans-serif",
688
+ fontVariantNumeric: "tabular-nums",
689
+ color: "var(--slide-text, #1a1a1a)",
690
+ opacity: .4,
691
+ pointerEvents: "none",
692
+ zIndex: 10
693
+ },
694
+ children: [
695
+ currentSlide + 1,
696
+ " / ",
697
+ totalSlides
698
+ ]
699
+ });
700
+ }
701
+ //#endregion
702
+ //#region src/SlideTransition.tsx
703
+ const DURATION = 300;
704
+ function SlideTransition({ children, currentSlide, transition }) {
705
+ const slides = Children.toArray(children);
706
+ const [displaySlide, setDisplaySlide] = useState(currentSlide);
707
+ const [prevSlide, setPrevSlide] = useState(null);
708
+ const [isAnimating, setIsAnimating] = useState(false);
709
+ const prevCurrentRef = useRef(currentSlide);
710
+ const timerRef = useRef(null);
711
+ useEffect(() => {
712
+ if (currentSlide === prevCurrentRef.current) return;
713
+ const from = prevCurrentRef.current;
714
+ prevCurrentRef.current = currentSlide;
715
+ if (transition === "none") {
716
+ setDisplaySlide(currentSlide);
717
+ return;
718
+ }
719
+ setPrevSlide(from);
720
+ setDisplaySlide(currentSlide);
721
+ setIsAnimating(true);
722
+ if (timerRef.current) clearTimeout(timerRef.current);
723
+ timerRef.current = setTimeout(() => {
724
+ setIsAnimating(false);
725
+ setPrevSlide(null);
726
+ }, DURATION);
727
+ return () => {
728
+ if (timerRef.current) clearTimeout(timerRef.current);
729
+ };
730
+ }, [currentSlide, transition]);
731
+ const resolvedTransition = resolveTransition(transition, prevSlide, displaySlide);
732
+ if (transition === "none" || !isAnimating) return /* @__PURE__ */ jsx("div", {
733
+ className: "reslide-transition-container",
734
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
735
+ value: displaySlide,
736
+ children: /* @__PURE__ */ jsx("div", {
737
+ className: "reslide-transition-slide",
738
+ children: slides[displaySlide]
739
+ })
740
+ })
741
+ });
742
+ return /* @__PURE__ */ jsxs("div", {
743
+ className: "reslide-transition-container",
744
+ children: [prevSlide != null && /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
745
+ value: prevSlide,
746
+ children: /* @__PURE__ */ jsx("div", {
747
+ className: `reslide-transition-slide reslide-transition-${resolvedTransition}-exit`,
748
+ children: slides[prevSlide]
749
+ })
750
+ }), /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
751
+ value: displaySlide,
752
+ children: /* @__PURE__ */ jsx("div", {
753
+ className: `reslide-transition-slide reslide-transition-${resolvedTransition}-enter`,
754
+ children: slides[displaySlide]
755
+ })
756
+ })]
757
+ });
758
+ }
759
+ function resolveTransition(transition, from, to) {
760
+ if (transition !== "slide-left" && transition !== "slide-right") return transition;
761
+ if (from == null) return transition;
762
+ const goingForward = to > from;
763
+ if (transition === "slide-left") return goingForward ? "slide-left" : "slide-right";
764
+ return goingForward ? "slide-right" : "slide-left";
765
+ }
766
+ //#endregion
767
+ //#region src/use-fullscreen.ts
768
+ function useFullscreen(ref) {
769
+ const [isFullscreen, setIsFullscreen] = useState(false);
770
+ useEffect(() => {
771
+ function handleChange() {
772
+ setIsFullscreen(document.fullscreenElement != null);
773
+ }
774
+ document.addEventListener("fullscreenchange", handleChange);
775
+ return () => document.removeEventListener("fullscreenchange", handleChange);
776
+ }, []);
777
+ return {
778
+ isFullscreen,
779
+ toggleFullscreen: useCallback(() => {
780
+ if (!ref.current) return;
781
+ if (document.fullscreenElement) document.exitFullscreen();
782
+ else ref.current.requestFullscreen();
783
+ }, [ref])
784
+ };
785
+ }
786
+ //#endregion
787
+ //#region src/Deck.tsx
788
+ function Deck({ children, transition = "none" }) {
789
+ const containerRef = useRef(null);
790
+ const [currentSlide, setCurrentSlide] = useState(0);
791
+ const [clickStep, setClickStep] = useState(0);
792
+ const [isOverview, setIsOverview] = useState(false);
793
+ const [isDrawing, setIsDrawing] = useState(false);
794
+ const [isPrinting, setIsPrinting] = useState(false);
795
+ const [clickStepsMap, setClickStepsMap] = useState({});
796
+ const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
797
+ const totalSlides = Children.count(children);
798
+ const totalClickSteps = clickStepsMap[currentSlide] ?? 0;
799
+ const registerClickSteps = useCallback((slideIndex, count) => {
800
+ setClickStepsMap((prev) => {
801
+ if (prev[slideIndex] === count) return prev;
802
+ return {
803
+ ...prev,
804
+ [slideIndex]: count
805
+ };
806
+ });
807
+ }, []);
808
+ const next = useCallback(() => {
809
+ if (isOverview) return;
810
+ if (clickStep < totalClickSteps) setClickStep((s) => s + 1);
811
+ else if (currentSlide < totalSlides - 1) {
812
+ setCurrentSlide((s) => s + 1);
813
+ setClickStep(0);
814
+ }
815
+ }, [
816
+ isOverview,
817
+ clickStep,
818
+ totalClickSteps,
819
+ currentSlide,
820
+ totalSlides
821
+ ]);
822
+ const prev = useCallback(() => {
823
+ if (isOverview) return;
824
+ if (clickStep > 0) setClickStep((s) => s - 1);
825
+ else if (currentSlide > 0) {
826
+ const prevSlide = currentSlide - 1;
827
+ setCurrentSlide(prevSlide);
828
+ setClickStep(clickStepsMap[prevSlide] ?? 0);
829
+ }
830
+ }, [
831
+ isOverview,
832
+ clickStep,
833
+ currentSlide,
834
+ clickStepsMap
835
+ ]);
836
+ const goTo = useCallback((slideIndex) => {
837
+ if (slideIndex >= 0 && slideIndex < totalSlides) {
838
+ setCurrentSlide(slideIndex);
839
+ setClickStep(0);
840
+ if (isOverview) setIsOverview(false);
841
+ }
842
+ }, [totalSlides, isOverview]);
843
+ usePresenterSync(currentSlide, clickStep, useMemo(() => ({
844
+ next,
845
+ prev,
846
+ goTo
847
+ }), [
848
+ next,
849
+ prev,
850
+ goTo
851
+ ]));
852
+ const toggleOverview = useCallback(() => {
853
+ setIsOverview((v) => !v);
854
+ }, []);
855
+ useEffect(() => {
856
+ function handleKeyDown(e) {
857
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
858
+ switch (e.key) {
859
+ case "ArrowRight":
860
+ case " ":
861
+ e.preventDefault();
862
+ next();
863
+ break;
864
+ case "ArrowLeft":
865
+ e.preventDefault();
866
+ prev();
867
+ break;
868
+ case "Escape":
869
+ if (!document.fullscreenElement) {
870
+ e.preventDefault();
871
+ toggleOverview();
872
+ }
873
+ break;
874
+ case "f":
875
+ e.preventDefault();
876
+ toggleFullscreen();
877
+ break;
878
+ case "p":
879
+ e.preventDefault();
880
+ openPresenterWindow();
881
+ break;
882
+ case "d":
883
+ e.preventDefault();
884
+ setIsDrawing((v) => !v);
885
+ break;
886
+ }
887
+ }
888
+ window.addEventListener("keydown", handleKeyDown);
889
+ return () => window.removeEventListener("keydown", handleKeyDown);
890
+ }, [
891
+ next,
892
+ prev,
893
+ toggleOverview,
894
+ toggleFullscreen
895
+ ]);
896
+ useEffect(() => {
897
+ function onBeforePrint() {
898
+ setIsPrinting(true);
899
+ }
900
+ function onAfterPrint() {
901
+ setIsPrinting(false);
902
+ }
903
+ window.addEventListener("beforeprint", onBeforePrint);
904
+ window.addEventListener("afterprint", onAfterPrint);
905
+ return () => {
906
+ window.removeEventListener("beforeprint", onBeforePrint);
907
+ window.removeEventListener("afterprint", onAfterPrint);
908
+ };
909
+ }, []);
910
+ const contextValue = useMemo(() => ({
911
+ currentSlide,
912
+ totalSlides,
913
+ clickStep,
914
+ totalClickSteps,
915
+ isOverview,
916
+ isFullscreen,
917
+ next,
918
+ prev,
919
+ goTo,
920
+ toggleOverview,
921
+ toggleFullscreen,
922
+ registerClickSteps
923
+ }), [
924
+ currentSlide,
925
+ totalSlides,
926
+ clickStep,
927
+ totalClickSteps,
928
+ isOverview,
929
+ isFullscreen,
930
+ next,
931
+ prev,
932
+ goTo,
933
+ toggleOverview,
934
+ toggleFullscreen,
935
+ registerClickSteps
936
+ ]);
937
+ return /* @__PURE__ */ jsx(DeckContext.Provider, {
938
+ value: contextValue,
939
+ children: /* @__PURE__ */ jsxs("div", {
940
+ ref: containerRef,
941
+ className: "reslide-deck",
942
+ style: {
943
+ position: "relative",
944
+ width: "100%",
945
+ height: "100%",
946
+ overflow: "hidden",
947
+ backgroundColor: "var(--slide-bg, #fff)",
948
+ color: "var(--slide-text, #1a1a1a)"
949
+ },
950
+ children: [
951
+ isPrinting ? /* @__PURE__ */ jsx(PrintView, { children }) : isOverview ? /* @__PURE__ */ jsx(OverviewGrid, {
952
+ totalSlides,
953
+ goTo,
954
+ children
955
+ }) : /* @__PURE__ */ jsx(SlideTransition, {
956
+ currentSlide,
957
+ transition,
958
+ children
959
+ }),
960
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(ClickNavigation, {
961
+ onPrev: prev,
962
+ onNext: next,
963
+ disabled: isDrawing
964
+ }),
965
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(ProgressBar, {}),
966
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(SlideNumber, {}),
967
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, {
968
+ active: isDrawing,
969
+ currentSlide
970
+ }),
971
+ !isPrinting && /* @__PURE__ */ jsx(NavigationBar, {
972
+ isDrawing,
973
+ onToggleDrawing: () => setIsDrawing((v) => !v)
974
+ })
975
+ ]
976
+ })
977
+ });
978
+ }
979
+ function OverviewGrid({ children, totalSlides, goTo }) {
980
+ const slides = Children.toArray(children);
981
+ return /* @__PURE__ */ jsx("div", {
982
+ className: "reslide-overview",
983
+ style: {
984
+ display: "grid",
985
+ gridTemplateColumns: `repeat(${Math.ceil(Math.sqrt(totalSlides))}, 1fr)`,
986
+ gap: "1rem",
987
+ padding: "1rem",
988
+ width: "100%",
989
+ height: "100%",
990
+ overflow: "auto"
991
+ },
992
+ children: slides.map((slide, i) => /* @__PURE__ */ jsx("button", {
993
+ type: "button",
994
+ onClick: () => goTo(i),
995
+ style: {
996
+ border: "1px solid var(--slide-accent, #3b82f6)",
997
+ borderRadius: "0.5rem",
998
+ overflow: "hidden",
999
+ cursor: "pointer",
1000
+ background: "var(--slide-bg, #fff)",
1001
+ aspectRatio: "16 / 9",
1002
+ padding: 0,
1003
+ position: "relative"
1004
+ },
1005
+ children: /* @__PURE__ */ jsx("div", {
1006
+ style: {
1007
+ transform: "scale(0.25)",
1008
+ transformOrigin: "top left",
1009
+ width: "400%",
1010
+ height: "400%",
1011
+ pointerEvents: "none"
1012
+ },
1013
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
1014
+ value: i,
1015
+ children: slide
1016
+ })
1017
+ })
1018
+ }, i))
1019
+ });
1020
+ }
1021
+ //#endregion
1022
+ //#region src/Slide.tsx
1023
+ const baseStyle = {
1024
+ width: "100%",
1025
+ height: "100%",
1026
+ display: "flex",
1027
+ boxSizing: "border-box"
1028
+ };
1029
+ function isSlotRight(child) {
1030
+ return isValidElement(child) && typeof child.type === "function" && "__reslideSlot" in child.type && child.type.__reslideSlot === "right";
1031
+ }
1032
+ function splitChildren(children) {
1033
+ const left = [];
1034
+ const right = [];
1035
+ let inRight = false;
1036
+ Children.forEach(children, (child) => {
1037
+ if (isSlotRight(child)) {
1038
+ inRight = true;
1039
+ right.push(child.props.children);
1040
+ return;
1041
+ }
1042
+ if (inRight) right.push(child);
1043
+ else left.push(child);
1044
+ });
1045
+ return {
1046
+ left,
1047
+ right
1048
+ };
1049
+ }
1050
+ function Slide({ children, layout = "default", image, className, style }) {
1051
+ const cls = `reslide-slide reslide-layout-${layout}${className ? ` ${className}` : ""}`;
1052
+ switch (layout) {
1053
+ case "center": return /* @__PURE__ */ jsx("div", {
1054
+ className: cls,
1055
+ style: {
1056
+ ...baseStyle,
1057
+ flexDirection: "column",
1058
+ justifyContent: "center",
1059
+ alignItems: "center",
1060
+ textAlign: "center",
1061
+ padding: "3rem 4rem",
1062
+ ...style
1063
+ },
1064
+ children
1065
+ });
1066
+ case "two-cols": {
1067
+ const { left, right } = splitChildren(children);
1068
+ return /* @__PURE__ */ jsxs("div", {
1069
+ className: cls,
1070
+ style: {
1071
+ ...baseStyle,
1072
+ flexDirection: "row",
1073
+ gap: "2rem",
1074
+ padding: "3rem 4rem",
1075
+ ...style
1076
+ },
1077
+ children: [/* @__PURE__ */ jsx("div", {
1078
+ style: {
1079
+ flex: 1,
1080
+ minWidth: 0
1081
+ },
1082
+ children: left
1083
+ }), /* @__PURE__ */ jsx("div", {
1084
+ style: {
1085
+ flex: 1,
1086
+ minWidth: 0
1087
+ },
1088
+ children: right
1089
+ })]
1090
+ });
1091
+ }
1092
+ case "image-right": return /* @__PURE__ */ jsxs("div", {
1093
+ className: cls,
1094
+ style: {
1095
+ ...baseStyle,
1096
+ flexDirection: "row",
1097
+ ...style
1098
+ },
1099
+ children: [/* @__PURE__ */ jsx("div", {
1100
+ style: {
1101
+ flex: 1,
1102
+ padding: "3rem 2rem 3rem 4rem",
1103
+ overflow: "auto"
1104
+ },
1105
+ children
1106
+ }), image && /* @__PURE__ */ jsx("div", { style: {
1107
+ flex: 1,
1108
+ backgroundImage: `url(${image})`,
1109
+ backgroundSize: "cover",
1110
+ backgroundPosition: "center"
1111
+ } })]
1112
+ });
1113
+ case "image-left": return /* @__PURE__ */ jsxs("div", {
1114
+ className: cls,
1115
+ style: {
1116
+ ...baseStyle,
1117
+ flexDirection: "row",
1118
+ ...style
1119
+ },
1120
+ children: [image && /* @__PURE__ */ jsx("div", { style: {
1121
+ flex: 1,
1122
+ backgroundImage: `url(${image})`,
1123
+ backgroundSize: "cover",
1124
+ backgroundPosition: "center"
1125
+ } }), /* @__PURE__ */ jsx("div", {
1126
+ style: {
1127
+ flex: 1,
1128
+ padding: "3rem 4rem 3rem 2rem",
1129
+ overflow: "auto"
1130
+ },
1131
+ children
1132
+ })]
1133
+ });
1134
+ case "section": return /* @__PURE__ */ jsx("div", {
1135
+ className: cls,
1136
+ style: {
1137
+ ...baseStyle,
1138
+ flexDirection: "column",
1139
+ justifyContent: "center",
1140
+ alignItems: "center",
1141
+ textAlign: "center",
1142
+ padding: "3rem 4rem",
1143
+ backgroundColor: "var(--slide-accent, #3b82f6)",
1144
+ color: "var(--slide-section-text, #fff)",
1145
+ ...style
1146
+ },
1147
+ children
1148
+ });
1149
+ case "quote": return /* @__PURE__ */ jsx("div", {
1150
+ className: cls,
1151
+ style: {
1152
+ ...baseStyle,
1153
+ flexDirection: "column",
1154
+ justifyContent: "center",
1155
+ padding: "3rem 6rem",
1156
+ ...style
1157
+ },
1158
+ children: /* @__PURE__ */ jsx("blockquote", {
1159
+ style: {
1160
+ fontSize: "1.5em",
1161
+ fontStyle: "italic",
1162
+ borderLeft: "4px solid var(--slide-accent, #3b82f6)",
1163
+ paddingLeft: "1.5rem",
1164
+ margin: 0
1165
+ },
1166
+ children
1167
+ })
1168
+ });
1169
+ default: return /* @__PURE__ */ jsx("div", {
1170
+ className: cls,
1171
+ style: {
1172
+ ...baseStyle,
1173
+ flexDirection: "column",
1174
+ padding: "3rem 4rem",
1175
+ ...style
1176
+ },
1177
+ children
1178
+ });
1179
+ }
1180
+ }
1181
+ //#endregion
1182
+ //#region src/Click.tsx
1183
+ function Click({ children, at }) {
1184
+ const { clickStep } = useDeck();
1185
+ const visible = clickStep >= (at ?? 1);
1186
+ return /* @__PURE__ */ jsx("div", {
1187
+ className: "reslide-click",
1188
+ style: {
1189
+ opacity: visible ? 1 : 0,
1190
+ visibility: visible ? "visible" : "hidden",
1191
+ pointerEvents: visible ? "auto" : "none",
1192
+ transition: "opacity 0.3s ease, visibility 0.3s ease"
1193
+ },
1194
+ children
1195
+ });
1196
+ }
1197
+ /**
1198
+ * Register click steps for the current slide.
1199
+ * Automatically reads the slide index from SlideIndexContext.
1200
+ * If slideIndex prop is provided, it takes precedence (for backwards compatibility).
1201
+ */
1202
+ function ClickSteps({ count, slideIndex }) {
1203
+ const { registerClickSteps } = useDeck();
1204
+ const contextIndex = useContext(SlideIndexContext);
1205
+ const resolvedIndex = slideIndex ?? contextIndex;
1206
+ useEffect(() => {
1207
+ if (resolvedIndex != null) registerClickSteps(resolvedIndex, count);
1208
+ }, [
1209
+ resolvedIndex,
1210
+ count,
1211
+ registerClickSteps
1212
+ ]);
1213
+ return null;
1214
+ }
1215
+ //#endregion
1216
+ //#region src/Mark.tsx
1217
+ const markStyles = {
1218
+ highlight: (colorName, resolvedColor) => ({
1219
+ backgroundColor: `var(--mark-${colorName}, ${resolvedColor})`,
1220
+ padding: "0.1em 0.2em",
1221
+ borderRadius: "0.2em"
1222
+ }),
1223
+ underline: (colorName, resolvedColor) => ({
1224
+ textDecoration: "underline",
1225
+ textDecorationColor: `var(--mark-${colorName}, ${resolvedColor})`,
1226
+ textDecorationThickness: "0.15em",
1227
+ textUnderlineOffset: "0.15em"
1228
+ }),
1229
+ circle: (colorName, resolvedColor) => ({
1230
+ border: `0.15em solid var(--mark-${colorName}, ${resolvedColor})`,
1231
+ borderRadius: "50%",
1232
+ padding: "0.1em 0.3em"
1233
+ })
1234
+ };
1235
+ const defaultColors = {
1236
+ orange: "#fb923c",
1237
+ red: "#ef4444",
1238
+ blue: "#3b82f6",
1239
+ green: "#22c55e",
1240
+ yellow: "#facc15",
1241
+ purple: "#a855f7"
1242
+ };
1243
+ function Mark({ children, type = "highlight", color = "yellow" }) {
1244
+ const resolvedColor = defaultColors[color] ?? color;
1245
+ const styleFn = markStyles[type] ?? markStyles.highlight;
1246
+ return /* @__PURE__ */ jsx("span", {
1247
+ className: `reslide-mark reslide-mark-${type}`,
1248
+ style: styleFn(color, resolvedColor),
1249
+ children
1250
+ });
1251
+ }
1252
+ //#endregion
1253
+ //#region src/Notes.tsx
1254
+ /**
1255
+ * Speaker notes. Hidden during normal presentation,
1256
+ * visible in overview mode.
1257
+ */
1258
+ function Notes({ children }) {
1259
+ const { isOverview } = useDeck();
1260
+ if (!isOverview) return null;
1261
+ return /* @__PURE__ */ jsx("div", {
1262
+ className: "reslide-notes",
1263
+ style: {
1264
+ marginTop: "auto",
1265
+ padding: "0.75rem",
1266
+ fontSize: "0.75rem",
1267
+ color: "var(--slide-text, #1a1a1a)",
1268
+ opacity: .7,
1269
+ borderTop: "1px solid currentColor"
1270
+ },
1271
+ children
1272
+ });
1273
+ }
1274
+ //#endregion
1275
+ //#region src/Slot.tsx
1276
+ /**
1277
+ * Marks content as belonging to the right column in a two-cols layout.
1278
+ * Used by remarkSlides to separate `::right` content.
1279
+ */
1280
+ function SlotRight({ children }) {
1281
+ return /* @__PURE__ */ jsx(Fragment$1, { children });
1282
+ }
1283
+ SlotRight.displayName = "SlotRight";
1284
+ SlotRight.__reslideSlot = "right";
1285
+ //#endregion
1286
+ //#region src/PresenterView.tsx
1287
+ /**
1288
+ * Presenter view that syncs with the main presentation window.
1289
+ * Shows: current slide, next slide preview, notes, and timer.
1290
+ * Supports bidirectional control — navigate from this window to
1291
+ * drive the main presentation.
1292
+ */
1293
+ function PresenterView({ children, notes }) {
1294
+ const [currentSlide, setCurrentSlide] = useState(0);
1295
+ const [clickStep, setClickStep] = useState(0);
1296
+ const [elapsed, setElapsed] = useState(0);
1297
+ const slides = Children.toArray(children);
1298
+ const totalSlides = slides.length;
1299
+ const { next, prev, goTo } = usePresenterChannel((msg) => {
1300
+ setCurrentSlide(msg.currentSlide);
1301
+ setClickStep(msg.clickStep);
1302
+ });
1303
+ useEffect(() => {
1304
+ function handleKeyDown(e) {
1305
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
1306
+ switch (e.key) {
1307
+ case "ArrowRight":
1308
+ case " ":
1309
+ e.preventDefault();
1310
+ next();
1311
+ break;
1312
+ case "ArrowLeft":
1313
+ e.preventDefault();
1314
+ prev();
1315
+ break;
1316
+ }
1317
+ }
1318
+ window.addEventListener("keydown", handleKeyDown);
1319
+ return () => window.removeEventListener("keydown", handleKeyDown);
1320
+ }, [next, prev]);
1321
+ useEffect(() => {
1322
+ const start = Date.now();
1323
+ const interval = setInterval(() => {
1324
+ setElapsed(Math.floor((Date.now() - start) / 1e3));
1325
+ }, 1e3);
1326
+ return () => clearInterval(interval);
1327
+ }, []);
1328
+ const formatTime = (seconds) => {
1329
+ const m = Math.floor(seconds / 60);
1330
+ const s = seconds % 60;
1331
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
1332
+ };
1333
+ const noopReg = useCallback((_i, _c) => {}, []);
1334
+ const noop = useCallback(() => {}, []);
1335
+ const contextValue = useMemo(() => ({
1336
+ currentSlide,
1337
+ totalSlides,
1338
+ clickStep,
1339
+ totalClickSteps: 0,
1340
+ isOverview: false,
1341
+ isFullscreen: false,
1342
+ next,
1343
+ prev,
1344
+ goTo,
1345
+ toggleOverview: noop,
1346
+ toggleFullscreen: noop,
1347
+ registerClickSteps: noopReg
1348
+ }), [
1349
+ currentSlide,
1350
+ totalSlides,
1351
+ clickStep,
1352
+ next,
1353
+ prev,
1354
+ goTo,
1355
+ noop,
1356
+ noopReg
1357
+ ]);
1358
+ return /* @__PURE__ */ jsx(DeckContext.Provider, {
1359
+ value: contextValue,
1360
+ children: /* @__PURE__ */ jsxs("div", {
1361
+ style: {
1362
+ display: "grid",
1363
+ gridTemplateColumns: "2fr 1fr",
1364
+ gridTemplateRows: "1fr auto",
1365
+ width: "100%",
1366
+ height: "100%",
1367
+ gap: "0.75rem",
1368
+ padding: "0.75rem",
1369
+ backgroundColor: "#1a1a2e",
1370
+ color: "#e2e8f0",
1371
+ fontFamily: "system-ui, sans-serif",
1372
+ boxSizing: "border-box"
1373
+ },
1374
+ children: [
1375
+ /* @__PURE__ */ jsx("div", {
1376
+ style: {
1377
+ border: "2px solid #3b82f6",
1378
+ borderRadius: "0.5rem",
1379
+ overflow: "hidden",
1380
+ position: "relative",
1381
+ backgroundColor: "var(--slide-bg, #fff)",
1382
+ color: "var(--slide-text, #1a1a1a)"
1383
+ },
1384
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
1385
+ value: currentSlide,
1386
+ children: slides[currentSlide]
1387
+ })
1388
+ }),
1389
+ /* @__PURE__ */ jsxs("div", {
1390
+ style: {
1391
+ display: "flex",
1392
+ flexDirection: "column",
1393
+ gap: "0.75rem",
1394
+ minHeight: 0
1395
+ },
1396
+ children: [/* @__PURE__ */ jsx("div", {
1397
+ style: {
1398
+ flex: "0 0 40%",
1399
+ border: "1px solid #334155",
1400
+ borderRadius: "0.5rem",
1401
+ overflow: "hidden",
1402
+ opacity: .8,
1403
+ backgroundColor: "var(--slide-bg, #fff)",
1404
+ color: "var(--slide-text, #1a1a1a)"
1405
+ },
1406
+ children: currentSlide < totalSlides - 1 && /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
1407
+ value: currentSlide + 1,
1408
+ children: /* @__PURE__ */ jsx("div", {
1409
+ style: {
1410
+ transform: "scale(0.5)",
1411
+ transformOrigin: "top left",
1412
+ width: "200%",
1413
+ height: "200%"
1414
+ },
1415
+ children: slides[currentSlide + 1]
1416
+ })
1417
+ })
1418
+ }), /* @__PURE__ */ jsxs("div", {
1419
+ style: {
1420
+ flex: 1,
1421
+ overflow: "auto",
1422
+ padding: "1rem",
1423
+ backgroundColor: "#0f172a",
1424
+ borderRadius: "0.5rem",
1425
+ fontSize: "0.875rem",
1426
+ lineHeight: 1.6
1427
+ },
1428
+ children: [/* @__PURE__ */ jsx("div", {
1429
+ style: {
1430
+ fontWeight: 600,
1431
+ marginBottom: "0.5rem",
1432
+ color: "#94a3b8"
1433
+ },
1434
+ children: "Notes"
1435
+ }), notes?.[currentSlide] ?? /* @__PURE__ */ jsx("span", {
1436
+ style: { color: "#64748b" },
1437
+ children: "No notes for this slide"
1438
+ })]
1439
+ })]
1440
+ }),
1441
+ /* @__PURE__ */ jsxs("div", {
1442
+ style: {
1443
+ gridColumn: "1 / -1",
1444
+ display: "flex",
1445
+ justifyContent: "space-between",
1446
+ alignItems: "center",
1447
+ padding: "0.5rem 1rem",
1448
+ backgroundColor: "#0f172a",
1449
+ borderRadius: "0.5rem"
1450
+ },
1451
+ children: [
1452
+ /* @__PURE__ */ jsx("div", {
1453
+ style: {
1454
+ fontSize: "1.5rem",
1455
+ fontVariantNumeric: "tabular-nums",
1456
+ fontWeight: 700
1457
+ },
1458
+ children: formatTime(elapsed)
1459
+ }),
1460
+ /* @__PURE__ */ jsxs("div", {
1461
+ style: {
1462
+ display: "flex",
1463
+ alignItems: "center",
1464
+ gap: "0.5rem"
1465
+ },
1466
+ children: [
1467
+ /* @__PURE__ */ jsx(PresenterNavButton, {
1468
+ onClick: prev,
1469
+ title: "Previous (←)",
1470
+ children: "◀"
1471
+ }),
1472
+ /* @__PURE__ */ jsxs("span", {
1473
+ style: {
1474
+ fontSize: "1.125rem",
1475
+ fontVariantNumeric: "tabular-nums"
1476
+ },
1477
+ children: [
1478
+ currentSlide + 1,
1479
+ " / ",
1480
+ totalSlides,
1481
+ clickStep > 0 && /* @__PURE__ */ jsxs("span", {
1482
+ style: { color: "#94a3b8" },
1483
+ children: [
1484
+ " (click ",
1485
+ clickStep,
1486
+ ")"
1487
+ ]
1488
+ })
1489
+ ]
1490
+ }),
1491
+ /* @__PURE__ */ jsx(PresenterNavButton, {
1492
+ onClick: next,
1493
+ title: "Next (→ / Space)",
1494
+ children: "▶"
1495
+ })
1496
+ ]
1497
+ }),
1498
+ /* @__PURE__ */ jsx("div", { style: { width: "5rem" } })
1499
+ ]
1500
+ })
1501
+ ]
1502
+ })
1503
+ });
1504
+ }
1505
+ function PresenterNavButton({ children, onClick, title }) {
1506
+ return /* @__PURE__ */ jsx("button", {
1507
+ type: "button",
1508
+ onClick,
1509
+ title,
1510
+ style: {
1511
+ display: "flex",
1512
+ alignItems: "center",
1513
+ justifyContent: "center",
1514
+ width: "2.25rem",
1515
+ height: "2.25rem",
1516
+ background: "rgba(255,255,255,0.1)",
1517
+ border: "1px solid rgba(255,255,255,0.15)",
1518
+ borderRadius: "0.375rem",
1519
+ cursor: "pointer",
1520
+ color: "#e2e8f0",
1521
+ fontSize: "0.875rem",
1522
+ transition: "background 0.15s"
1523
+ },
1524
+ onMouseEnter: (e) => {
1525
+ e.currentTarget.style.background = "rgba(255,255,255,0.2)";
1526
+ },
1527
+ onMouseLeave: (e) => {
1528
+ e.currentTarget.style.background = "rgba(255,255,255,0.1)";
1529
+ },
1530
+ children
1531
+ });
1532
+ }
1533
+ //#endregion
1534
+ //#region src/GlobalLayer.tsx
1535
+ /**
1536
+ * Global overlay layer that persists across all slides.
1537
+ * Use for headers, footers, logos, watermarks, or progress bars.
1538
+ *
1539
+ * Place inside <Deck> to render on every slide.
1540
+ *
1541
+ * @example
1542
+ * ```tsx
1543
+ * <Deck>
1544
+ * <GlobalLayer position="above" style={{ bottom: 0 }}>
1545
+ * <footer>My Company</footer>
1546
+ * </GlobalLayer>
1547
+ * <Slide>...</Slide>
1548
+ * </Deck>
1549
+ * ```
1550
+ */
1551
+ function GlobalLayer({ children, position = "above", style }) {
1552
+ return /* @__PURE__ */ jsx("div", {
1553
+ className: `reslide-global-layer reslide-global-layer-${position}`,
1554
+ style: {
1555
+ position: "absolute",
1556
+ inset: 0,
1557
+ pointerEvents: "none",
1558
+ zIndex: position === "above" ? 40 : 0,
1559
+ ...style
1560
+ },
1561
+ children: /* @__PURE__ */ jsx("div", {
1562
+ style: { pointerEvents: "auto" },
1563
+ children
1564
+ })
1565
+ });
1566
+ }
1567
+ GlobalLayer.displayName = "GlobalLayer";
1568
+ GlobalLayer.__reslideGlobalLayer = true;
1569
+ //#endregion
1570
+ //#region src/Draggable.tsx
1571
+ /**
1572
+ * A draggable element within a slide.
1573
+ * Click and drag to reposition during presentation.
1574
+ */
1575
+ function Draggable({ children, x = 0, y = 0, style }) {
1576
+ const [pos, setPos] = useState({
1577
+ x: typeof x === "number" ? x : 0,
1578
+ y: typeof y === "number" ? y : 0
1579
+ });
1580
+ const dragRef = useRef(null);
1581
+ return /* @__PURE__ */ jsx("div", {
1582
+ className: "reslide-draggable",
1583
+ onMouseDown: useCallback((e) => {
1584
+ e.preventDefault();
1585
+ dragRef.current = {
1586
+ startX: e.clientX,
1587
+ startY: e.clientY,
1588
+ origX: pos.x,
1589
+ origY: pos.y
1590
+ };
1591
+ function onMouseMove(me) {
1592
+ if (!dragRef.current) return;
1593
+ setPos({
1594
+ x: dragRef.current.origX + (me.clientX - dragRef.current.startX),
1595
+ y: dragRef.current.origY + (me.clientY - dragRef.current.startY)
1596
+ });
1597
+ }
1598
+ function onMouseUp() {
1599
+ dragRef.current = null;
1600
+ window.removeEventListener("mousemove", onMouseMove);
1601
+ window.removeEventListener("mouseup", onMouseUp);
1602
+ }
1603
+ window.addEventListener("mousemove", onMouseMove);
1604
+ window.addEventListener("mouseup", onMouseUp);
1605
+ }, [pos.x, pos.y]),
1606
+ style: {
1607
+ position: "absolute",
1608
+ left: typeof x === "string" ? x : void 0,
1609
+ top: typeof y === "string" ? y : void 0,
1610
+ transform: `translate(${pos.x}px, ${pos.y}px)`,
1611
+ cursor: "grab",
1612
+ userSelect: "none",
1613
+ zIndex: 30,
1614
+ ...style
1615
+ },
1616
+ children
1617
+ });
1618
+ }
1619
+ //#endregion
1620
+ //#region src/Toc.tsx
1621
+ /**
1622
+ * Extract heading text (h1/h2) from a React element tree.
1623
+ * Traverses children recursively to find the first h1 or h2.
1624
+ */
1625
+ function extractHeading(node) {
1626
+ if (!isValidElement(node)) return null;
1627
+ const el = node;
1628
+ const type = el.type;
1629
+ if (type === "h1" || type === "h2") return extractText(el.props.children);
1630
+ const children = el.props.children;
1631
+ if (children == null) return null;
1632
+ let result = null;
1633
+ Children.forEach(children, (child) => {
1634
+ if (result) return;
1635
+ const found = extractHeading(child);
1636
+ if (found) result = found;
1637
+ });
1638
+ return result;
1639
+ }
1640
+ /** Extract plain text from a React node tree */
1641
+ function extractText(node) {
1642
+ if (node == null) return "";
1643
+ if (typeof node === "string") return node;
1644
+ if (typeof node === "number") return String(node);
1645
+ if (Array.isArray(node)) return node.map(extractText).join("");
1646
+ if (isValidElement(node)) return extractText(node.props.children);
1647
+ return "";
1648
+ }
1649
+ /**
1650
+ * Table of Contents component that renders a clickable list of slides
1651
+ * with their heading text extracted from h1/h2 elements.
1652
+ *
1653
+ * Must be rendered inside a `<Deck>` component.
1654
+ *
1655
+ * ```tsx
1656
+ * <Toc>
1657
+ * <Slide><h1>Introduction</h1></Slide>
1658
+ * <Slide><h2>Agenda</h2></Slide>
1659
+ * </Toc>
1660
+ * ```
1661
+ */
1662
+ function Toc({ children, className, style }) {
1663
+ const { currentSlide, goTo } = useDeck();
1664
+ const items = Children.toArray(children).map((slide, index) => {
1665
+ return {
1666
+ index,
1667
+ title: extractHeading(slide) || `Slide ${index + 1}`
1668
+ };
1669
+ });
1670
+ return /* @__PURE__ */ jsx("nav", {
1671
+ className: `reslide-toc${className ? ` ${className}` : ""}`,
1672
+ style: {
1673
+ display: "flex",
1674
+ flexDirection: "column",
1675
+ gap: "0.25rem",
1676
+ padding: "0.5rem 0",
1677
+ ...style
1678
+ },
1679
+ children: items.map((item) => {
1680
+ const isActive = item.index === currentSlide;
1681
+ return /* @__PURE__ */ jsxs("button", {
1682
+ type: "button",
1683
+ onClick: () => goTo(item.index),
1684
+ style: {
1685
+ display: "flex",
1686
+ alignItems: "center",
1687
+ gap: "0.5rem",
1688
+ padding: "0.5rem 1rem",
1689
+ border: "none",
1690
+ borderRadius: "0.375rem",
1691
+ cursor: "pointer",
1692
+ textAlign: "left",
1693
+ fontSize: "0.875rem",
1694
+ lineHeight: 1.4,
1695
+ fontFamily: "inherit",
1696
+ color: isActive ? "var(--toc-active-text, var(--slide-accent, #3b82f6))" : "var(--toc-text, var(--slide-text, #1a1a1a))",
1697
+ backgroundColor: isActive ? "var(--toc-active-bg, rgba(59, 130, 246, 0.1))" : "transparent",
1698
+ fontWeight: isActive ? 600 : 400,
1699
+ transition: "background-color 0.15s, color 0.15s"
1700
+ },
1701
+ onMouseEnter: (e) => {
1702
+ if (!isActive) e.currentTarget.style.backgroundColor = "var(--toc-hover-bg, rgba(0, 0, 0, 0.05))";
1703
+ },
1704
+ onMouseLeave: (e) => {
1705
+ e.currentTarget.style.backgroundColor = isActive ? "var(--toc-active-bg, rgba(59, 130, 246, 0.1))" : "transparent";
1706
+ },
1707
+ children: [/* @__PURE__ */ jsx("span", {
1708
+ style: {
1709
+ minWidth: "1.5rem",
1710
+ textAlign: "right",
1711
+ opacity: .5,
1712
+ fontSize: "0.75rem",
1713
+ fontVariantNumeric: "tabular-nums"
1714
+ },
1715
+ children: item.index + 1
1716
+ }), /* @__PURE__ */ jsx("span", { children: item.title })]
1717
+ }, item.index);
1718
+ })
1719
+ });
1720
+ }
1721
+ //#endregion
1722
+ //#region src/Mermaid.tsx
1723
+ /**
1724
+ * Renders a Mermaid diagram.
1725
+ *
1726
+ * Usage in MDX:
1727
+ * ```mdx
1728
+ * <Mermaid>
1729
+ * graph TD
1730
+ * A --> B
1731
+ * </Mermaid>
1732
+ * ```
1733
+ *
1734
+ * The mermaid library is dynamically imported on the client side,
1735
+ * so it does not need to be installed as a project dependency.
1736
+ */
1737
+ function Mermaid({ children }) {
1738
+ const containerRef = useRef(null);
1739
+ const [svg, setSvg] = useState("");
1740
+ const [error, setError] = useState("");
1741
+ const id = useId().replace(/:/g, "_");
1742
+ useEffect(() => {
1743
+ let cancelled = false;
1744
+ async function render() {
1745
+ try {
1746
+ const mermaid = await import("mermaid");
1747
+ mermaid.default.initialize({
1748
+ startOnLoad: false,
1749
+ theme: "default",
1750
+ securityLevel: "loose"
1751
+ });
1752
+ const code = typeof children === "string" ? children.trim() : "";
1753
+ if (!code) return;
1754
+ const { svg: rendered } = await mermaid.default.render(`mermaid-${id}`, code);
1755
+ if (!cancelled) {
1756
+ setSvg(rendered);
1757
+ setError("");
1758
+ }
1759
+ } catch (err) {
1760
+ if (!cancelled) setError(err instanceof Error ? err.message : "Failed to render diagram");
1761
+ }
1762
+ }
1763
+ render();
1764
+ return () => {
1765
+ cancelled = true;
1766
+ };
1767
+ }, [children, id]);
1768
+ if (error) return /* @__PURE__ */ jsx("div", {
1769
+ className: "reslide-mermaid reslide-mermaid--error",
1770
+ children: /* @__PURE__ */ jsx("pre", { children: error })
1771
+ });
1772
+ return /* @__PURE__ */ jsx("div", {
1773
+ ref: containerRef,
1774
+ className: "reslide-mermaid",
1775
+ dangerouslySetInnerHTML: { __html: svg }
1776
+ });
1777
+ }
1778
+ //#endregion
1779
+ export { useDeck as C, DeckContext as S, useSlideIndex as _, PresenterView as a, openPresenterWindow as b, Mark as c, Slide as d, Deck as f, SlideIndexContext as g, PrintView as h, GlobalLayer as i, Click as l, ProgressBar as m, Toc as n, SlotRight as o, SlideNumber as p, Draggable as r, Notes as s, Mermaid as t, ClickSteps as u, NavigationBar as v, ClickNavigation as w, DrawingLayer as x, isPresenterView as y };