@reslide-dev/core 0.1.0 → 0.2.0

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 CHANGED
@@ -123,6 +123,8 @@ interface PresenterViewProps {
123
123
  /**
124
124
  * Presenter view that syncs with the main presentation window.
125
125
  * Shows: current slide, next slide preview, notes, and timer.
126
+ * Supports bidirectional control — navigate from this window to
127
+ * drive the main presentation.
126
128
  */
127
129
  declare function PresenterView({
128
130
  children,
@@ -143,6 +145,8 @@ declare function isPresenterView(): boolean;
143
145
  interface DrawingLayerProps {
144
146
  /** Whether drawing mode is active */
145
147
  active: boolean;
148
+ /** Current slide index — drawings are stored per slide */
149
+ currentSlide: number;
146
150
  /** Pen color */
147
151
  color?: string;
148
152
  /** Pen width */
@@ -150,10 +154,12 @@ interface DrawingLayerProps {
150
154
  }
151
155
  /**
152
156
  * Canvas-based freehand drawing overlay for presentations.
153
- * Toggle with `d` key (handled in Deck).
157
+ * Drawings are stored per slide and persist across navigation.
158
+ * Toggle with `d` key, clear current slide with `c` key.
154
159
  */
155
160
  declare function DrawingLayer({
156
161
  active,
162
+ currentSlide,
157
163
  color,
158
164
  width
159
165
  }: DrawingLayerProps): react_jsx_runtime0.JSX.Element;
@@ -314,6 +320,19 @@ declare function ClickNavigation({
314
320
  disabled
315
321
  }: ClickNavigationProps): react_jsx_runtime0.JSX.Element | null;
316
322
  //#endregion
323
+ //#region src/NavigationBar.d.ts
324
+ /**
325
+ * Slidev-style navigation bar that appears on hover at the bottom of the presentation.
326
+ * Provides buttons for prev/next, overview, fullscreen, presenter, and drawing modes.
327
+ */
328
+ declare function NavigationBar({
329
+ isDrawing,
330
+ onToggleDrawing
331
+ }: {
332
+ isDrawing: boolean;
333
+ onToggleDrawing: () => void;
334
+ }): react_jsx_runtime0.JSX.Element;
335
+ //#endregion
317
336
  //#region src/ProgressBar.d.ts
318
337
  declare function ProgressBar(): react_jsx_runtime0.JSX.Element;
319
338
  //#endregion
@@ -391,4 +410,4 @@ declare function useDeck(): DeckContextValue;
391
410
  declare const SlideIndexContext: react.Context<number | null>;
392
411
  declare function useSlideIndex(): number;
393
412
  //#endregion
394
- export { Click, ClickNavigation, type ClickProps, ClickSteps, CodeEditor, type CodeEditorProps, Deck, type DeckActions, DeckContext, type DeckContextValue, type DeckProps, type DeckState, Draggable, type DraggableProps, DrawingLayer, type DrawingLayerProps, GlobalLayer, type GlobalLayerProps, Mark, type MarkProps, Mermaid, type MermaidProps, Notes, type NotesProps, PresenterView, type PresenterViewProps, PrintView, ProgressBar, ReslideEmbed, type ReslideEmbedProps, Slide, SlideIndexContext, type SlideProps, SlotRight, Toc, type TocProps, type TransitionType, isPresenterView, openPresenterWindow, useDeck, useSlideIndex };
413
+ export { Click, ClickNavigation, type ClickProps, ClickSteps, CodeEditor, type CodeEditorProps, Deck, type DeckActions, DeckContext, type DeckContextValue, type DeckProps, type DeckState, Draggable, type DraggableProps, DrawingLayer, type DrawingLayerProps, GlobalLayer, type GlobalLayerProps, Mark, type MarkProps, Mermaid, type MermaidProps, NavigationBar, Notes, type NotesProps, PresenterView, type PresenterViewProps, PrintView, ProgressBar, ReslideEmbed, type ReslideEmbedProps, Slide, SlideIndexContext, type SlideProps, SlotRight, Toc, type TocProps, type TransitionType, isPresenterView, openPresenterWindow, useDeck, useSlideIndex };
package/dist/index.mjs CHANGED
@@ -78,12 +78,56 @@ function useDeck() {
78
78
  //#region src/DrawingLayer.tsx
79
79
  /**
80
80
  * Canvas-based freehand drawing overlay for presentations.
81
- * Toggle with `d` key (handled in Deck).
81
+ * Drawings are stored per slide and persist across navigation.
82
+ * Toggle with `d` key, clear current slide with `c` key.
82
83
  */
83
- function DrawingLayer({ active, color = "#ef4444", width = 3 }) {
84
+ function DrawingLayer({ active, currentSlide, color = "#ef4444", width = 3 }) {
84
85
  const canvasRef = useRef(null);
85
86
  const [isDrawing, setIsDrawing] = useState(false);
86
87
  const lastPoint = useRef(null);
88
+ const drawingsRef = useRef(/* @__PURE__ */ new Map());
89
+ const prevSlideRef = useRef(currentSlide);
90
+ const getCanvasSize = useCallback(() => {
91
+ const canvas = canvasRef.current;
92
+ if (!canvas) return {
93
+ w: 0,
94
+ h: 0
95
+ };
96
+ return {
97
+ w: canvas.width,
98
+ h: canvas.height
99
+ };
100
+ }, []);
101
+ const saveCurrentSlide = useCallback((slideIndex) => {
102
+ const canvas = canvasRef.current;
103
+ const ctx = canvas?.getContext("2d");
104
+ if (!ctx || !canvas) return;
105
+ const { w, h } = getCanvasSize();
106
+ if (w === 0 || h === 0) return;
107
+ const imageData = ctx.getImageData(0, 0, w, h);
108
+ if (imageData.data.some((_, i) => i % 4 === 3 && imageData.data[i] > 0)) drawingsRef.current.set(slideIndex, imageData);
109
+ else drawingsRef.current.delete(slideIndex);
110
+ }, [getCanvasSize]);
111
+ const restoreSlide = useCallback((slideIndex) => {
112
+ const canvas = canvasRef.current;
113
+ const ctx = canvas?.getContext("2d");
114
+ if (!ctx || !canvas) return;
115
+ const { w, h } = getCanvasSize();
116
+ ctx.clearRect(0, 0, w, h);
117
+ const saved = drawingsRef.current.get(slideIndex);
118
+ if (saved && saved.width === w && saved.height === h) ctx.putImageData(saved, 0, 0);
119
+ }, [getCanvasSize]);
120
+ useEffect(() => {
121
+ if (prevSlideRef.current !== currentSlide) {
122
+ saveCurrentSlide(prevSlideRef.current);
123
+ restoreSlide(currentSlide);
124
+ prevSlideRef.current = currentSlide;
125
+ }
126
+ }, [
127
+ currentSlide,
128
+ saveCurrentSlide,
129
+ restoreSlide
130
+ ]);
87
131
  const getPoint = useCallback((e) => {
88
132
  const canvas = canvasRef.current;
89
133
  const rect = canvas.getBoundingClientRect();
@@ -127,26 +171,35 @@ function DrawingLayer({ active, color = "#ef4444", width = 3 }) {
127
171
  if (!canvas) return;
128
172
  const resize = () => {
129
173
  const rect = canvas.parentElement?.getBoundingClientRect();
130
- if (rect) {
131
- canvas.width = rect.width * window.devicePixelRatio;
132
- canvas.height = rect.height * window.devicePixelRatio;
174
+ if (!rect) return;
175
+ const newWidth = rect.width * window.devicePixelRatio;
176
+ const newHeight = rect.height * window.devicePixelRatio;
177
+ if (canvas.width !== newWidth || canvas.height !== newHeight) {
178
+ saveCurrentSlide(currentSlide);
179
+ drawingsRef.current.clear();
180
+ canvas.width = newWidth;
181
+ canvas.height = newHeight;
133
182
  }
134
183
  };
135
184
  resize();
136
185
  window.addEventListener("resize", resize);
137
186
  return () => window.removeEventListener("resize", resize);
138
- }, []);
187
+ }, [currentSlide, saveCurrentSlide]);
139
188
  useEffect(() => {
140
189
  if (!active) return;
141
190
  function handleKey(e) {
142
191
  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);
192
+ const canvas = canvasRef.current;
193
+ const ctx = canvas?.getContext("2d");
194
+ if (ctx && canvas) {
195
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
196
+ drawingsRef.current.delete(currentSlide);
197
+ }
145
198
  }
146
199
  }
147
200
  window.addEventListener("keydown", handleKey);
148
201
  return () => window.removeEventListener("keydown", handleKey);
149
- }, [active]);
202
+ }, [active, currentSlide]);
150
203
  return /* @__PURE__ */ jsx("canvas", {
151
204
  ref: canvasRef,
152
205
  onMouseDown: startDraw,
@@ -165,6 +218,412 @@ function DrawingLayer({ active, color = "#ef4444", width = 3 }) {
165
218
  });
166
219
  }
167
220
  //#endregion
221
+ //#region src/use-presenter.ts
222
+ const CHANNEL_NAME = "reslide-presenter";
223
+ /**
224
+ * Hook for syncing presentation state across windows via BroadcastChannel.
225
+ * The main presentation window broadcasts state changes and listens for
226
+ * navigation commands from the presenter window.
227
+ */
228
+ function usePresenterSync(currentSlide, clickStep, handlers) {
229
+ const channelRef = useRef(null);
230
+ useEffect(() => {
231
+ if (typeof BroadcastChannel === "undefined") return;
232
+ const channel = new BroadcastChannel(CHANNEL_NAME);
233
+ channelRef.current = channel;
234
+ if (handlers) channel.onmessage = (e) => {
235
+ if (e.data.type === "navigate") switch (e.data.action) {
236
+ case "next":
237
+ handlers.next();
238
+ break;
239
+ case "prev":
240
+ handlers.prev();
241
+ break;
242
+ case "goTo":
243
+ if (e.data.slideIndex != null) handlers.goTo(e.data.slideIndex);
244
+ break;
245
+ }
246
+ };
247
+ return () => {
248
+ channel.close();
249
+ channelRef.current = null;
250
+ };
251
+ }, [handlers]);
252
+ useEffect(() => {
253
+ channelRef.current?.postMessage({
254
+ type: "sync",
255
+ currentSlide,
256
+ clickStep
257
+ });
258
+ }, [currentSlide, clickStep]);
259
+ }
260
+ /**
261
+ * Hook for the presenter window to listen for sync messages and send
262
+ * navigation commands back to the main window.
263
+ */
264
+ function usePresenterChannel(onSync) {
265
+ const channelRef = useRef(null);
266
+ const onSyncRef = useRef(onSync);
267
+ onSyncRef.current = onSync;
268
+ useEffect(() => {
269
+ if (typeof BroadcastChannel === "undefined") return;
270
+ const channel = new BroadcastChannel(CHANNEL_NAME);
271
+ channelRef.current = channel;
272
+ channel.onmessage = (e) => {
273
+ if (e.data.type === "sync") onSyncRef.current(e.data);
274
+ };
275
+ return () => {
276
+ channel.close();
277
+ channelRef.current = null;
278
+ };
279
+ }, []);
280
+ return {
281
+ next: useCallback(() => {
282
+ channelRef.current?.postMessage({
283
+ type: "navigate",
284
+ action: "next"
285
+ });
286
+ }, []),
287
+ prev: useCallback(() => {
288
+ channelRef.current?.postMessage({
289
+ type: "navigate",
290
+ action: "prev"
291
+ });
292
+ }, []),
293
+ goTo: useCallback((index) => {
294
+ channelRef.current?.postMessage({
295
+ type: "navigate",
296
+ action: "goTo",
297
+ slideIndex: index
298
+ });
299
+ }, [])
300
+ };
301
+ }
302
+ /**
303
+ * Opens the presenter window at the /presenter route.
304
+ */
305
+ function openPresenterWindow() {
306
+ const url = new URL(window.location.href);
307
+ url.searchParams.set("presenter", "true");
308
+ window.open(url.toString(), "reslide-presenter", "width=1024,height=768,menubar=no,toolbar=no");
309
+ }
310
+ /**
311
+ * Check if the current window is the presenter view.
312
+ */
313
+ function isPresenterView() {
314
+ if (typeof window === "undefined") return false;
315
+ return new URLSearchParams(window.location.search).get("presenter") === "true";
316
+ }
317
+ //#endregion
318
+ //#region src/NavigationBar.tsx
319
+ /**
320
+ * Slidev-style navigation bar that appears on hover at the bottom of the presentation.
321
+ * Provides buttons for prev/next, overview, fullscreen, presenter, and drawing modes.
322
+ */
323
+ function NavigationBar({ isDrawing, onToggleDrawing }) {
324
+ const { currentSlide, totalSlides, clickStep, totalClickSteps, isOverview, isFullscreen, next, prev, toggleOverview, toggleFullscreen } = useDeck();
325
+ const [visible, setVisible] = useState(false);
326
+ const timerRef = useRef(null);
327
+ const barRef = useRef(null);
328
+ const showBar = useCallback(() => {
329
+ setVisible(true);
330
+ if (timerRef.current) clearTimeout(timerRef.current);
331
+ timerRef.current = setTimeout(() => setVisible(false), 3e3);
332
+ }, []);
333
+ useEffect(() => {
334
+ function handleMouseMove(e) {
335
+ const threshold = window.innerHeight - 80;
336
+ if (e.clientY > threshold) showBar();
337
+ }
338
+ window.addEventListener("mousemove", handleMouseMove);
339
+ return () => window.removeEventListener("mousemove", handleMouseMove);
340
+ }, [showBar]);
341
+ const handleMouseEnter = () => {
342
+ if (timerRef.current) clearTimeout(timerRef.current);
343
+ setVisible(true);
344
+ };
345
+ const handleMouseLeave = () => {
346
+ timerRef.current = setTimeout(() => setVisible(false), 1500);
347
+ };
348
+ return /* @__PURE__ */ jsxs("div", {
349
+ ref: barRef,
350
+ className: "reslide-navbar",
351
+ onMouseEnter: handleMouseEnter,
352
+ onMouseLeave: handleMouseLeave,
353
+ style: {
354
+ position: "absolute",
355
+ bottom: 0,
356
+ left: "50%",
357
+ transform: `translateX(-50%) translateY(${visible ? "0" : "100%"})`,
358
+ display: "flex",
359
+ alignItems: "center",
360
+ gap: "0.25rem",
361
+ padding: "0.375rem 0.75rem",
362
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
363
+ backdropFilter: "blur(8px)",
364
+ borderRadius: "0.5rem 0.5rem 0 0",
365
+ transition: "transform 0.25s ease, opacity 0.25s ease",
366
+ opacity: visible ? 1 : 0,
367
+ zIndex: 200,
368
+ color: "#e2e8f0",
369
+ fontSize: "0.8125rem",
370
+ fontFamily: "system-ui, sans-serif",
371
+ fontVariantNumeric: "tabular-nums",
372
+ pointerEvents: visible ? "auto" : "none"
373
+ },
374
+ children: [
375
+ /* @__PURE__ */ jsx(NavButton, {
376
+ onClick: prev,
377
+ title: "Previous (←)",
378
+ disabled: isOverview,
379
+ children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "left" })
380
+ }),
381
+ /* @__PURE__ */ jsxs("span", {
382
+ style: {
383
+ padding: "0 0.5rem",
384
+ userSelect: "none",
385
+ whiteSpace: "nowrap"
386
+ },
387
+ children: [
388
+ currentSlide + 1,
389
+ " / ",
390
+ totalSlides,
391
+ clickStep > 0 && totalClickSteps > 0 && /* @__PURE__ */ jsxs("span", {
392
+ style: {
393
+ opacity: .6,
394
+ marginLeft: "0.25rem"
395
+ },
396
+ children: [
397
+ "(",
398
+ clickStep,
399
+ "/",
400
+ totalClickSteps,
401
+ ")"
402
+ ]
403
+ })
404
+ ]
405
+ }),
406
+ /* @__PURE__ */ jsx(NavButton, {
407
+ onClick: next,
408
+ title: "Next (→ / Space)",
409
+ disabled: isOverview,
410
+ children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "right" })
411
+ }),
412
+ /* @__PURE__ */ jsx(Divider, {}),
413
+ /* @__PURE__ */ jsx(NavButton, {
414
+ onClick: toggleOverview,
415
+ title: "Overview (Esc)",
416
+ active: isOverview,
417
+ children: /* @__PURE__ */ jsx(GridIcon, {})
418
+ }),
419
+ /* @__PURE__ */ jsx(NavButton, {
420
+ onClick: toggleFullscreen,
421
+ title: "Fullscreen (f)",
422
+ active: isFullscreen,
423
+ children: /* @__PURE__ */ jsx(FullscreenIcon, { expanded: isFullscreen })
424
+ }),
425
+ /* @__PURE__ */ jsx(NavButton, {
426
+ onClick: openPresenterWindow,
427
+ title: "Presenter (p)",
428
+ children: /* @__PURE__ */ jsx(PresenterIcon, {})
429
+ }),
430
+ /* @__PURE__ */ jsx(NavButton, {
431
+ onClick: onToggleDrawing,
432
+ title: "Drawing (d)",
433
+ active: isDrawing,
434
+ children: /* @__PURE__ */ jsx(PenIcon, {})
435
+ })
436
+ ]
437
+ });
438
+ }
439
+ function NavButton({ children, onClick, title, active, disabled }) {
440
+ return /* @__PURE__ */ jsx("button", {
441
+ type: "button",
442
+ onClick,
443
+ title,
444
+ disabled,
445
+ style: {
446
+ display: "flex",
447
+ alignItems: "center",
448
+ justifyContent: "center",
449
+ width: "2rem",
450
+ height: "2rem",
451
+ background: active ? "rgba(255,255,255,0.2)" : "none",
452
+ border: "none",
453
+ borderRadius: "0.25rem",
454
+ cursor: disabled ? "default" : "pointer",
455
+ color: active ? "#fff" : "#cbd5e1",
456
+ opacity: disabled ? .4 : 1,
457
+ transition: "background 0.15s, color 0.15s",
458
+ padding: 0
459
+ },
460
+ onMouseEnter: (e) => {
461
+ if (!disabled) e.currentTarget.style.background = "rgba(255,255,255,0.15)";
462
+ },
463
+ onMouseLeave: (e) => {
464
+ e.currentTarget.style.background = active ? "rgba(255,255,255,0.2)" : "none";
465
+ },
466
+ children
467
+ });
468
+ }
469
+ function Divider() {
470
+ return /* @__PURE__ */ jsx("div", { style: {
471
+ width: 1,
472
+ height: "1.25rem",
473
+ backgroundColor: "rgba(255,255,255,0.2)",
474
+ margin: "0 0.25rem"
475
+ } });
476
+ }
477
+ function ArrowIcon({ direction }) {
478
+ return /* @__PURE__ */ jsx("svg", {
479
+ width: "16",
480
+ height: "16",
481
+ viewBox: "0 0 24 24",
482
+ fill: "none",
483
+ stroke: "currentColor",
484
+ strokeWidth: "2",
485
+ strokeLinecap: "round",
486
+ strokeLinejoin: "round",
487
+ children: direction === "left" ? /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) : /* @__PURE__ */ jsx("polyline", { points: "9 6 15 12 9 18" })
488
+ });
489
+ }
490
+ function GridIcon() {
491
+ return /* @__PURE__ */ jsxs("svg", {
492
+ width: "16",
493
+ height: "16",
494
+ viewBox: "0 0 24 24",
495
+ fill: "none",
496
+ stroke: "currentColor",
497
+ strokeWidth: "2",
498
+ strokeLinecap: "round",
499
+ strokeLinejoin: "round",
500
+ children: [
501
+ /* @__PURE__ */ jsx("rect", {
502
+ x: "3",
503
+ y: "3",
504
+ width: "7",
505
+ height: "7"
506
+ }),
507
+ /* @__PURE__ */ jsx("rect", {
508
+ x: "14",
509
+ y: "3",
510
+ width: "7",
511
+ height: "7"
512
+ }),
513
+ /* @__PURE__ */ jsx("rect", {
514
+ x: "3",
515
+ y: "14",
516
+ width: "7",
517
+ height: "7"
518
+ }),
519
+ /* @__PURE__ */ jsx("rect", {
520
+ x: "14",
521
+ y: "14",
522
+ width: "7",
523
+ height: "7"
524
+ })
525
+ ]
526
+ });
527
+ }
528
+ function FullscreenIcon({ expanded }) {
529
+ return /* @__PURE__ */ jsx("svg", {
530
+ width: "16",
531
+ height: "16",
532
+ viewBox: "0 0 24 24",
533
+ fill: "none",
534
+ stroke: "currentColor",
535
+ strokeWidth: "2",
536
+ strokeLinecap: "round",
537
+ strokeLinejoin: "round",
538
+ children: expanded ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
539
+ /* @__PURE__ */ jsx("polyline", { points: "4 14 10 14 10 20" }),
540
+ /* @__PURE__ */ jsx("polyline", { points: "20 10 14 10 14 4" }),
541
+ /* @__PURE__ */ jsx("line", {
542
+ x1: "14",
543
+ y1: "10",
544
+ x2: "21",
545
+ y2: "3"
546
+ }),
547
+ /* @__PURE__ */ jsx("line", {
548
+ x1: "3",
549
+ y1: "21",
550
+ x2: "10",
551
+ y2: "14"
552
+ })
553
+ ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
554
+ /* @__PURE__ */ jsx("polyline", { points: "15 3 21 3 21 9" }),
555
+ /* @__PURE__ */ jsx("polyline", { points: "9 21 3 21 3 15" }),
556
+ /* @__PURE__ */ jsx("line", {
557
+ x1: "21",
558
+ y1: "3",
559
+ x2: "14",
560
+ y2: "10"
561
+ }),
562
+ /* @__PURE__ */ jsx("line", {
563
+ x1: "3",
564
+ y1: "21",
565
+ x2: "10",
566
+ y2: "14"
567
+ })
568
+ ] })
569
+ });
570
+ }
571
+ function PresenterIcon() {
572
+ return /* @__PURE__ */ jsxs("svg", {
573
+ width: "16",
574
+ height: "16",
575
+ viewBox: "0 0 24 24",
576
+ fill: "none",
577
+ stroke: "currentColor",
578
+ strokeWidth: "2",
579
+ strokeLinecap: "round",
580
+ strokeLinejoin: "round",
581
+ children: [
582
+ /* @__PURE__ */ jsx("rect", {
583
+ x: "2",
584
+ y: "3",
585
+ width: "20",
586
+ height: "14",
587
+ rx: "2"
588
+ }),
589
+ /* @__PURE__ */ jsx("line", {
590
+ x1: "8",
591
+ y1: "21",
592
+ x2: "16",
593
+ y2: "21"
594
+ }),
595
+ /* @__PURE__ */ jsx("line", {
596
+ x1: "12",
597
+ y1: "17",
598
+ x2: "12",
599
+ y2: "21"
600
+ })
601
+ ]
602
+ });
603
+ }
604
+ function PenIcon() {
605
+ return /* @__PURE__ */ jsxs("svg", {
606
+ width: "16",
607
+ height: "16",
608
+ viewBox: "0 0 24 24",
609
+ fill: "none",
610
+ stroke: "currentColor",
611
+ strokeWidth: "2",
612
+ strokeLinecap: "round",
613
+ strokeLinejoin: "round",
614
+ children: [
615
+ /* @__PURE__ */ jsx("path", { d: "M12 19l7-7 3 3-7 7-3-3z" }),
616
+ /* @__PURE__ */ jsx("path", { d: "M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" }),
617
+ /* @__PURE__ */ jsx("path", { d: "M2 2l7.586 7.586" }),
618
+ /* @__PURE__ */ jsx("circle", {
619
+ cx: "11",
620
+ cy: "11",
621
+ r: "2"
622
+ })
623
+ ]
624
+ });
625
+ }
626
+ //#endregion
168
627
  //#region src/slide-context.ts
169
628
  const SlideIndexContext = createContext(null);
170
629
  function useSlideIndex() {
@@ -248,7 +707,6 @@ function SlideTransition({ children, currentSlide, transition }) {
248
707
  value: displaySlide,
249
708
  children: /* @__PURE__ */ jsx("div", {
250
709
  className: "reslide-transition-slide",
251
- style: { position: "relative" },
252
710
  children: slides[displaySlide]
253
711
  })
254
712
  })
@@ -298,50 +756,6 @@ function useFullscreen(ref) {
298
756
  };
299
757
  }
300
758
  //#endregion
301
- //#region src/use-presenter.ts
302
- const CHANNEL_NAME = "reslide-presenter";
303
- /**
304
- * Hook for syncing presentation state across windows via BroadcastChannel.
305
- * The main presentation window broadcasts state changes.
306
- */
307
- function usePresenterSync(currentSlide, clickStep, onReceive) {
308
- const channelRef = useRef(null);
309
- useEffect(() => {
310
- if (typeof BroadcastChannel === "undefined") return;
311
- const channel = new BroadcastChannel(CHANNEL_NAME);
312
- channelRef.current = channel;
313
- if (onReceive) channel.onmessage = (e) => {
314
- onReceive(e.data);
315
- };
316
- return () => {
317
- channel.close();
318
- channelRef.current = null;
319
- };
320
- }, [onReceive]);
321
- useEffect(() => {
322
- channelRef.current?.postMessage({
323
- type: "sync",
324
- currentSlide,
325
- clickStep
326
- });
327
- }, [currentSlide, clickStep]);
328
- }
329
- /**
330
- * Opens the presenter window at the /presenter route.
331
- */
332
- function openPresenterWindow() {
333
- const url = new URL(window.location.href);
334
- url.searchParams.set("presenter", "true");
335
- window.open(url.toString(), "reslide-presenter", "width=1024,height=768,menubar=no,toolbar=no");
336
- }
337
- /**
338
- * Check if the current window is the presenter view.
339
- */
340
- function isPresenterView() {
341
- if (typeof window === "undefined") return false;
342
- return new URLSearchParams(window.location.search).get("presenter") === "true";
343
- }
344
- //#endregion
345
759
  //#region src/Deck.tsx
346
760
  function Deck({ children, transition = "none" }) {
347
761
  const containerRef = useRef(null);
@@ -352,7 +766,6 @@ function Deck({ children, transition = "none" }) {
352
766
  const [isPrinting, setIsPrinting] = useState(false);
353
767
  const [clickStepsMap, setClickStepsMap] = useState({});
354
768
  const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
355
- usePresenterSync(currentSlide, clickStep);
356
769
  const totalSlides = Children.count(children);
357
770
  const totalClickSteps = clickStepsMap[currentSlide] ?? 0;
358
771
  const registerClickSteps = useCallback((slideIndex, count) => {
@@ -399,6 +812,15 @@ function Deck({ children, transition = "none" }) {
399
812
  if (isOverview) setIsOverview(false);
400
813
  }
401
814
  }, [totalSlides, isOverview]);
815
+ usePresenterSync(currentSlide, clickStep, useMemo(() => ({
816
+ next,
817
+ prev,
818
+ goTo
819
+ }), [
820
+ next,
821
+ prev,
822
+ goTo
823
+ ]));
402
824
  const toggleOverview = useCallback(() => {
403
825
  setIsOverview((v) => !v);
404
826
  }, []);
@@ -512,12 +934,15 @@ function Deck({ children, transition = "none" }) {
512
934
  onNext: next,
513
935
  disabled: isDrawing
514
936
  }),
515
- !isOverview && !isPrinting && /* @__PURE__ */ jsx(SlideNumber, {
516
- current: currentSlide + 1,
517
- total: totalSlides
518
- }),
519
937
  !isOverview && !isPrinting && /* @__PURE__ */ jsx(ProgressBar, {}),
520
- !isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, { active: isDrawing })
938
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, {
939
+ active: isDrawing,
940
+ currentSlide
941
+ }),
942
+ !isPrinting && /* @__PURE__ */ jsx(NavigationBar, {
943
+ isDrawing,
944
+ onToggleDrawing: () => setIsDrawing((v) => !v)
945
+ })
521
946
  ]
522
947
  })
523
948
  });
@@ -564,24 +989,6 @@ function OverviewGrid({ children, totalSlides, goTo }) {
564
989
  }, i))
565
990
  });
566
991
  }
567
- function SlideNumber({ current, total }) {
568
- return /* @__PURE__ */ jsxs("div", {
569
- className: "reslide-slide-number",
570
- style: {
571
- position: "absolute",
572
- bottom: "1rem",
573
- right: "1rem",
574
- fontSize: "0.875rem",
575
- opacity: .6,
576
- fontVariantNumeric: "tabular-nums"
577
- },
578
- children: [
579
- current,
580
- " / ",
581
- total
582
- ]
583
- });
584
- }
585
992
  //#endregion
586
993
  //#region src/Slide.tsx
587
994
  const baseStyle = {
@@ -851,6 +1258,8 @@ SlotRight.__reslideSlot = "right";
851
1258
  /**
852
1259
  * Presenter view that syncs with the main presentation window.
853
1260
  * Shows: current slide, next slide preview, notes, and timer.
1261
+ * Supports bidirectional control — navigate from this window to
1262
+ * drive the main presentation.
854
1263
  */
855
1264
  function PresenterView({ children, notes }) {
856
1265
  const [currentSlide, setCurrentSlide] = useState(0);
@@ -858,17 +1267,28 @@ function PresenterView({ children, notes }) {
858
1267
  const [elapsed, setElapsed] = useState(0);
859
1268
  const slides = Children.toArray(children);
860
1269
  const totalSlides = slides.length;
1270
+ const { next, prev, goTo } = usePresenterChannel((msg) => {
1271
+ setCurrentSlide(msg.currentSlide);
1272
+ setClickStep(msg.clickStep);
1273
+ });
861
1274
  useEffect(() => {
862
- if (typeof BroadcastChannel === "undefined") return;
863
- const channel = new BroadcastChannel("reslide-presenter");
864
- channel.onmessage = (e) => {
865
- if (e.data.type === "sync") {
866
- setCurrentSlide(e.data.currentSlide);
867
- setClickStep(e.data.clickStep);
1275
+ function handleKeyDown(e) {
1276
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
1277
+ switch (e.key) {
1278
+ case "ArrowRight":
1279
+ case " ":
1280
+ e.preventDefault();
1281
+ next();
1282
+ break;
1283
+ case "ArrowLeft":
1284
+ e.preventDefault();
1285
+ prev();
1286
+ break;
868
1287
  }
869
- };
870
- return () => channel.close();
871
- }, []);
1288
+ }
1289
+ window.addEventListener("keydown", handleKeyDown);
1290
+ return () => window.removeEventListener("keydown", handleKeyDown);
1291
+ }, [next, prev]);
872
1292
  useEffect(() => {
873
1293
  const start = Date.now();
874
1294
  const interval = setInterval(() => {
@@ -881,21 +1301,31 @@ function PresenterView({ children, notes }) {
881
1301
  const s = seconds % 60;
882
1302
  return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
883
1303
  };
1304
+ const noopReg = useCallback((_i, _c) => {}, []);
884
1305
  const noop = useCallback(() => {}, []);
885
- const contextValue = {
1306
+ const contextValue = useMemo(() => ({
886
1307
  currentSlide,
887
1308
  totalSlides,
888
1309
  clickStep,
889
1310
  totalClickSteps: 0,
890
1311
  isOverview: false,
891
1312
  isFullscreen: false,
892
- next: noop,
893
- prev: noop,
894
- goTo: useCallback((_n) => {}, []),
1313
+ next,
1314
+ prev,
1315
+ goTo,
895
1316
  toggleOverview: noop,
896
1317
  toggleFullscreen: noop,
897
- registerClickSteps: useCallback((_i, _c) => {}, [])
898
- };
1318
+ registerClickSteps: noopReg
1319
+ }), [
1320
+ currentSlide,
1321
+ totalSlides,
1322
+ clickStep,
1323
+ next,
1324
+ prev,
1325
+ goTo,
1326
+ noop,
1327
+ noopReg
1328
+ ]);
899
1329
  return /* @__PURE__ */ jsx(DeckContext.Provider, {
900
1330
  value: contextValue,
901
1331
  children: /* @__PURE__ */ jsxs("div", {
@@ -989,37 +1419,88 @@ function PresenterView({ children, notes }) {
989
1419
  backgroundColor: "#0f172a",
990
1420
  borderRadius: "0.5rem"
991
1421
  },
992
- children: [/* @__PURE__ */ jsx("div", {
993
- style: {
994
- fontSize: "1.5rem",
995
- fontVariantNumeric: "tabular-nums",
996
- fontWeight: 700
997
- },
998
- children: formatTime(elapsed)
999
- }), /* @__PURE__ */ jsxs("div", {
1000
- style: {
1001
- fontSize: "1.125rem",
1002
- fontVariantNumeric: "tabular-nums"
1003
- },
1004
- children: [
1005
- currentSlide + 1,
1006
- " / ",
1007
- totalSlides,
1008
- clickStep > 0 && /* @__PURE__ */ jsxs("span", {
1009
- style: { color: "#94a3b8" },
1010
- children: [
1011
- " (click ",
1012
- clickStep,
1013
- ")"
1014
- ]
1015
- })
1016
- ]
1017
- })]
1422
+ children: [
1423
+ /* @__PURE__ */ jsx("div", {
1424
+ style: {
1425
+ fontSize: "1.5rem",
1426
+ fontVariantNumeric: "tabular-nums",
1427
+ fontWeight: 700
1428
+ },
1429
+ children: formatTime(elapsed)
1430
+ }),
1431
+ /* @__PURE__ */ jsxs("div", {
1432
+ style: {
1433
+ display: "flex",
1434
+ alignItems: "center",
1435
+ gap: "0.5rem"
1436
+ },
1437
+ children: [
1438
+ /* @__PURE__ */ jsx(PresenterNavButton, {
1439
+ onClick: prev,
1440
+ title: "Previous (←)",
1441
+ children: "◀"
1442
+ }),
1443
+ /* @__PURE__ */ jsxs("span", {
1444
+ style: {
1445
+ fontSize: "1.125rem",
1446
+ fontVariantNumeric: "tabular-nums"
1447
+ },
1448
+ children: [
1449
+ currentSlide + 1,
1450
+ " / ",
1451
+ totalSlides,
1452
+ clickStep > 0 && /* @__PURE__ */ jsxs("span", {
1453
+ style: { color: "#94a3b8" },
1454
+ children: [
1455
+ " (click ",
1456
+ clickStep,
1457
+ ")"
1458
+ ]
1459
+ })
1460
+ ]
1461
+ }),
1462
+ /* @__PURE__ */ jsx(PresenterNavButton, {
1463
+ onClick: next,
1464
+ title: "Next (→ / Space)",
1465
+ children: "▶"
1466
+ })
1467
+ ]
1468
+ }),
1469
+ /* @__PURE__ */ jsx("div", { style: { width: "5rem" } })
1470
+ ]
1018
1471
  })
1019
1472
  ]
1020
1473
  })
1021
1474
  });
1022
1475
  }
1476
+ function PresenterNavButton({ children, onClick, title }) {
1477
+ return /* @__PURE__ */ jsx("button", {
1478
+ type: "button",
1479
+ onClick,
1480
+ title,
1481
+ style: {
1482
+ display: "flex",
1483
+ alignItems: "center",
1484
+ justifyContent: "center",
1485
+ width: "2.25rem",
1486
+ height: "2.25rem",
1487
+ background: "rgba(255,255,255,0.1)",
1488
+ border: "1px solid rgba(255,255,255,0.15)",
1489
+ borderRadius: "0.375rem",
1490
+ cursor: "pointer",
1491
+ color: "#e2e8f0",
1492
+ fontSize: "0.875rem",
1493
+ transition: "background 0.15s"
1494
+ },
1495
+ onMouseEnter: (e) => {
1496
+ e.currentTarget.style.background = "rgba(255,255,255,0.2)";
1497
+ },
1498
+ onMouseLeave: (e) => {
1499
+ e.currentTarget.style.background = "rgba(255,255,255,0.1)";
1500
+ },
1501
+ children
1502
+ });
1503
+ }
1023
1504
  //#endregion
1024
1505
  //#region src/CodeEditor.tsx
1025
1506
  /**
@@ -1441,4 +1922,4 @@ function ReslideEmbed({ code, transition, components: userComponents, className,
1441
1922
  });
1442
1923
  }
1443
1924
  //#endregion
1444
- export { Click, ClickNavigation, ClickSteps, CodeEditor, Deck, DeckContext, Draggable, DrawingLayer, GlobalLayer, Mark, Mermaid, Notes, PresenterView, PrintView, ProgressBar, ReslideEmbed, Slide, SlideIndexContext, SlotRight, Toc, isPresenterView, openPresenterWindow, useDeck, useSlideIndex };
1925
+ export { Click, ClickNavigation, ClickSteps, CodeEditor, Deck, DeckContext, Draggable, DrawingLayer, GlobalLayer, Mark, Mermaid, NavigationBar, Notes, PresenterView, PrintView, ProgressBar, ReslideEmbed, Slide, SlideIndexContext, SlotRight, Toc, isPresenterView, openPresenterWindow, useDeck, useSlideIndex };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reslide-dev/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-agnostic React presentation components",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "type": "module",
11
11
  "exports": {
12
- ".": "./dist/index.mjs",
12
+ ".": "./src/index.ts",
13
13
  "./mdx-components": "./src/mdx-components.ts",
14
14
  "./themes/default.css": "./src/themes/default.css",
15
15
  "./themes/dark.css": "./src/themes/dark.css",
@@ -21,22 +21,22 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
- "@mdx-js/mdx": "^3.1.1"
24
+ "@mdx-js/mdx": "3.1.1"
25
25
  },
26
26
  "devDependencies": {
27
- "@testing-library/dom": "^10.4.1",
28
- "@testing-library/react": "^16.3.2",
29
- "@types/react": "^19",
30
- "@types/react-dom": "^19",
31
- "jsdom": "^29.0.0",
32
- "react": "^19.1.0",
33
- "react-dom": "^19.1.0",
27
+ "@testing-library/dom": "10.4.1",
28
+ "@testing-library/react": "16.3.2",
29
+ "@types/react": "19.2.14",
30
+ "@types/react-dom": "19.2.3",
31
+ "jsdom": "29.0.1",
32
+ "react": "19.2.4",
33
+ "react-dom": "19.2.4",
34
34
  "vite-plus": "latest",
35
35
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
36
36
  },
37
37
  "peerDependencies": {
38
- "react": "^19.0.0",
39
- "react-dom": "^19.0.0"
38
+ "react": ">=19.0.0",
39
+ "react-dom": ">=19.0.0"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "vp pack",