@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.
- package/dist/Mermaid-BtXG2Ea2.mjs +1749 -0
- package/dist/Mermaid-CUQUPHdK.d.mts +240 -0
- package/dist/index.d.mts +3 -238
- package/dist/index.mjs +3 -1748
- package/dist/mdx-components.d.mts +18 -0
- package/dist/mdx-components.mjs +17 -0
- package/package.json +9 -3
|
@@ -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 };
|