@reslide-dev/core 0.0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1261 @@
1
+ import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2
+ import * as runtime from "react/jsx-runtime";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/ClickNavigation.tsx
5
+ /**
6
+ * Invisible click zones on the left/right edges of the slide.
7
+ * Clicking the left ~15% goes to the previous slide,
8
+ * clicking the right ~15% goes to the next slide.
9
+ * Shows a subtle arrow indicator on hover.
10
+ */
11
+ function ClickNavigation({ onPrev, onNext, disabled }) {
12
+ if (disabled) return null;
13
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(NavZone, {
14
+ direction: "prev",
15
+ onClick: onPrev
16
+ }), /* @__PURE__ */ jsx(NavZone, {
17
+ direction: "next",
18
+ onClick: onNext
19
+ })] });
20
+ }
21
+ function NavZone({ direction, onClick }) {
22
+ const [hovered, setHovered] = useState(false);
23
+ const isPrev = direction === "prev";
24
+ return /* @__PURE__ */ jsx("button", {
25
+ type: "button",
26
+ onClick: useCallback((e) => {
27
+ e.stopPropagation();
28
+ onClick();
29
+ }, [onClick]),
30
+ onMouseEnter: () => setHovered(true),
31
+ onMouseLeave: () => setHovered(false),
32
+ "aria-label": isPrev ? "Previous slide" : "Next slide",
33
+ style: {
34
+ position: "absolute",
35
+ top: 0,
36
+ bottom: 0,
37
+ [isPrev ? "left" : "right"]: 0,
38
+ width: "15%",
39
+ background: "none",
40
+ border: "none",
41
+ cursor: "pointer",
42
+ zIndex: 20,
43
+ display: "flex",
44
+ alignItems: "center",
45
+ justifyContent: isPrev ? "flex-start" : "flex-end",
46
+ padding: "0 1.5rem",
47
+ opacity: hovered ? 1 : 0,
48
+ transition: "opacity 0.2s ease"
49
+ },
50
+ children: /* @__PURE__ */ jsx("svg", {
51
+ width: "32",
52
+ height: "32",
53
+ viewBox: "0 0 24 24",
54
+ fill: "none",
55
+ stroke: "currentColor",
56
+ strokeWidth: "2",
57
+ strokeLinecap: "round",
58
+ strokeLinejoin: "round",
59
+ style: {
60
+ color: "var(--slide-text, #1a1a1a)",
61
+ opacity: .4,
62
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.2))",
63
+ transform: isPrev ? "none" : "rotate(180deg)"
64
+ },
65
+ children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" })
66
+ })
67
+ });
68
+ }
69
+ //#endregion
70
+ //#region src/context.ts
71
+ const DeckContext = createContext(null);
72
+ function useDeck() {
73
+ const ctx = useContext(DeckContext);
74
+ if (!ctx) throw new Error("useDeck must be used within a <Deck> component");
75
+ return ctx;
76
+ }
77
+ //#endregion
78
+ //#region src/DrawingLayer.tsx
79
+ /**
80
+ * Canvas-based freehand drawing overlay for presentations.
81
+ * Toggle with `d` key (handled in Deck).
82
+ */
83
+ function DrawingLayer({ active, color = "#ef4444", width = 3 }) {
84
+ const canvasRef = useRef(null);
85
+ const [isDrawing, setIsDrawing] = useState(false);
86
+ const lastPoint = useRef(null);
87
+ const getPoint = useCallback((e) => {
88
+ const canvas = canvasRef.current;
89
+ const rect = canvas.getBoundingClientRect();
90
+ return {
91
+ x: (e.clientX - rect.left) * (canvas.width / rect.width),
92
+ y: (e.clientY - rect.top) * (canvas.height / rect.height)
93
+ };
94
+ }, []);
95
+ const startDraw = useCallback((e) => {
96
+ if (!active) return;
97
+ setIsDrawing(true);
98
+ lastPoint.current = getPoint(e);
99
+ }, [active, getPoint]);
100
+ const draw = useCallback((e) => {
101
+ if (!isDrawing || !active) return;
102
+ const ctx = canvasRef.current?.getContext("2d");
103
+ if (!ctx || !lastPoint.current) return;
104
+ const point = getPoint(e);
105
+ ctx.beginPath();
106
+ ctx.moveTo(lastPoint.current.x, lastPoint.current.y);
107
+ ctx.lineTo(point.x, point.y);
108
+ ctx.strokeStyle = color;
109
+ ctx.lineWidth = width;
110
+ ctx.lineCap = "round";
111
+ ctx.lineJoin = "round";
112
+ ctx.stroke();
113
+ lastPoint.current = point;
114
+ }, [
115
+ isDrawing,
116
+ active,
117
+ color,
118
+ width,
119
+ getPoint
120
+ ]);
121
+ const stopDraw = useCallback(() => {
122
+ setIsDrawing(false);
123
+ lastPoint.current = null;
124
+ }, []);
125
+ useEffect(() => {
126
+ const canvas = canvasRef.current;
127
+ if (!canvas) return;
128
+ const resize = () => {
129
+ const rect = canvas.parentElement?.getBoundingClientRect();
130
+ if (rect) {
131
+ canvas.width = rect.width * window.devicePixelRatio;
132
+ canvas.height = rect.height * window.devicePixelRatio;
133
+ }
134
+ };
135
+ resize();
136
+ window.addEventListener("resize", resize);
137
+ return () => window.removeEventListener("resize", resize);
138
+ }, []);
139
+ useEffect(() => {
140
+ if (!active) return;
141
+ function handleKey(e) {
142
+ if (e.key === "c") {
143
+ const ctx = canvasRef.current?.getContext("2d");
144
+ if (ctx && canvasRef.current) ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
145
+ }
146
+ }
147
+ window.addEventListener("keydown", handleKey);
148
+ return () => window.removeEventListener("keydown", handleKey);
149
+ }, [active]);
150
+ return /* @__PURE__ */ jsx("canvas", {
151
+ ref: canvasRef,
152
+ onMouseDown: startDraw,
153
+ onMouseMove: draw,
154
+ onMouseUp: stopDraw,
155
+ onMouseLeave: stopDraw,
156
+ style: {
157
+ position: "absolute",
158
+ inset: 0,
159
+ width: "100%",
160
+ height: "100%",
161
+ cursor: active ? "crosshair" : "default",
162
+ pointerEvents: active ? "auto" : "none",
163
+ zIndex: 50
164
+ }
165
+ });
166
+ }
167
+ //#endregion
168
+ //#region src/slide-context.ts
169
+ const SlideIndexContext = createContext(null);
170
+ function useSlideIndex() {
171
+ const index = useContext(SlideIndexContext);
172
+ if (index == null) throw new Error("useSlideIndex must be used within a <Slide> component");
173
+ return index;
174
+ }
175
+ //#endregion
176
+ //#region src/PrintView.tsx
177
+ /**
178
+ * Renders all slides vertically for print/PDF export.
179
+ * Use with @media print CSS to generate PDFs via browser print.
180
+ */
181
+ function PrintView({ children }) {
182
+ return /* @__PURE__ */ jsx("div", {
183
+ className: "reslide-print-view",
184
+ children: Children.toArray(children).map((slide, i) => /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
185
+ value: i,
186
+ children: slide
187
+ }, i))
188
+ });
189
+ }
190
+ //#endregion
191
+ //#region src/SlideTransition.tsx
192
+ const DURATION = 300;
193
+ function SlideTransition({ children, currentSlide, transition }) {
194
+ const slides = Children.toArray(children);
195
+ const [displaySlide, setDisplaySlide] = useState(currentSlide);
196
+ const [prevSlide, setPrevSlide] = useState(null);
197
+ const [isAnimating, setIsAnimating] = useState(false);
198
+ const timerRef = useRef(null);
199
+ useEffect(() => {
200
+ if (currentSlide === displaySlide) return;
201
+ if (transition === "none") {
202
+ setDisplaySlide(currentSlide);
203
+ return;
204
+ }
205
+ setPrevSlide(displaySlide);
206
+ setDisplaySlide(currentSlide);
207
+ setIsAnimating(true);
208
+ if (timerRef.current) clearTimeout(timerRef.current);
209
+ timerRef.current = setTimeout(() => {
210
+ setIsAnimating(false);
211
+ setPrevSlide(null);
212
+ }, DURATION);
213
+ return () => {
214
+ if (timerRef.current) clearTimeout(timerRef.current);
215
+ };
216
+ }, [
217
+ currentSlide,
218
+ displaySlide,
219
+ transition
220
+ ]);
221
+ const resolvedTransition = resolveTransition(transition, prevSlide, displaySlide);
222
+ if (transition === "none" || !isAnimating) return /* @__PURE__ */ jsx("div", {
223
+ className: "reslide-transition-container",
224
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
225
+ value: displaySlide,
226
+ children: /* @__PURE__ */ jsx("div", {
227
+ className: "reslide-transition-slide",
228
+ style: { position: "relative" },
229
+ children: slides[displaySlide]
230
+ })
231
+ })
232
+ });
233
+ return /* @__PURE__ */ jsxs("div", {
234
+ className: "reslide-transition-container",
235
+ children: [prevSlide != null && /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
236
+ value: prevSlide,
237
+ children: /* @__PURE__ */ jsx("div", {
238
+ className: `reslide-transition-slide reslide-transition-${resolvedTransition}-exit`,
239
+ children: slides[prevSlide]
240
+ })
241
+ }), /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
242
+ value: displaySlide,
243
+ children: /* @__PURE__ */ jsx("div", {
244
+ className: `reslide-transition-slide reslide-transition-${resolvedTransition}-enter`,
245
+ children: slides[displaySlide]
246
+ })
247
+ })]
248
+ });
249
+ }
250
+ function resolveTransition(transition, from, to) {
251
+ if (transition !== "slide-left" && transition !== "slide-right") return transition;
252
+ if (from == null) return transition;
253
+ const goingForward = to > from;
254
+ if (transition === "slide-left") return goingForward ? "slide-left" : "slide-right";
255
+ return goingForward ? "slide-right" : "slide-left";
256
+ }
257
+ //#endregion
258
+ //#region src/use-fullscreen.ts
259
+ function useFullscreen(ref) {
260
+ const [isFullscreen, setIsFullscreen] = useState(false);
261
+ useEffect(() => {
262
+ function handleChange() {
263
+ setIsFullscreen(document.fullscreenElement != null);
264
+ }
265
+ document.addEventListener("fullscreenchange", handleChange);
266
+ return () => document.removeEventListener("fullscreenchange", handleChange);
267
+ }, []);
268
+ return {
269
+ isFullscreen,
270
+ toggleFullscreen: useCallback(() => {
271
+ if (!ref.current) return;
272
+ if (document.fullscreenElement) document.exitFullscreen();
273
+ else ref.current.requestFullscreen();
274
+ }, [ref])
275
+ };
276
+ }
277
+ //#endregion
278
+ //#region src/use-presenter.ts
279
+ const CHANNEL_NAME = "reslide-presenter";
280
+ /**
281
+ * Hook for syncing presentation state across windows via BroadcastChannel.
282
+ * The main presentation window broadcasts state changes.
283
+ */
284
+ function usePresenterSync(currentSlide, clickStep, onReceive) {
285
+ const channelRef = useRef(null);
286
+ useEffect(() => {
287
+ if (typeof BroadcastChannel === "undefined") return;
288
+ const channel = new BroadcastChannel(CHANNEL_NAME);
289
+ channelRef.current = channel;
290
+ if (onReceive) channel.onmessage = (e) => {
291
+ onReceive(e.data);
292
+ };
293
+ return () => {
294
+ channel.close();
295
+ channelRef.current = null;
296
+ };
297
+ }, [onReceive]);
298
+ useEffect(() => {
299
+ channelRef.current?.postMessage({
300
+ type: "sync",
301
+ currentSlide,
302
+ clickStep
303
+ });
304
+ }, [currentSlide, clickStep]);
305
+ }
306
+ /**
307
+ * Opens the presenter window at the /presenter route.
308
+ */
309
+ function openPresenterWindow() {
310
+ const url = new URL(window.location.href);
311
+ url.searchParams.set("presenter", "true");
312
+ window.open(url.toString(), "reslide-presenter", "width=1024,height=768,menubar=no,toolbar=no");
313
+ }
314
+ /**
315
+ * Check if the current window is the presenter view.
316
+ */
317
+ function isPresenterView() {
318
+ if (typeof window === "undefined") return false;
319
+ return new URLSearchParams(window.location.search).get("presenter") === "true";
320
+ }
321
+ //#endregion
322
+ //#region src/Deck.tsx
323
+ function Deck({ children, transition = "none" }) {
324
+ const containerRef = useRef(null);
325
+ const [currentSlide, setCurrentSlide] = useState(0);
326
+ const [clickStep, setClickStep] = useState(0);
327
+ const [isOverview, setIsOverview] = useState(false);
328
+ const [isDrawing, setIsDrawing] = useState(false);
329
+ const [isPrinting, setIsPrinting] = useState(false);
330
+ const [clickStepsMap, setClickStepsMap] = useState({});
331
+ const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
332
+ usePresenterSync(currentSlide, clickStep);
333
+ const totalSlides = Children.count(children);
334
+ const totalClickSteps = clickStepsMap[currentSlide] ?? 0;
335
+ const registerClickSteps = useCallback((slideIndex, count) => {
336
+ setClickStepsMap((prev) => {
337
+ if (prev[slideIndex] === count) return prev;
338
+ return {
339
+ ...prev,
340
+ [slideIndex]: count
341
+ };
342
+ });
343
+ }, []);
344
+ const next = useCallback(() => {
345
+ if (isOverview) return;
346
+ if (clickStep < totalClickSteps) setClickStep((s) => s + 1);
347
+ else if (currentSlide < totalSlides - 1) {
348
+ setCurrentSlide((s) => s + 1);
349
+ setClickStep(0);
350
+ }
351
+ }, [
352
+ isOverview,
353
+ clickStep,
354
+ totalClickSteps,
355
+ currentSlide,
356
+ totalSlides
357
+ ]);
358
+ const prev = useCallback(() => {
359
+ if (isOverview) return;
360
+ if (clickStep > 0) setClickStep((s) => s - 1);
361
+ else if (currentSlide > 0) {
362
+ const prevSlide = currentSlide - 1;
363
+ setCurrentSlide(prevSlide);
364
+ setClickStep(clickStepsMap[prevSlide] ?? 0);
365
+ }
366
+ }, [
367
+ isOverview,
368
+ clickStep,
369
+ currentSlide,
370
+ clickStepsMap
371
+ ]);
372
+ const goTo = useCallback((slideIndex) => {
373
+ if (slideIndex >= 0 && slideIndex < totalSlides) {
374
+ setCurrentSlide(slideIndex);
375
+ setClickStep(0);
376
+ if (isOverview) setIsOverview(false);
377
+ }
378
+ }, [totalSlides, isOverview]);
379
+ const toggleOverview = useCallback(() => {
380
+ setIsOverview((v) => !v);
381
+ }, []);
382
+ useEffect(() => {
383
+ function handleKeyDown(e) {
384
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
385
+ switch (e.key) {
386
+ case "ArrowRight":
387
+ case " ":
388
+ e.preventDefault();
389
+ next();
390
+ break;
391
+ case "ArrowLeft":
392
+ e.preventDefault();
393
+ prev();
394
+ break;
395
+ case "Escape":
396
+ if (!document.fullscreenElement) {
397
+ e.preventDefault();
398
+ toggleOverview();
399
+ }
400
+ break;
401
+ case "f":
402
+ e.preventDefault();
403
+ toggleFullscreen();
404
+ break;
405
+ case "p":
406
+ e.preventDefault();
407
+ openPresenterWindow();
408
+ break;
409
+ case "d":
410
+ e.preventDefault();
411
+ setIsDrawing((v) => !v);
412
+ break;
413
+ }
414
+ }
415
+ window.addEventListener("keydown", handleKeyDown);
416
+ return () => window.removeEventListener("keydown", handleKeyDown);
417
+ }, [
418
+ next,
419
+ prev,
420
+ toggleOverview,
421
+ toggleFullscreen
422
+ ]);
423
+ useEffect(() => {
424
+ function onBeforePrint() {
425
+ setIsPrinting(true);
426
+ }
427
+ function onAfterPrint() {
428
+ setIsPrinting(false);
429
+ }
430
+ window.addEventListener("beforeprint", onBeforePrint);
431
+ window.addEventListener("afterprint", onAfterPrint);
432
+ return () => {
433
+ window.removeEventListener("beforeprint", onBeforePrint);
434
+ window.removeEventListener("afterprint", onAfterPrint);
435
+ };
436
+ }, []);
437
+ const contextValue = useMemo(() => ({
438
+ currentSlide,
439
+ totalSlides,
440
+ clickStep,
441
+ totalClickSteps,
442
+ isOverview,
443
+ isFullscreen,
444
+ next,
445
+ prev,
446
+ goTo,
447
+ toggleOverview,
448
+ toggleFullscreen,
449
+ registerClickSteps
450
+ }), [
451
+ currentSlide,
452
+ totalSlides,
453
+ clickStep,
454
+ totalClickSteps,
455
+ isOverview,
456
+ isFullscreen,
457
+ next,
458
+ prev,
459
+ goTo,
460
+ toggleOverview,
461
+ toggleFullscreen,
462
+ registerClickSteps
463
+ ]);
464
+ return /* @__PURE__ */ jsx(DeckContext.Provider, {
465
+ value: contextValue,
466
+ children: /* @__PURE__ */ jsxs("div", {
467
+ ref: containerRef,
468
+ className: "reslide-deck",
469
+ style: {
470
+ position: "relative",
471
+ width: "100%",
472
+ height: "100%",
473
+ overflow: "hidden",
474
+ backgroundColor: "var(--slide-bg, #fff)",
475
+ color: "var(--slide-text, #1a1a1a)"
476
+ },
477
+ children: [
478
+ isPrinting ? /* @__PURE__ */ jsx(PrintView, { children }) : isOverview ? /* @__PURE__ */ jsx(OverviewGrid, {
479
+ totalSlides,
480
+ goTo,
481
+ children
482
+ }) : /* @__PURE__ */ jsx(SlideTransition, {
483
+ currentSlide,
484
+ transition,
485
+ children
486
+ }),
487
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(ClickNavigation, {
488
+ onPrev: prev,
489
+ onNext: next,
490
+ disabled: isDrawing
491
+ }),
492
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(SlideNumber, {
493
+ current: currentSlide + 1,
494
+ total: totalSlides
495
+ }),
496
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, { active: isDrawing })
497
+ ]
498
+ })
499
+ });
500
+ }
501
+ function OverviewGrid({ children, totalSlides, goTo }) {
502
+ const slides = Children.toArray(children);
503
+ return /* @__PURE__ */ jsx("div", {
504
+ className: "reslide-overview",
505
+ style: {
506
+ display: "grid",
507
+ gridTemplateColumns: `repeat(${Math.ceil(Math.sqrt(totalSlides))}, 1fr)`,
508
+ gap: "1rem",
509
+ padding: "1rem",
510
+ width: "100%",
511
+ height: "100%",
512
+ overflow: "auto"
513
+ },
514
+ children: slides.map((slide, i) => /* @__PURE__ */ jsx("button", {
515
+ type: "button",
516
+ onClick: () => goTo(i),
517
+ style: {
518
+ border: "1px solid var(--slide-accent, #3b82f6)",
519
+ borderRadius: "0.5rem",
520
+ overflow: "hidden",
521
+ cursor: "pointer",
522
+ background: "var(--slide-bg, #fff)",
523
+ aspectRatio: "16 / 9",
524
+ padding: 0,
525
+ position: "relative"
526
+ },
527
+ children: /* @__PURE__ */ jsx("div", {
528
+ style: {
529
+ transform: "scale(0.25)",
530
+ transformOrigin: "top left",
531
+ width: "400%",
532
+ height: "400%",
533
+ pointerEvents: "none"
534
+ },
535
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
536
+ value: i,
537
+ children: slide
538
+ })
539
+ })
540
+ }, i))
541
+ });
542
+ }
543
+ function SlideNumber({ current, total }) {
544
+ return /* @__PURE__ */ jsxs("div", {
545
+ className: "reslide-slide-number",
546
+ style: {
547
+ position: "absolute",
548
+ bottom: "1rem",
549
+ right: "1rem",
550
+ fontSize: "0.875rem",
551
+ opacity: .6,
552
+ fontVariantNumeric: "tabular-nums"
553
+ },
554
+ children: [
555
+ current,
556
+ " / ",
557
+ total
558
+ ]
559
+ });
560
+ }
561
+ //#endregion
562
+ //#region src/Slide.tsx
563
+ const baseStyle = {
564
+ width: "100%",
565
+ height: "100%",
566
+ display: "flex",
567
+ boxSizing: "border-box"
568
+ };
569
+ function isSlotRight(child) {
570
+ return isValidElement(child) && typeof child.type === "function" && "__reslideSlot" in child.type && child.type.__reslideSlot === "right";
571
+ }
572
+ function splitChildren(children) {
573
+ const left = [];
574
+ const right = [];
575
+ let inRight = false;
576
+ Children.forEach(children, (child) => {
577
+ if (isSlotRight(child)) {
578
+ inRight = true;
579
+ right.push(child.props.children);
580
+ return;
581
+ }
582
+ if (inRight) right.push(child);
583
+ else left.push(child);
584
+ });
585
+ return {
586
+ left,
587
+ right
588
+ };
589
+ }
590
+ function Slide({ children, layout = "default", image, className, style }) {
591
+ const cls = `reslide-slide reslide-layout-${layout}${className ? ` ${className}` : ""}`;
592
+ switch (layout) {
593
+ case "center": return /* @__PURE__ */ jsx("div", {
594
+ className: cls,
595
+ style: {
596
+ ...baseStyle,
597
+ flexDirection: "column",
598
+ justifyContent: "center",
599
+ alignItems: "center",
600
+ textAlign: "center",
601
+ padding: "3rem 4rem",
602
+ ...style
603
+ },
604
+ children
605
+ });
606
+ case "two-cols": {
607
+ const { left, right } = splitChildren(children);
608
+ return /* @__PURE__ */ jsxs("div", {
609
+ className: cls,
610
+ style: {
611
+ ...baseStyle,
612
+ flexDirection: "row",
613
+ gap: "2rem",
614
+ padding: "3rem 4rem",
615
+ ...style
616
+ },
617
+ children: [/* @__PURE__ */ jsx("div", {
618
+ style: {
619
+ flex: 1,
620
+ minWidth: 0
621
+ },
622
+ children: left
623
+ }), /* @__PURE__ */ jsx("div", {
624
+ style: {
625
+ flex: 1,
626
+ minWidth: 0
627
+ },
628
+ children: right
629
+ })]
630
+ });
631
+ }
632
+ case "image-right": return /* @__PURE__ */ jsxs("div", {
633
+ className: cls,
634
+ style: {
635
+ ...baseStyle,
636
+ flexDirection: "row",
637
+ ...style
638
+ },
639
+ children: [/* @__PURE__ */ jsx("div", {
640
+ style: {
641
+ flex: 1,
642
+ padding: "3rem 2rem 3rem 4rem",
643
+ overflow: "auto"
644
+ },
645
+ children
646
+ }), image && /* @__PURE__ */ jsx("div", { style: {
647
+ flex: 1,
648
+ backgroundImage: `url(${image})`,
649
+ backgroundSize: "cover",
650
+ backgroundPosition: "center"
651
+ } })]
652
+ });
653
+ case "image-left": return /* @__PURE__ */ jsxs("div", {
654
+ className: cls,
655
+ style: {
656
+ ...baseStyle,
657
+ flexDirection: "row",
658
+ ...style
659
+ },
660
+ children: [image && /* @__PURE__ */ jsx("div", { style: {
661
+ flex: 1,
662
+ backgroundImage: `url(${image})`,
663
+ backgroundSize: "cover",
664
+ backgroundPosition: "center"
665
+ } }), /* @__PURE__ */ jsx("div", {
666
+ style: {
667
+ flex: 1,
668
+ padding: "3rem 4rem 3rem 2rem",
669
+ overflow: "auto"
670
+ },
671
+ children
672
+ })]
673
+ });
674
+ case "section": return /* @__PURE__ */ jsx("div", {
675
+ className: cls,
676
+ style: {
677
+ ...baseStyle,
678
+ flexDirection: "column",
679
+ justifyContent: "center",
680
+ alignItems: "center",
681
+ textAlign: "center",
682
+ padding: "3rem 4rem",
683
+ backgroundColor: "var(--slide-accent, #3b82f6)",
684
+ color: "var(--slide-section-text, #fff)",
685
+ ...style
686
+ },
687
+ children
688
+ });
689
+ case "quote": return /* @__PURE__ */ jsx("div", {
690
+ className: cls,
691
+ style: {
692
+ ...baseStyle,
693
+ flexDirection: "column",
694
+ justifyContent: "center",
695
+ padding: "3rem 6rem",
696
+ ...style
697
+ },
698
+ children: /* @__PURE__ */ jsx("blockquote", {
699
+ style: {
700
+ fontSize: "1.5em",
701
+ fontStyle: "italic",
702
+ borderLeft: "4px solid var(--slide-accent, #3b82f6)",
703
+ paddingLeft: "1.5rem",
704
+ margin: 0
705
+ },
706
+ children
707
+ })
708
+ });
709
+ default: return /* @__PURE__ */ jsx("div", {
710
+ className: cls,
711
+ style: {
712
+ ...baseStyle,
713
+ flexDirection: "column",
714
+ padding: "3rem 4rem",
715
+ ...style
716
+ },
717
+ children
718
+ });
719
+ }
720
+ }
721
+ //#endregion
722
+ //#region src/Click.tsx
723
+ function Click({ children, at }) {
724
+ const { clickStep } = useDeck();
725
+ const visible = clickStep >= (at ?? 1);
726
+ return /* @__PURE__ */ jsx("div", {
727
+ className: "reslide-click",
728
+ style: {
729
+ opacity: visible ? 1 : 0,
730
+ visibility: visible ? "visible" : "hidden",
731
+ pointerEvents: visible ? "auto" : "none",
732
+ transition: "opacity 0.3s ease, visibility 0.3s ease"
733
+ },
734
+ children
735
+ });
736
+ }
737
+ /**
738
+ * Register click steps for the current slide.
739
+ * Automatically reads the slide index from SlideIndexContext.
740
+ * If slideIndex prop is provided, it takes precedence (for backwards compatibility).
741
+ */
742
+ function ClickSteps({ count, slideIndex }) {
743
+ const { registerClickSteps } = useDeck();
744
+ const contextIndex = useContext(SlideIndexContext);
745
+ const resolvedIndex = slideIndex ?? contextIndex;
746
+ useEffect(() => {
747
+ if (resolvedIndex != null) registerClickSteps(resolvedIndex, count);
748
+ }, [
749
+ resolvedIndex,
750
+ count,
751
+ registerClickSteps
752
+ ]);
753
+ return null;
754
+ }
755
+ //#endregion
756
+ //#region src/Mark.tsx
757
+ const markStyles = {
758
+ highlight: (color) => ({
759
+ backgroundColor: `var(--mark-${color}, ${color})`,
760
+ padding: "0.1em 0.2em",
761
+ borderRadius: "0.2em"
762
+ }),
763
+ underline: (color) => ({
764
+ textDecoration: "underline",
765
+ textDecorationColor: `var(--mark-${color}, ${color})`,
766
+ textDecorationThickness: "0.15em",
767
+ textUnderlineOffset: "0.15em"
768
+ }),
769
+ circle: (color) => ({
770
+ border: `0.15em solid var(--mark-${color}, ${color})`,
771
+ borderRadius: "50%",
772
+ padding: "0.1em 0.3em"
773
+ })
774
+ };
775
+ const defaultColors = {
776
+ orange: "#fb923c",
777
+ red: "#ef4444",
778
+ blue: "#3b82f6",
779
+ green: "#22c55e",
780
+ yellow: "#facc15",
781
+ purple: "#a855f7"
782
+ };
783
+ function Mark({ children, type = "highlight", color = "yellow" }) {
784
+ const resolvedColor = defaultColors[color] ?? color;
785
+ const styleFn = markStyles[type] ?? markStyles.highlight;
786
+ return /* @__PURE__ */ jsx("span", {
787
+ className: `reslide-mark reslide-mark-${type}`,
788
+ style: styleFn(resolvedColor),
789
+ children
790
+ });
791
+ }
792
+ //#endregion
793
+ //#region src/Notes.tsx
794
+ /**
795
+ * Speaker notes. Hidden during normal presentation,
796
+ * visible in overview mode.
797
+ */
798
+ function Notes({ children }) {
799
+ const { isOverview } = useDeck();
800
+ if (!isOverview) return null;
801
+ return /* @__PURE__ */ jsx("div", {
802
+ className: "reslide-notes",
803
+ style: {
804
+ marginTop: "auto",
805
+ padding: "0.75rem",
806
+ fontSize: "0.75rem",
807
+ color: "var(--slide-text, #1a1a1a)",
808
+ opacity: .7,
809
+ borderTop: "1px solid currentColor"
810
+ },
811
+ children
812
+ });
813
+ }
814
+ //#endregion
815
+ //#region src/Slot.tsx
816
+ /**
817
+ * Marks content as belonging to the right column in a two-cols layout.
818
+ * Used by remarkSlides to separate `::right` content.
819
+ */
820
+ function SlotRight({ children }) {
821
+ return /* @__PURE__ */ jsx(Fragment$1, { children });
822
+ }
823
+ SlotRight.displayName = "SlotRight";
824
+ SlotRight.__reslideSlot = "right";
825
+ //#endregion
826
+ //#region src/PresenterView.tsx
827
+ /**
828
+ * Presenter view that syncs with the main presentation window.
829
+ * Shows: current slide, next slide preview, notes, and timer.
830
+ */
831
+ function PresenterView({ children, notes }) {
832
+ const [currentSlide, setCurrentSlide] = useState(0);
833
+ const [clickStep, setClickStep] = useState(0);
834
+ const [elapsed, setElapsed] = useState(0);
835
+ const slides = Children.toArray(children);
836
+ const totalSlides = slides.length;
837
+ useEffect(() => {
838
+ if (typeof BroadcastChannel === "undefined") return;
839
+ const channel = new BroadcastChannel("reslide-presenter");
840
+ channel.onmessage = (e) => {
841
+ if (e.data.type === "sync") {
842
+ setCurrentSlide(e.data.currentSlide);
843
+ setClickStep(e.data.clickStep);
844
+ }
845
+ };
846
+ return () => channel.close();
847
+ }, []);
848
+ useEffect(() => {
849
+ const start = Date.now();
850
+ const interval = setInterval(() => {
851
+ setElapsed(Math.floor((Date.now() - start) / 1e3));
852
+ }, 1e3);
853
+ return () => clearInterval(interval);
854
+ }, []);
855
+ const formatTime = (seconds) => {
856
+ const m = Math.floor(seconds / 60);
857
+ const s = seconds % 60;
858
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
859
+ };
860
+ const noop = useCallback(() => {}, []);
861
+ const contextValue = {
862
+ currentSlide,
863
+ totalSlides,
864
+ clickStep,
865
+ totalClickSteps: 0,
866
+ isOverview: false,
867
+ isFullscreen: false,
868
+ next: noop,
869
+ prev: noop,
870
+ goTo: useCallback((_n) => {}, []),
871
+ toggleOverview: noop,
872
+ toggleFullscreen: noop,
873
+ registerClickSteps: useCallback((_i, _c) => {}, [])
874
+ };
875
+ return /* @__PURE__ */ jsx(DeckContext.Provider, {
876
+ value: contextValue,
877
+ children: /* @__PURE__ */ jsxs("div", {
878
+ style: {
879
+ display: "grid",
880
+ gridTemplateColumns: "2fr 1fr",
881
+ gridTemplateRows: "1fr auto",
882
+ width: "100%",
883
+ height: "100%",
884
+ gap: "0.75rem",
885
+ padding: "0.75rem",
886
+ backgroundColor: "#1a1a2e",
887
+ color: "#e2e8f0",
888
+ fontFamily: "system-ui, sans-serif",
889
+ boxSizing: "border-box"
890
+ },
891
+ children: [
892
+ /* @__PURE__ */ jsx("div", {
893
+ style: {
894
+ border: "2px solid #3b82f6",
895
+ borderRadius: "0.5rem",
896
+ overflow: "hidden",
897
+ position: "relative",
898
+ backgroundColor: "var(--slide-bg, #fff)",
899
+ color: "var(--slide-text, #1a1a1a)"
900
+ },
901
+ children: /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
902
+ value: currentSlide,
903
+ children: slides[currentSlide]
904
+ })
905
+ }),
906
+ /* @__PURE__ */ jsxs("div", {
907
+ style: {
908
+ display: "flex",
909
+ flexDirection: "column",
910
+ gap: "0.75rem",
911
+ minHeight: 0
912
+ },
913
+ children: [/* @__PURE__ */ jsx("div", {
914
+ style: {
915
+ flex: "0 0 40%",
916
+ border: "1px solid #334155",
917
+ borderRadius: "0.5rem",
918
+ overflow: "hidden",
919
+ opacity: .8,
920
+ backgroundColor: "var(--slide-bg, #fff)",
921
+ color: "var(--slide-text, #1a1a1a)"
922
+ },
923
+ children: currentSlide < totalSlides - 1 && /* @__PURE__ */ jsx(SlideIndexContext.Provider, {
924
+ value: currentSlide + 1,
925
+ children: /* @__PURE__ */ jsx("div", {
926
+ style: {
927
+ transform: "scale(0.5)",
928
+ transformOrigin: "top left",
929
+ width: "200%",
930
+ height: "200%"
931
+ },
932
+ children: slides[currentSlide + 1]
933
+ })
934
+ })
935
+ }), /* @__PURE__ */ jsxs("div", {
936
+ style: {
937
+ flex: 1,
938
+ overflow: "auto",
939
+ padding: "1rem",
940
+ backgroundColor: "#0f172a",
941
+ borderRadius: "0.5rem",
942
+ fontSize: "0.875rem",
943
+ lineHeight: 1.6
944
+ },
945
+ children: [/* @__PURE__ */ jsx("div", {
946
+ style: {
947
+ fontWeight: 600,
948
+ marginBottom: "0.5rem",
949
+ color: "#94a3b8"
950
+ },
951
+ children: "Notes"
952
+ }), notes?.[currentSlide] ?? /* @__PURE__ */ jsx("span", {
953
+ style: { color: "#64748b" },
954
+ children: "No notes for this slide"
955
+ })]
956
+ })]
957
+ }),
958
+ /* @__PURE__ */ jsxs("div", {
959
+ style: {
960
+ gridColumn: "1 / -1",
961
+ display: "flex",
962
+ justifyContent: "space-between",
963
+ alignItems: "center",
964
+ padding: "0.5rem 1rem",
965
+ backgroundColor: "#0f172a",
966
+ borderRadius: "0.5rem"
967
+ },
968
+ children: [/* @__PURE__ */ jsx("div", {
969
+ style: {
970
+ fontSize: "1.5rem",
971
+ fontVariantNumeric: "tabular-nums",
972
+ fontWeight: 700
973
+ },
974
+ children: formatTime(elapsed)
975
+ }), /* @__PURE__ */ jsxs("div", {
976
+ style: {
977
+ fontSize: "1.125rem",
978
+ fontVariantNumeric: "tabular-nums"
979
+ },
980
+ children: [
981
+ currentSlide + 1,
982
+ " / ",
983
+ totalSlides,
984
+ clickStep > 0 && /* @__PURE__ */ jsxs("span", {
985
+ style: { color: "#94a3b8" },
986
+ children: [
987
+ " (click ",
988
+ clickStep,
989
+ ")"
990
+ ]
991
+ })
992
+ ]
993
+ })]
994
+ })
995
+ ]
996
+ })
997
+ });
998
+ }
999
+ //#endregion
1000
+ //#region src/CodeEditor.tsx
1001
+ /**
1002
+ * Live code editor component for presentations.
1003
+ * Uses Monaco Editor (loaded from CDN) for syntax highlighting and editing.
1004
+ *
1005
+ * Falls back to a styled <textarea> if Monaco fails to load.
1006
+ */
1007
+ function CodeEditor({ value, language = "typescript", readOnly = false, height = 300, onChange, style }) {
1008
+ const containerRef = useRef(null);
1009
+ const editorRef = useRef(null);
1010
+ const [fallback, setFallback] = useState(false);
1011
+ const [text, setText] = useState(value);
1012
+ useEffect(() => {
1013
+ const container = containerRef.current;
1014
+ if (!container || fallback) return;
1015
+ let disposed = false;
1016
+ async function initMonaco() {
1017
+ try {
1018
+ const monacoUrl = "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs";
1019
+ if (!window.require) await new Promise((resolve, reject) => {
1020
+ const script = document.createElement("script");
1021
+ script.src = `${monacoUrl}/loader.js`;
1022
+ script.onload = () => resolve();
1023
+ script.onerror = () => reject(/* @__PURE__ */ new Error("Failed to load Monaco loader"));
1024
+ document.head.appendChild(script);
1025
+ });
1026
+ if (disposed) return;
1027
+ const amdRequire = window.require;
1028
+ amdRequire({ paths: { vs: monacoUrl } })(["vs/editor/editor.main"], (monaco) => {
1029
+ if (disposed || !container) return;
1030
+ const editor = monaco.editor.create(container, {
1031
+ value,
1032
+ language,
1033
+ readOnly,
1034
+ theme: "vs-dark",
1035
+ minimap: { enabled: false },
1036
+ scrollBeyondLastLine: false,
1037
+ fontSize: 14,
1038
+ lineNumbers: "on",
1039
+ automaticLayout: true,
1040
+ padding: {
1041
+ top: 12,
1042
+ bottom: 12
1043
+ }
1044
+ });
1045
+ editor.onDidChangeModelContent(() => {
1046
+ const newValue = editor.getValue();
1047
+ setText(newValue);
1048
+ onChange?.(newValue);
1049
+ });
1050
+ editorRef.current = editor;
1051
+ });
1052
+ } catch {
1053
+ if (!disposed) setFallback(true);
1054
+ }
1055
+ }
1056
+ initMonaco();
1057
+ return () => {
1058
+ disposed = true;
1059
+ if (editorRef.current && typeof editorRef.current.dispose === "function") editorRef.current.dispose();
1060
+ };
1061
+ }, [
1062
+ language,
1063
+ readOnly,
1064
+ value,
1065
+ onChange,
1066
+ fallback
1067
+ ]);
1068
+ const handleFallbackChange = useCallback((e) => {
1069
+ setText(e.target.value);
1070
+ onChange?.(e.target.value);
1071
+ }, [onChange]);
1072
+ if (fallback) return /* @__PURE__ */ jsx("textarea", {
1073
+ className: "reslide-code-editor-fallback",
1074
+ value: text,
1075
+ onChange: handleFallbackChange,
1076
+ readOnly,
1077
+ style: {
1078
+ width: "100%",
1079
+ height,
1080
+ fontFamily: "monospace",
1081
+ fontSize: "14px",
1082
+ padding: "12px",
1083
+ backgroundColor: "#1e1e1e",
1084
+ color: "#d4d4d4",
1085
+ border: "1px solid #333",
1086
+ borderRadius: "0.5rem",
1087
+ resize: "vertical",
1088
+ ...style
1089
+ }
1090
+ });
1091
+ return /* @__PURE__ */ jsx("div", {
1092
+ ref: containerRef,
1093
+ className: "reslide-code-editor",
1094
+ style: {
1095
+ width: "100%",
1096
+ height,
1097
+ borderRadius: "0.5rem",
1098
+ overflow: "hidden",
1099
+ ...style
1100
+ }
1101
+ });
1102
+ }
1103
+ //#endregion
1104
+ //#region src/GlobalLayer.tsx
1105
+ /**
1106
+ * Global overlay layer that persists across all slides.
1107
+ * Use for headers, footers, logos, watermarks, or progress bars.
1108
+ *
1109
+ * Place inside <Deck> to render on every slide.
1110
+ *
1111
+ * @example
1112
+ * ```tsx
1113
+ * <Deck>
1114
+ * <GlobalLayer position="above" style={{ bottom: 0 }}>
1115
+ * <footer>My Company</footer>
1116
+ * </GlobalLayer>
1117
+ * <Slide>...</Slide>
1118
+ * </Deck>
1119
+ * ```
1120
+ */
1121
+ function GlobalLayer({ children, position = "above", style }) {
1122
+ return /* @__PURE__ */ jsx("div", {
1123
+ className: `reslide-global-layer reslide-global-layer-${position}`,
1124
+ style: {
1125
+ position: "absolute",
1126
+ inset: 0,
1127
+ pointerEvents: "none",
1128
+ zIndex: position === "above" ? 40 : 0,
1129
+ ...style
1130
+ },
1131
+ children: /* @__PURE__ */ jsx("div", {
1132
+ style: { pointerEvents: "auto" },
1133
+ children
1134
+ })
1135
+ });
1136
+ }
1137
+ GlobalLayer.displayName = "GlobalLayer";
1138
+ GlobalLayer.__reslideGlobalLayer = true;
1139
+ //#endregion
1140
+ //#region src/Draggable.tsx
1141
+ /**
1142
+ * A draggable element within a slide.
1143
+ * Click and drag to reposition during presentation.
1144
+ */
1145
+ function Draggable({ children, x = 0, y = 0, style }) {
1146
+ const [pos, setPos] = useState({
1147
+ x: typeof x === "number" ? x : 0,
1148
+ y: typeof y === "number" ? y : 0
1149
+ });
1150
+ const dragRef = useRef(null);
1151
+ return /* @__PURE__ */ jsx("div", {
1152
+ className: "reslide-draggable",
1153
+ onMouseDown: useCallback((e) => {
1154
+ e.preventDefault();
1155
+ dragRef.current = {
1156
+ startX: e.clientX,
1157
+ startY: e.clientY,
1158
+ origX: pos.x,
1159
+ origY: pos.y
1160
+ };
1161
+ function onMouseMove(me) {
1162
+ if (!dragRef.current) return;
1163
+ setPos({
1164
+ x: dragRef.current.origX + (me.clientX - dragRef.current.startX),
1165
+ y: dragRef.current.origY + (me.clientY - dragRef.current.startY)
1166
+ });
1167
+ }
1168
+ function onMouseUp() {
1169
+ dragRef.current = null;
1170
+ window.removeEventListener("mousemove", onMouseMove);
1171
+ window.removeEventListener("mouseup", onMouseUp);
1172
+ }
1173
+ window.addEventListener("mousemove", onMouseMove);
1174
+ window.addEventListener("mouseup", onMouseUp);
1175
+ }, [pos.x, pos.y]),
1176
+ style: {
1177
+ position: "absolute",
1178
+ left: typeof x === "string" ? x : void 0,
1179
+ top: typeof y === "string" ? y : void 0,
1180
+ transform: `translate(${pos.x}px, ${pos.y}px)`,
1181
+ cursor: "grab",
1182
+ userSelect: "none",
1183
+ zIndex: 30,
1184
+ ...style
1185
+ },
1186
+ children
1187
+ });
1188
+ }
1189
+ //#endregion
1190
+ //#region src/ReslideEmbed.tsx
1191
+ /** Built-in reslide components available in MDX */
1192
+ const builtinComponents = {
1193
+ Deck,
1194
+ Slide,
1195
+ Click,
1196
+ ClickSteps,
1197
+ Mark,
1198
+ Notes,
1199
+ SlotRight,
1200
+ GlobalLayer,
1201
+ Draggable
1202
+ };
1203
+ /**
1204
+ * Renders compiled MDX slides as a full reslide presentation.
1205
+ *
1206
+ * Usage:
1207
+ * ```tsx
1208
+ * // Server component
1209
+ * import { compileMdxSlides } from '@reslide/mdx'
1210
+ * const { code } = await compileMdxSlides(mdxSource)
1211
+ *
1212
+ * // Client component
1213
+ * import { ReslideEmbed } from '@reslide/core'
1214
+ * <ReslideEmbed code={code} transition="fade" />
1215
+ * ```
1216
+ */
1217
+ function ReslideEmbed({ code, transition, components: userComponents, className, style }) {
1218
+ const [Content, setContent] = useState(null);
1219
+ useEffect(() => {
1220
+ async function evaluate() {
1221
+ const { run } = await import("@mdx-js/mdx");
1222
+ const mod = await run(code, {
1223
+ ...runtime,
1224
+ Fragment,
1225
+ baseUrl: import.meta.url
1226
+ });
1227
+ setContent(() => mod.default);
1228
+ }
1229
+ evaluate();
1230
+ }, [code]);
1231
+ if (!Content) return /* @__PURE__ */ jsx("div", {
1232
+ className,
1233
+ style: {
1234
+ display: "flex",
1235
+ alignItems: "center",
1236
+ justifyContent: "center",
1237
+ width: "100%",
1238
+ height: "100%",
1239
+ ...style
1240
+ },
1241
+ children: /* @__PURE__ */ jsx("div", {
1242
+ style: { opacity: .5 },
1243
+ children: "Loading slides..."
1244
+ })
1245
+ });
1246
+ const allComponents = {
1247
+ ...builtinComponents,
1248
+ ...userComponents
1249
+ };
1250
+ return /* @__PURE__ */ jsx("div", {
1251
+ className,
1252
+ style: {
1253
+ width: "100%",
1254
+ height: "100%",
1255
+ ...style
1256
+ },
1257
+ children: /* @__PURE__ */ jsx(Content, { components: allComponents })
1258
+ });
1259
+ }
1260
+ //#endregion
1261
+ export { Click, ClickNavigation, ClickSteps, CodeEditor, Deck, DeckContext, Draggable, DrawingLayer, GlobalLayer, Mark, Notes, PresenterView, PrintView, ReslideEmbed, Slide, SlideIndexContext, SlotRight, isPresenterView, openPresenterWindow, useDeck, useSlideIndex };