@reslide-dev/core 0.2.0 → 0.2.1

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,1749 @@
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/SlideTransition.tsx
674
+ const DURATION = 300;
675
+ function SlideTransition({ children, currentSlide, transition }) {
676
+ const slides = Children.toArray(children);
677
+ const [displaySlide, setDisplaySlide] = useState(currentSlide);
678
+ const [prevSlide, setPrevSlide] = useState(null);
679
+ const [isAnimating, setIsAnimating] = useState(false);
680
+ const prevCurrentRef = useRef(currentSlide);
681
+ const timerRef = useRef(null);
682
+ useEffect(() => {
683
+ if (currentSlide === prevCurrentRef.current) return;
684
+ const from = prevCurrentRef.current;
685
+ prevCurrentRef.current = currentSlide;
686
+ if (transition === "none") {
687
+ setDisplaySlide(currentSlide);
688
+ return;
689
+ }
690
+ setPrevSlide(from);
691
+ setDisplaySlide(currentSlide);
692
+ setIsAnimating(true);
693
+ if (timerRef.current) clearTimeout(timerRef.current);
694
+ timerRef.current = setTimeout(() => {
695
+ setIsAnimating(false);
696
+ setPrevSlide(null);
697
+ }, DURATION);
698
+ return () => {
699
+ if (timerRef.current) clearTimeout(timerRef.current);
700
+ };
701
+ }, [currentSlide, transition]);
702
+ const resolvedTransition = resolveTransition(transition, prevSlide, displaySlide);
703
+ if (transition === "none" || !isAnimating) return /* @__PURE__ */ jsx("div", {
704
+ className: "reslide-transition-container",
705
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
706
+ value: displaySlide,
707
+ children: /* @__PURE__ */ jsx("div", {
708
+ className: "reslide-transition-slide",
709
+ children: slides[displaySlide]
710
+ })
711
+ })
712
+ });
713
+ return /* @__PURE__ */ jsxs("div", {
714
+ className: "reslide-transition-container",
715
+ children: [prevSlide != null && /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
716
+ value: prevSlide,
717
+ children: /* @__PURE__ */ jsx("div", {
718
+ className: `reslide-transition-slide reslide-transition-${resolvedTransition}-exit`,
719
+ children: slides[prevSlide]
720
+ })
721
+ }), /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
722
+ value: displaySlide,
723
+ children: /* @__PURE__ */ jsx("div", {
724
+ className: `reslide-transition-slide reslide-transition-${resolvedTransition}-enter`,
725
+ children: slides[displaySlide]
726
+ })
727
+ })]
728
+ });
729
+ }
730
+ function resolveTransition(transition, from, to) {
731
+ if (transition !== "slide-left" && transition !== "slide-right") return transition;
732
+ if (from == null) return transition;
733
+ const goingForward = to > from;
734
+ if (transition === "slide-left") return goingForward ? "slide-left" : "slide-right";
735
+ return goingForward ? "slide-right" : "slide-left";
736
+ }
737
+ //#endregion
738
+ //#region src/use-fullscreen.ts
739
+ function useFullscreen(ref) {
740
+ const [isFullscreen, setIsFullscreen] = useState(false);
741
+ useEffect(() => {
742
+ function handleChange() {
743
+ setIsFullscreen(document.fullscreenElement != null);
744
+ }
745
+ document.addEventListener("fullscreenchange", handleChange);
746
+ return () => document.removeEventListener("fullscreenchange", handleChange);
747
+ }, []);
748
+ return {
749
+ isFullscreen,
750
+ toggleFullscreen: useCallback(() => {
751
+ if (!ref.current) return;
752
+ if (document.fullscreenElement) document.exitFullscreen();
753
+ else ref.current.requestFullscreen();
754
+ }, [ref])
755
+ };
756
+ }
757
+ //#endregion
758
+ //#region src/Deck.tsx
759
+ function Deck({ children, transition = "none" }) {
760
+ const containerRef = useRef(null);
761
+ const [currentSlide, setCurrentSlide] = useState(0);
762
+ const [clickStep, setClickStep] = useState(0);
763
+ const [isOverview, setIsOverview] = useState(false);
764
+ const [isDrawing, setIsDrawing] = useState(false);
765
+ const [isPrinting, setIsPrinting] = useState(false);
766
+ const [clickStepsMap, setClickStepsMap] = useState({});
767
+ const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
768
+ const totalSlides = Children.count(children);
769
+ const totalClickSteps = clickStepsMap[currentSlide] ?? 0;
770
+ const registerClickSteps = useCallback((slideIndex, count) => {
771
+ setClickStepsMap((prev) => {
772
+ if (prev[slideIndex] === count) return prev;
773
+ return {
774
+ ...prev,
775
+ [slideIndex]: count
776
+ };
777
+ });
778
+ }, []);
779
+ const next = useCallback(() => {
780
+ if (isOverview) return;
781
+ if (clickStep < totalClickSteps) setClickStep((s) => s + 1);
782
+ else if (currentSlide < totalSlides - 1) {
783
+ setCurrentSlide((s) => s + 1);
784
+ setClickStep(0);
785
+ }
786
+ }, [
787
+ isOverview,
788
+ clickStep,
789
+ totalClickSteps,
790
+ currentSlide,
791
+ totalSlides
792
+ ]);
793
+ const prev = useCallback(() => {
794
+ if (isOverview) return;
795
+ if (clickStep > 0) setClickStep((s) => s - 1);
796
+ else if (currentSlide > 0) {
797
+ const prevSlide = currentSlide - 1;
798
+ setCurrentSlide(prevSlide);
799
+ setClickStep(clickStepsMap[prevSlide] ?? 0);
800
+ }
801
+ }, [
802
+ isOverview,
803
+ clickStep,
804
+ currentSlide,
805
+ clickStepsMap
806
+ ]);
807
+ const goTo = useCallback((slideIndex) => {
808
+ if (slideIndex >= 0 && slideIndex < totalSlides) {
809
+ setCurrentSlide(slideIndex);
810
+ setClickStep(0);
811
+ if (isOverview) setIsOverview(false);
812
+ }
813
+ }, [totalSlides, isOverview]);
814
+ usePresenterSync(currentSlide, clickStep, useMemo(() => ({
815
+ next,
816
+ prev,
817
+ goTo
818
+ }), [
819
+ next,
820
+ prev,
821
+ goTo
822
+ ]));
823
+ const toggleOverview = useCallback(() => {
824
+ setIsOverview((v) => !v);
825
+ }, []);
826
+ useEffect(() => {
827
+ function handleKeyDown(e) {
828
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
829
+ switch (e.key) {
830
+ case "ArrowRight":
831
+ case " ":
832
+ e.preventDefault();
833
+ next();
834
+ break;
835
+ case "ArrowLeft":
836
+ e.preventDefault();
837
+ prev();
838
+ break;
839
+ case "Escape":
840
+ if (!document.fullscreenElement) {
841
+ e.preventDefault();
842
+ toggleOverview();
843
+ }
844
+ break;
845
+ case "f":
846
+ e.preventDefault();
847
+ toggleFullscreen();
848
+ break;
849
+ case "p":
850
+ e.preventDefault();
851
+ openPresenterWindow();
852
+ break;
853
+ case "d":
854
+ e.preventDefault();
855
+ setIsDrawing((v) => !v);
856
+ break;
857
+ }
858
+ }
859
+ window.addEventListener("keydown", handleKeyDown);
860
+ return () => window.removeEventListener("keydown", handleKeyDown);
861
+ }, [
862
+ next,
863
+ prev,
864
+ toggleOverview,
865
+ toggleFullscreen
866
+ ]);
867
+ useEffect(() => {
868
+ function onBeforePrint() {
869
+ setIsPrinting(true);
870
+ }
871
+ function onAfterPrint() {
872
+ setIsPrinting(false);
873
+ }
874
+ window.addEventListener("beforeprint", onBeforePrint);
875
+ window.addEventListener("afterprint", onAfterPrint);
876
+ return () => {
877
+ window.removeEventListener("beforeprint", onBeforePrint);
878
+ window.removeEventListener("afterprint", onAfterPrint);
879
+ };
880
+ }, []);
881
+ const contextValue = useMemo(() => ({
882
+ currentSlide,
883
+ totalSlides,
884
+ clickStep,
885
+ totalClickSteps,
886
+ isOverview,
887
+ isFullscreen,
888
+ next,
889
+ prev,
890
+ goTo,
891
+ toggleOverview,
892
+ toggleFullscreen,
893
+ registerClickSteps
894
+ }), [
895
+ currentSlide,
896
+ totalSlides,
897
+ clickStep,
898
+ totalClickSteps,
899
+ isOverview,
900
+ isFullscreen,
901
+ next,
902
+ prev,
903
+ goTo,
904
+ toggleOverview,
905
+ toggleFullscreen,
906
+ registerClickSteps
907
+ ]);
908
+ return /* @__PURE__ */ jsx(DeckContext.Provider, {
909
+ value: contextValue,
910
+ children: /* @__PURE__ */ jsxs("div", {
911
+ ref: containerRef,
912
+ className: "reslide-deck",
913
+ style: {
914
+ position: "relative",
915
+ width: "100%",
916
+ height: "100%",
917
+ overflow: "hidden",
918
+ backgroundColor: "var(--slide-bg, #fff)",
919
+ color: "var(--slide-text, #1a1a1a)"
920
+ },
921
+ children: [
922
+ isPrinting ? /* @__PURE__ */ jsx(PrintView, { children }) : isOverview ? /* @__PURE__ */ jsx(OverviewGrid, {
923
+ totalSlides,
924
+ goTo,
925
+ children
926
+ }) : /* @__PURE__ */ jsx(SlideTransition, {
927
+ currentSlide,
928
+ transition,
929
+ children
930
+ }),
931
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(ClickNavigation, {
932
+ onPrev: prev,
933
+ onNext: next,
934
+ disabled: isDrawing
935
+ }),
936
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(ProgressBar, {}),
937
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, {
938
+ active: isDrawing,
939
+ currentSlide
940
+ }),
941
+ !isPrinting && /* @__PURE__ */ jsx(NavigationBar, {
942
+ isDrawing,
943
+ onToggleDrawing: () => setIsDrawing((v) => !v)
944
+ })
945
+ ]
946
+ })
947
+ });
948
+ }
949
+ function OverviewGrid({ children, totalSlides, goTo }) {
950
+ const slides = Children.toArray(children);
951
+ return /* @__PURE__ */ jsx("div", {
952
+ className: "reslide-overview",
953
+ style: {
954
+ display: "grid",
955
+ gridTemplateColumns: `repeat(${Math.ceil(Math.sqrt(totalSlides))}, 1fr)`,
956
+ gap: "1rem",
957
+ padding: "1rem",
958
+ width: "100%",
959
+ height: "100%",
960
+ overflow: "auto"
961
+ },
962
+ children: slides.map((slide, i) => /* @__PURE__ */ jsx("button", {
963
+ type: "button",
964
+ onClick: () => goTo(i),
965
+ style: {
966
+ border: "1px solid var(--slide-accent, #3b82f6)",
967
+ borderRadius: "0.5rem",
968
+ overflow: "hidden",
969
+ cursor: "pointer",
970
+ background: "var(--slide-bg, #fff)",
971
+ aspectRatio: "16 / 9",
972
+ padding: 0,
973
+ position: "relative"
974
+ },
975
+ children: /* @__PURE__ */ jsx("div", {
976
+ style: {
977
+ transform: "scale(0.25)",
978
+ transformOrigin: "top left",
979
+ width: "400%",
980
+ height: "400%",
981
+ pointerEvents: "none"
982
+ },
983
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
984
+ value: i,
985
+ children: slide
986
+ })
987
+ })
988
+ }, i))
989
+ });
990
+ }
991
+ //#endregion
992
+ //#region src/Slide.tsx
993
+ const baseStyle = {
994
+ width: "100%",
995
+ height: "100%",
996
+ display: "flex",
997
+ boxSizing: "border-box"
998
+ };
999
+ function isSlotRight(child) {
1000
+ return isValidElement(child) && typeof child.type === "function" && "__reslideSlot" in child.type && child.type.__reslideSlot === "right";
1001
+ }
1002
+ function splitChildren(children) {
1003
+ const left = [];
1004
+ const right = [];
1005
+ let inRight = false;
1006
+ Children.forEach(children, (child) => {
1007
+ if (isSlotRight(child)) {
1008
+ inRight = true;
1009
+ right.push(child.props.children);
1010
+ return;
1011
+ }
1012
+ if (inRight) right.push(child);
1013
+ else left.push(child);
1014
+ });
1015
+ return {
1016
+ left,
1017
+ right
1018
+ };
1019
+ }
1020
+ function Slide({ children, layout = "default", image, className, style }) {
1021
+ const cls = `reslide-slide reslide-layout-${layout}${className ? ` ${className}` : ""}`;
1022
+ switch (layout) {
1023
+ case "center": return /* @__PURE__ */ jsx("div", {
1024
+ className: cls,
1025
+ style: {
1026
+ ...baseStyle,
1027
+ flexDirection: "column",
1028
+ justifyContent: "center",
1029
+ alignItems: "center",
1030
+ textAlign: "center",
1031
+ padding: "3rem 4rem",
1032
+ ...style
1033
+ },
1034
+ children
1035
+ });
1036
+ case "two-cols": {
1037
+ const { left, right } = splitChildren(children);
1038
+ return /* @__PURE__ */ jsxs("div", {
1039
+ className: cls,
1040
+ style: {
1041
+ ...baseStyle,
1042
+ flexDirection: "row",
1043
+ gap: "2rem",
1044
+ padding: "3rem 4rem",
1045
+ ...style
1046
+ },
1047
+ children: [/* @__PURE__ */ jsx("div", {
1048
+ style: {
1049
+ flex: 1,
1050
+ minWidth: 0
1051
+ },
1052
+ children: left
1053
+ }), /* @__PURE__ */ jsx("div", {
1054
+ style: {
1055
+ flex: 1,
1056
+ minWidth: 0
1057
+ },
1058
+ children: right
1059
+ })]
1060
+ });
1061
+ }
1062
+ case "image-right": return /* @__PURE__ */ jsxs("div", {
1063
+ className: cls,
1064
+ style: {
1065
+ ...baseStyle,
1066
+ flexDirection: "row",
1067
+ ...style
1068
+ },
1069
+ children: [/* @__PURE__ */ jsx("div", {
1070
+ style: {
1071
+ flex: 1,
1072
+ padding: "3rem 2rem 3rem 4rem",
1073
+ overflow: "auto"
1074
+ },
1075
+ children
1076
+ }), image && /* @__PURE__ */ jsx("div", { style: {
1077
+ flex: 1,
1078
+ backgroundImage: `url(${image})`,
1079
+ backgroundSize: "cover",
1080
+ backgroundPosition: "center"
1081
+ } })]
1082
+ });
1083
+ case "image-left": return /* @__PURE__ */ jsxs("div", {
1084
+ className: cls,
1085
+ style: {
1086
+ ...baseStyle,
1087
+ flexDirection: "row",
1088
+ ...style
1089
+ },
1090
+ children: [image && /* @__PURE__ */ jsx("div", { style: {
1091
+ flex: 1,
1092
+ backgroundImage: `url(${image})`,
1093
+ backgroundSize: "cover",
1094
+ backgroundPosition: "center"
1095
+ } }), /* @__PURE__ */ jsx("div", {
1096
+ style: {
1097
+ flex: 1,
1098
+ padding: "3rem 4rem 3rem 2rem",
1099
+ overflow: "auto"
1100
+ },
1101
+ children
1102
+ })]
1103
+ });
1104
+ case "section": return /* @__PURE__ */ jsx("div", {
1105
+ className: cls,
1106
+ style: {
1107
+ ...baseStyle,
1108
+ flexDirection: "column",
1109
+ justifyContent: "center",
1110
+ alignItems: "center",
1111
+ textAlign: "center",
1112
+ padding: "3rem 4rem",
1113
+ backgroundColor: "var(--slide-accent, #3b82f6)",
1114
+ color: "var(--slide-section-text, #fff)",
1115
+ ...style
1116
+ },
1117
+ children
1118
+ });
1119
+ case "quote": return /* @__PURE__ */ jsx("div", {
1120
+ className: cls,
1121
+ style: {
1122
+ ...baseStyle,
1123
+ flexDirection: "column",
1124
+ justifyContent: "center",
1125
+ padding: "3rem 6rem",
1126
+ ...style
1127
+ },
1128
+ children: /* @__PURE__ */ jsx("blockquote", {
1129
+ style: {
1130
+ fontSize: "1.5em",
1131
+ fontStyle: "italic",
1132
+ borderLeft: "4px solid var(--slide-accent, #3b82f6)",
1133
+ paddingLeft: "1.5rem",
1134
+ margin: 0
1135
+ },
1136
+ children
1137
+ })
1138
+ });
1139
+ default: return /* @__PURE__ */ jsx("div", {
1140
+ className: cls,
1141
+ style: {
1142
+ ...baseStyle,
1143
+ flexDirection: "column",
1144
+ padding: "3rem 4rem",
1145
+ ...style
1146
+ },
1147
+ children
1148
+ });
1149
+ }
1150
+ }
1151
+ //#endregion
1152
+ //#region src/Click.tsx
1153
+ function Click({ children, at }) {
1154
+ const { clickStep } = useDeck();
1155
+ const visible = clickStep >= (at ?? 1);
1156
+ return /* @__PURE__ */ jsx("div", {
1157
+ className: "reslide-click",
1158
+ style: {
1159
+ opacity: visible ? 1 : 0,
1160
+ visibility: visible ? "visible" : "hidden",
1161
+ pointerEvents: visible ? "auto" : "none",
1162
+ transition: "opacity 0.3s ease, visibility 0.3s ease"
1163
+ },
1164
+ children
1165
+ });
1166
+ }
1167
+ /**
1168
+ * Register click steps for the current slide.
1169
+ * Automatically reads the slide index from SlideIndexContext.
1170
+ * If slideIndex prop is provided, it takes precedence (for backwards compatibility).
1171
+ */
1172
+ function ClickSteps({ count, slideIndex }) {
1173
+ const { registerClickSteps } = useDeck();
1174
+ const contextIndex = useContext(SlideIndexContext);
1175
+ const resolvedIndex = slideIndex ?? contextIndex;
1176
+ useEffect(() => {
1177
+ if (resolvedIndex != null) registerClickSteps(resolvedIndex, count);
1178
+ }, [
1179
+ resolvedIndex,
1180
+ count,
1181
+ registerClickSteps
1182
+ ]);
1183
+ return null;
1184
+ }
1185
+ //#endregion
1186
+ //#region src/Mark.tsx
1187
+ const markStyles = {
1188
+ highlight: (colorName, resolvedColor) => ({
1189
+ backgroundColor: `var(--mark-${colorName}, ${resolvedColor})`,
1190
+ padding: "0.1em 0.2em",
1191
+ borderRadius: "0.2em"
1192
+ }),
1193
+ underline: (colorName, resolvedColor) => ({
1194
+ textDecoration: "underline",
1195
+ textDecorationColor: `var(--mark-${colorName}, ${resolvedColor})`,
1196
+ textDecorationThickness: "0.15em",
1197
+ textUnderlineOffset: "0.15em"
1198
+ }),
1199
+ circle: (colorName, resolvedColor) => ({
1200
+ border: `0.15em solid var(--mark-${colorName}, ${resolvedColor})`,
1201
+ borderRadius: "50%",
1202
+ padding: "0.1em 0.3em"
1203
+ })
1204
+ };
1205
+ const defaultColors = {
1206
+ orange: "#fb923c",
1207
+ red: "#ef4444",
1208
+ blue: "#3b82f6",
1209
+ green: "#22c55e",
1210
+ yellow: "#facc15",
1211
+ purple: "#a855f7"
1212
+ };
1213
+ function Mark({ children, type = "highlight", color = "yellow" }) {
1214
+ const resolvedColor = defaultColors[color] ?? color;
1215
+ const styleFn = markStyles[type] ?? markStyles.highlight;
1216
+ return /* @__PURE__ */ jsx("span", {
1217
+ className: `reslide-mark reslide-mark-${type}`,
1218
+ style: styleFn(color, resolvedColor),
1219
+ children
1220
+ });
1221
+ }
1222
+ //#endregion
1223
+ //#region src/Notes.tsx
1224
+ /**
1225
+ * Speaker notes. Hidden during normal presentation,
1226
+ * visible in overview mode.
1227
+ */
1228
+ function Notes({ children }) {
1229
+ const { isOverview } = useDeck();
1230
+ if (!isOverview) return null;
1231
+ return /* @__PURE__ */ jsx("div", {
1232
+ className: "reslide-notes",
1233
+ style: {
1234
+ marginTop: "auto",
1235
+ padding: "0.75rem",
1236
+ fontSize: "0.75rem",
1237
+ color: "var(--slide-text, #1a1a1a)",
1238
+ opacity: .7,
1239
+ borderTop: "1px solid currentColor"
1240
+ },
1241
+ children
1242
+ });
1243
+ }
1244
+ //#endregion
1245
+ //#region src/Slot.tsx
1246
+ /**
1247
+ * Marks content as belonging to the right column in a two-cols layout.
1248
+ * Used by remarkSlides to separate `::right` content.
1249
+ */
1250
+ function SlotRight({ children }) {
1251
+ return /* @__PURE__ */ jsx(Fragment$1, { children });
1252
+ }
1253
+ SlotRight.displayName = "SlotRight";
1254
+ SlotRight.__reslideSlot = "right";
1255
+ //#endregion
1256
+ //#region src/PresenterView.tsx
1257
+ /**
1258
+ * Presenter view that syncs with the main presentation window.
1259
+ * Shows: current slide, next slide preview, notes, and timer.
1260
+ * Supports bidirectional control — navigate from this window to
1261
+ * drive the main presentation.
1262
+ */
1263
+ function PresenterView({ children, notes }) {
1264
+ const [currentSlide, setCurrentSlide] = useState(0);
1265
+ const [clickStep, setClickStep] = useState(0);
1266
+ const [elapsed, setElapsed] = useState(0);
1267
+ const slides = Children.toArray(children);
1268
+ const totalSlides = slides.length;
1269
+ const { next, prev, goTo } = usePresenterChannel((msg) => {
1270
+ setCurrentSlide(msg.currentSlide);
1271
+ setClickStep(msg.clickStep);
1272
+ });
1273
+ useEffect(() => {
1274
+ function handleKeyDown(e) {
1275
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
1276
+ switch (e.key) {
1277
+ case "ArrowRight":
1278
+ case " ":
1279
+ e.preventDefault();
1280
+ next();
1281
+ break;
1282
+ case "ArrowLeft":
1283
+ e.preventDefault();
1284
+ prev();
1285
+ break;
1286
+ }
1287
+ }
1288
+ window.addEventListener("keydown", handleKeyDown);
1289
+ return () => window.removeEventListener("keydown", handleKeyDown);
1290
+ }, [next, prev]);
1291
+ useEffect(() => {
1292
+ const start = Date.now();
1293
+ const interval = setInterval(() => {
1294
+ setElapsed(Math.floor((Date.now() - start) / 1e3));
1295
+ }, 1e3);
1296
+ return () => clearInterval(interval);
1297
+ }, []);
1298
+ const formatTime = (seconds) => {
1299
+ const m = Math.floor(seconds / 60);
1300
+ const s = seconds % 60;
1301
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
1302
+ };
1303
+ const noopReg = useCallback((_i, _c) => {}, []);
1304
+ const noop = useCallback(() => {}, []);
1305
+ const contextValue = useMemo(() => ({
1306
+ currentSlide,
1307
+ totalSlides,
1308
+ clickStep,
1309
+ totalClickSteps: 0,
1310
+ isOverview: false,
1311
+ isFullscreen: false,
1312
+ next,
1313
+ prev,
1314
+ goTo,
1315
+ toggleOverview: noop,
1316
+ toggleFullscreen: noop,
1317
+ registerClickSteps: noopReg
1318
+ }), [
1319
+ currentSlide,
1320
+ totalSlides,
1321
+ clickStep,
1322
+ next,
1323
+ prev,
1324
+ goTo,
1325
+ noop,
1326
+ noopReg
1327
+ ]);
1328
+ return /* @__PURE__ */ jsx(DeckContext.Provider, {
1329
+ value: contextValue,
1330
+ children: /* @__PURE__ */ jsxs("div", {
1331
+ style: {
1332
+ display: "grid",
1333
+ gridTemplateColumns: "2fr 1fr",
1334
+ gridTemplateRows: "1fr auto",
1335
+ width: "100%",
1336
+ height: "100%",
1337
+ gap: "0.75rem",
1338
+ padding: "0.75rem",
1339
+ backgroundColor: "#1a1a2e",
1340
+ color: "#e2e8f0",
1341
+ fontFamily: "system-ui, sans-serif",
1342
+ boxSizing: "border-box"
1343
+ },
1344
+ children: [
1345
+ /* @__PURE__ */ jsx("div", {
1346
+ style: {
1347
+ border: "2px solid #3b82f6",
1348
+ borderRadius: "0.5rem",
1349
+ overflow: "hidden",
1350
+ position: "relative",
1351
+ backgroundColor: "var(--slide-bg, #fff)",
1352
+ color: "var(--slide-text, #1a1a1a)"
1353
+ },
1354
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
1355
+ value: currentSlide,
1356
+ children: slides[currentSlide]
1357
+ })
1358
+ }),
1359
+ /* @__PURE__ */ jsxs("div", {
1360
+ style: {
1361
+ display: "flex",
1362
+ flexDirection: "column",
1363
+ gap: "0.75rem",
1364
+ minHeight: 0
1365
+ },
1366
+ children: [/* @__PURE__ */ jsx("div", {
1367
+ style: {
1368
+ flex: "0 0 40%",
1369
+ border: "1px solid #334155",
1370
+ borderRadius: "0.5rem",
1371
+ overflow: "hidden",
1372
+ opacity: .8,
1373
+ backgroundColor: "var(--slide-bg, #fff)",
1374
+ color: "var(--slide-text, #1a1a1a)"
1375
+ },
1376
+ children: currentSlide < totalSlides - 1 && /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
1377
+ value: currentSlide + 1,
1378
+ children: /* @__PURE__ */ jsx("div", {
1379
+ style: {
1380
+ transform: "scale(0.5)",
1381
+ transformOrigin: "top left",
1382
+ width: "200%",
1383
+ height: "200%"
1384
+ },
1385
+ children: slides[currentSlide + 1]
1386
+ })
1387
+ })
1388
+ }), /* @__PURE__ */ jsxs("div", {
1389
+ style: {
1390
+ flex: 1,
1391
+ overflow: "auto",
1392
+ padding: "1rem",
1393
+ backgroundColor: "#0f172a",
1394
+ borderRadius: "0.5rem",
1395
+ fontSize: "0.875rem",
1396
+ lineHeight: 1.6
1397
+ },
1398
+ children: [/* @__PURE__ */ jsx("div", {
1399
+ style: {
1400
+ fontWeight: 600,
1401
+ marginBottom: "0.5rem",
1402
+ color: "#94a3b8"
1403
+ },
1404
+ children: "Notes"
1405
+ }), notes?.[currentSlide] ?? /* @__PURE__ */ jsx("span", {
1406
+ style: { color: "#64748b" },
1407
+ children: "No notes for this slide"
1408
+ })]
1409
+ })]
1410
+ }),
1411
+ /* @__PURE__ */ jsxs("div", {
1412
+ style: {
1413
+ gridColumn: "1 / -1",
1414
+ display: "flex",
1415
+ justifyContent: "space-between",
1416
+ alignItems: "center",
1417
+ padding: "0.5rem 1rem",
1418
+ backgroundColor: "#0f172a",
1419
+ borderRadius: "0.5rem"
1420
+ },
1421
+ children: [
1422
+ /* @__PURE__ */ jsx("div", {
1423
+ style: {
1424
+ fontSize: "1.5rem",
1425
+ fontVariantNumeric: "tabular-nums",
1426
+ fontWeight: 700
1427
+ },
1428
+ children: formatTime(elapsed)
1429
+ }),
1430
+ /* @__PURE__ */ jsxs("div", {
1431
+ style: {
1432
+ display: "flex",
1433
+ alignItems: "center",
1434
+ gap: "0.5rem"
1435
+ },
1436
+ children: [
1437
+ /* @__PURE__ */ jsx(PresenterNavButton, {
1438
+ onClick: prev,
1439
+ title: "Previous (←)",
1440
+ children: "◀"
1441
+ }),
1442
+ /* @__PURE__ */ jsxs("span", {
1443
+ style: {
1444
+ fontSize: "1.125rem",
1445
+ fontVariantNumeric: "tabular-nums"
1446
+ },
1447
+ children: [
1448
+ currentSlide + 1,
1449
+ " / ",
1450
+ totalSlides,
1451
+ clickStep > 0 && /* @__PURE__ */ jsxs("span", {
1452
+ style: { color: "#94a3b8" },
1453
+ children: [
1454
+ " (click ",
1455
+ clickStep,
1456
+ ")"
1457
+ ]
1458
+ })
1459
+ ]
1460
+ }),
1461
+ /* @__PURE__ */ jsx(PresenterNavButton, {
1462
+ onClick: next,
1463
+ title: "Next (→ / Space)",
1464
+ children: "▶"
1465
+ })
1466
+ ]
1467
+ }),
1468
+ /* @__PURE__ */ jsx("div", { style: { width: "5rem" } })
1469
+ ]
1470
+ })
1471
+ ]
1472
+ })
1473
+ });
1474
+ }
1475
+ function PresenterNavButton({ children, onClick, title }) {
1476
+ return /* @__PURE__ */ jsx("button", {
1477
+ type: "button",
1478
+ onClick,
1479
+ title,
1480
+ style: {
1481
+ display: "flex",
1482
+ alignItems: "center",
1483
+ justifyContent: "center",
1484
+ width: "2.25rem",
1485
+ height: "2.25rem",
1486
+ background: "rgba(255,255,255,0.1)",
1487
+ border: "1px solid rgba(255,255,255,0.15)",
1488
+ borderRadius: "0.375rem",
1489
+ cursor: "pointer",
1490
+ color: "#e2e8f0",
1491
+ fontSize: "0.875rem",
1492
+ transition: "background 0.15s"
1493
+ },
1494
+ onMouseEnter: (e) => {
1495
+ e.currentTarget.style.background = "rgba(255,255,255,0.2)";
1496
+ },
1497
+ onMouseLeave: (e) => {
1498
+ e.currentTarget.style.background = "rgba(255,255,255,0.1)";
1499
+ },
1500
+ children
1501
+ });
1502
+ }
1503
+ //#endregion
1504
+ //#region src/GlobalLayer.tsx
1505
+ /**
1506
+ * Global overlay layer that persists across all slides.
1507
+ * Use for headers, footers, logos, watermarks, or progress bars.
1508
+ *
1509
+ * Place inside <Deck> to render on every slide.
1510
+ *
1511
+ * @example
1512
+ * ```tsx
1513
+ * <Deck>
1514
+ * <GlobalLayer position="above" style={{ bottom: 0 }}>
1515
+ * <footer>My Company</footer>
1516
+ * </GlobalLayer>
1517
+ * <Slide>...</Slide>
1518
+ * </Deck>
1519
+ * ```
1520
+ */
1521
+ function GlobalLayer({ children, position = "above", style }) {
1522
+ return /* @__PURE__ */ jsx("div", {
1523
+ className: `reslide-global-layer reslide-global-layer-${position}`,
1524
+ style: {
1525
+ position: "absolute",
1526
+ inset: 0,
1527
+ pointerEvents: "none",
1528
+ zIndex: position === "above" ? 40 : 0,
1529
+ ...style
1530
+ },
1531
+ children: /* @__PURE__ */ jsx("div", {
1532
+ style: { pointerEvents: "auto" },
1533
+ children
1534
+ })
1535
+ });
1536
+ }
1537
+ GlobalLayer.displayName = "GlobalLayer";
1538
+ GlobalLayer.__reslideGlobalLayer = true;
1539
+ //#endregion
1540
+ //#region src/Draggable.tsx
1541
+ /**
1542
+ * A draggable element within a slide.
1543
+ * Click and drag to reposition during presentation.
1544
+ */
1545
+ function Draggable({ children, x = 0, y = 0, style }) {
1546
+ const [pos, setPos] = useState({
1547
+ x: typeof x === "number" ? x : 0,
1548
+ y: typeof y === "number" ? y : 0
1549
+ });
1550
+ const dragRef = useRef(null);
1551
+ return /* @__PURE__ */ jsx("div", {
1552
+ className: "reslide-draggable",
1553
+ onMouseDown: useCallback((e) => {
1554
+ e.preventDefault();
1555
+ dragRef.current = {
1556
+ startX: e.clientX,
1557
+ startY: e.clientY,
1558
+ origX: pos.x,
1559
+ origY: pos.y
1560
+ };
1561
+ function onMouseMove(me) {
1562
+ if (!dragRef.current) return;
1563
+ setPos({
1564
+ x: dragRef.current.origX + (me.clientX - dragRef.current.startX),
1565
+ y: dragRef.current.origY + (me.clientY - dragRef.current.startY)
1566
+ });
1567
+ }
1568
+ function onMouseUp() {
1569
+ dragRef.current = null;
1570
+ window.removeEventListener("mousemove", onMouseMove);
1571
+ window.removeEventListener("mouseup", onMouseUp);
1572
+ }
1573
+ window.addEventListener("mousemove", onMouseMove);
1574
+ window.addEventListener("mouseup", onMouseUp);
1575
+ }, [pos.x, pos.y]),
1576
+ style: {
1577
+ position: "absolute",
1578
+ left: typeof x === "string" ? x : void 0,
1579
+ top: typeof y === "string" ? y : void 0,
1580
+ transform: `translate(${pos.x}px, ${pos.y}px)`,
1581
+ cursor: "grab",
1582
+ userSelect: "none",
1583
+ zIndex: 30,
1584
+ ...style
1585
+ },
1586
+ children
1587
+ });
1588
+ }
1589
+ //#endregion
1590
+ //#region src/Toc.tsx
1591
+ /**
1592
+ * Extract heading text (h1/h2) from a React element tree.
1593
+ * Traverses children recursively to find the first h1 or h2.
1594
+ */
1595
+ function extractHeading(node) {
1596
+ if (!isValidElement(node)) return null;
1597
+ const el = node;
1598
+ const type = el.type;
1599
+ if (type === "h1" || type === "h2") return extractText(el.props.children);
1600
+ const children = el.props.children;
1601
+ if (children == null) return null;
1602
+ let result = null;
1603
+ Children.forEach(children, (child) => {
1604
+ if (result) return;
1605
+ const found = extractHeading(child);
1606
+ if (found) result = found;
1607
+ });
1608
+ return result;
1609
+ }
1610
+ /** Extract plain text from a React node tree */
1611
+ function extractText(node) {
1612
+ if (node == null) return "";
1613
+ if (typeof node === "string") return node;
1614
+ if (typeof node === "number") return String(node);
1615
+ if (Array.isArray(node)) return node.map(extractText).join("");
1616
+ if (isValidElement(node)) return extractText(node.props.children);
1617
+ return "";
1618
+ }
1619
+ /**
1620
+ * Table of Contents component that renders a clickable list of slides
1621
+ * with their heading text extracted from h1/h2 elements.
1622
+ *
1623
+ * Must be rendered inside a `<Deck>` component.
1624
+ *
1625
+ * ```tsx
1626
+ * <Toc>
1627
+ * <Slide><h1>Introduction</h1></Slide>
1628
+ * <Slide><h2>Agenda</h2></Slide>
1629
+ * </Toc>
1630
+ * ```
1631
+ */
1632
+ function Toc({ children, className, style }) {
1633
+ const { currentSlide, goTo } = useDeck();
1634
+ const items = Children.toArray(children).map((slide, index) => {
1635
+ return {
1636
+ index,
1637
+ title: extractHeading(slide) || `Slide ${index + 1}`
1638
+ };
1639
+ });
1640
+ return /* @__PURE__ */ jsx("nav", {
1641
+ className: `reslide-toc${className ? ` ${className}` : ""}`,
1642
+ style: {
1643
+ display: "flex",
1644
+ flexDirection: "column",
1645
+ gap: "0.25rem",
1646
+ padding: "0.5rem 0",
1647
+ ...style
1648
+ },
1649
+ children: items.map((item) => {
1650
+ const isActive = item.index === currentSlide;
1651
+ return /* @__PURE__ */ jsxs("button", {
1652
+ type: "button",
1653
+ onClick: () => goTo(item.index),
1654
+ style: {
1655
+ display: "flex",
1656
+ alignItems: "center",
1657
+ gap: "0.5rem",
1658
+ padding: "0.5rem 1rem",
1659
+ border: "none",
1660
+ borderRadius: "0.375rem",
1661
+ cursor: "pointer",
1662
+ textAlign: "left",
1663
+ fontSize: "0.875rem",
1664
+ lineHeight: 1.4,
1665
+ fontFamily: "inherit",
1666
+ color: isActive ? "var(--toc-active-text, var(--slide-accent, #3b82f6))" : "var(--toc-text, var(--slide-text, #1a1a1a))",
1667
+ backgroundColor: isActive ? "var(--toc-active-bg, rgba(59, 130, 246, 0.1))" : "transparent",
1668
+ fontWeight: isActive ? 600 : 400,
1669
+ transition: "background-color 0.15s, color 0.15s"
1670
+ },
1671
+ onMouseEnter: (e) => {
1672
+ if (!isActive) e.currentTarget.style.backgroundColor = "var(--toc-hover-bg, rgba(0, 0, 0, 0.05))";
1673
+ },
1674
+ onMouseLeave: (e) => {
1675
+ e.currentTarget.style.backgroundColor = isActive ? "var(--toc-active-bg, rgba(59, 130, 246, 0.1))" : "transparent";
1676
+ },
1677
+ children: [/* @__PURE__ */ jsx("span", {
1678
+ style: {
1679
+ minWidth: "1.5rem",
1680
+ textAlign: "right",
1681
+ opacity: .5,
1682
+ fontSize: "0.75rem",
1683
+ fontVariantNumeric: "tabular-nums"
1684
+ },
1685
+ children: item.index + 1
1686
+ }), /* @__PURE__ */ jsx("span", { children: item.title })]
1687
+ }, item.index);
1688
+ })
1689
+ });
1690
+ }
1691
+ //#endregion
1692
+ //#region src/Mermaid.tsx
1693
+ /**
1694
+ * Renders a Mermaid diagram.
1695
+ *
1696
+ * Usage in MDX:
1697
+ * ```mdx
1698
+ * <Mermaid>
1699
+ * graph TD
1700
+ * A --> B
1701
+ * </Mermaid>
1702
+ * ```
1703
+ *
1704
+ * The mermaid library is dynamically imported on the client side,
1705
+ * so it does not need to be installed as a project dependency.
1706
+ */
1707
+ function Mermaid({ children }) {
1708
+ const containerRef = useRef(null);
1709
+ const [svg, setSvg] = useState("");
1710
+ const [error, setError] = useState("");
1711
+ const id = useId().replace(/:/g, "_");
1712
+ useEffect(() => {
1713
+ let cancelled = false;
1714
+ async function render() {
1715
+ try {
1716
+ const mermaid = await import("mermaid");
1717
+ mermaid.default.initialize({
1718
+ startOnLoad: false,
1719
+ theme: "default",
1720
+ securityLevel: "loose"
1721
+ });
1722
+ const code = typeof children === "string" ? children.trim() : "";
1723
+ if (!code) return;
1724
+ const { svg: rendered } = await mermaid.default.render(`mermaid-${id}`, code);
1725
+ if (!cancelled) {
1726
+ setSvg(rendered);
1727
+ setError("");
1728
+ }
1729
+ } catch (err) {
1730
+ if (!cancelled) setError(err instanceof Error ? err.message : "Failed to render diagram");
1731
+ }
1732
+ }
1733
+ render();
1734
+ return () => {
1735
+ cancelled = true;
1736
+ };
1737
+ }, [children, id]);
1738
+ if (error) return /* @__PURE__ */ jsx("div", {
1739
+ className: "reslide-mermaid reslide-mermaid--error",
1740
+ children: /* @__PURE__ */ jsx("pre", { children: error })
1741
+ });
1742
+ return /* @__PURE__ */ jsx("div", {
1743
+ ref: containerRef,
1744
+ className: "reslide-mermaid",
1745
+ dangerouslySetInnerHTML: { __html: svg }
1746
+ });
1747
+ }
1748
+ //#endregion
1749
+ export { ClickNavigation as C, useDeck as S, NavigationBar as _, PresenterView as a, DrawingLayer as b, Mark as c, Slide as d, Deck as f, useSlideIndex as g, SlideIndexContext as h, GlobalLayer as i, Click as l, PrintView as m, Toc as n, SlotRight as o, ProgressBar as p, Draggable as r, Notes as s, Mermaid as t, ClickSteps as u, isPresenterView as v, DeckContext as x, openPresenterWindow as y };