@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.d.mts +340 -0
- package/dist/index.mjs +1261 -0
- package/package.json +41 -0
- package/src/themes/dark.css +86 -0
- package/src/themes/default.css +86 -0
- package/src/themes/print.css +58 -0
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 };
|