@reslide-dev/core 0.0.1 → 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;
@@ -245,6 +251,57 @@ declare function Draggable({
245
251
  style
246
252
  }: DraggableProps): react_jsx_runtime0.JSX.Element;
247
253
  //#endregion
254
+ //#region src/Toc.d.ts
255
+ interface TocProps {
256
+ /** The slide elements to extract headings from */
257
+ children: ReactNode;
258
+ /** Additional CSS class name */
259
+ className?: string;
260
+ /** Additional inline styles */
261
+ style?: CSSProperties;
262
+ }
263
+ /**
264
+ * Table of Contents component that renders a clickable list of slides
265
+ * with their heading text extracted from h1/h2 elements.
266
+ *
267
+ * Must be rendered inside a `<Deck>` component.
268
+ *
269
+ * ```tsx
270
+ * <Toc>
271
+ * <Slide><h1>Introduction</h1></Slide>
272
+ * <Slide><h2>Agenda</h2></Slide>
273
+ * </Toc>
274
+ * ```
275
+ */
276
+ declare function Toc({
277
+ children,
278
+ className,
279
+ style
280
+ }: TocProps): react_jsx_runtime0.JSX.Element;
281
+ //#endregion
282
+ //#region src/Mermaid.d.ts
283
+ interface MermaidProps {
284
+ /** Mermaid diagram code */
285
+ children: string;
286
+ }
287
+ /**
288
+ * Renders a Mermaid diagram.
289
+ *
290
+ * Usage in MDX:
291
+ * ```mdx
292
+ * <Mermaid>
293
+ * graph TD
294
+ * A --> B
295
+ * </Mermaid>
296
+ * ```
297
+ *
298
+ * The mermaid library is dynamically imported on the client side,
299
+ * so it does not need to be installed as a project dependency.
300
+ */
301
+ declare function Mermaid({
302
+ children
303
+ }: MermaidProps): react_jsx_runtime0.JSX.Element;
304
+ //#endregion
248
305
  //#region src/ClickNavigation.d.ts
249
306
  interface ClickNavigationProps {
250
307
  onPrev: () => void;
@@ -263,6 +320,22 @@ declare function ClickNavigation({
263
320
  disabled
264
321
  }: ClickNavigationProps): react_jsx_runtime0.JSX.Element | null;
265
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
336
+ //#region src/ProgressBar.d.ts
337
+ declare function ProgressBar(): react_jsx_runtime0.JSX.Element;
338
+ //#endregion
266
339
  //#region src/ReslideEmbed.d.ts
267
340
  interface ReslideEmbedProps {
268
341
  /** Compiled MDX code from compileMdxSlides() */
@@ -337,4 +410,4 @@ declare function useDeck(): DeckContextValue;
337
410
  declare const SlideIndexContext: react.Context<number | null>;
338
411
  declare function useSlideIndex(): number;
339
412
  //#endregion
340
- 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, Notes, type NotesProps, PresenterView, type PresenterViewProps, PrintView, ReslideEmbed, type ReslideEmbedProps, Slide, SlideIndexContext, type SlideProps, SlotRight, 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
@@ -1,4 +1,4 @@
1
- import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
1
+ import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
2
2
  import * as runtime from "react/jsx-runtime";
3
3
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/ClickNavigation.tsx
@@ -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() {
@@ -188,6 +647,30 @@ function PrintView({ children }) {
188
647
  });
189
648
  }
190
649
  //#endregion
650
+ //#region src/ProgressBar.tsx
651
+ function ProgressBar() {
652
+ const { currentSlide, totalSlides, clickStep, totalClickSteps } = useDeck();
653
+ const slideProgress = totalSlides <= 1 ? 1 : currentSlide / (totalSlides - 1);
654
+ const clickFraction = totalSlides <= 1 ? 0 : totalClickSteps > 0 ? clickStep / totalClickSteps * (1 / (totalSlides - 1)) : 0;
655
+ return /* @__PURE__ */ jsx("div", {
656
+ className: "reslide-progress-bar",
657
+ style: {
658
+ position: "absolute",
659
+ top: 0,
660
+ left: 0,
661
+ width: "100%",
662
+ height: "3px",
663
+ zIndex: 100
664
+ },
665
+ children: /* @__PURE__ */ jsx("div", { style: {
666
+ height: "100%",
667
+ width: `${Math.min(slideProgress + clickFraction, 1) * 100}%`,
668
+ backgroundColor: "var(--slide-accent, #3b82f6)",
669
+ transition: "width 0.3s ease"
670
+ } })
671
+ });
672
+ }
673
+ //#endregion
191
674
  //#region src/SlideTransition.tsx
192
675
  const DURATION = 300;
193
676
  function SlideTransition({ children, currentSlide, transition }) {
@@ -195,14 +678,17 @@ function SlideTransition({ children, currentSlide, transition }) {
195
678
  const [displaySlide, setDisplaySlide] = useState(currentSlide);
196
679
  const [prevSlide, setPrevSlide] = useState(null);
197
680
  const [isAnimating, setIsAnimating] = useState(false);
681
+ const prevCurrentRef = useRef(currentSlide);
198
682
  const timerRef = useRef(null);
199
683
  useEffect(() => {
200
- if (currentSlide === displaySlide) return;
684
+ if (currentSlide === prevCurrentRef.current) return;
685
+ const from = prevCurrentRef.current;
686
+ prevCurrentRef.current = currentSlide;
201
687
  if (transition === "none") {
202
688
  setDisplaySlide(currentSlide);
203
689
  return;
204
690
  }
205
- setPrevSlide(displaySlide);
691
+ setPrevSlide(from);
206
692
  setDisplaySlide(currentSlide);
207
693
  setIsAnimating(true);
208
694
  if (timerRef.current) clearTimeout(timerRef.current);
@@ -213,11 +699,7 @@ function SlideTransition({ children, currentSlide, transition }) {
213
699
  return () => {
214
700
  if (timerRef.current) clearTimeout(timerRef.current);
215
701
  };
216
- }, [
217
- currentSlide,
218
- displaySlide,
219
- transition
220
- ]);
702
+ }, [currentSlide, transition]);
221
703
  const resolvedTransition = resolveTransition(transition, prevSlide, displaySlide);
222
704
  if (transition === "none" || !isAnimating) return /* @__PURE__ */ jsx("div", {
223
705
  className: "reslide-transition-container",
@@ -225,7 +707,6 @@ function SlideTransition({ children, currentSlide, transition }) {
225
707
  value: displaySlide,
226
708
  children: /* @__PURE__ */ jsx("div", {
227
709
  className: "reslide-transition-slide",
228
- style: { position: "relative" },
229
710
  children: slides[displaySlide]
230
711
  })
231
712
  })
@@ -275,50 +756,6 @@ function useFullscreen(ref) {
275
756
  };
276
757
  }
277
758
  //#endregion
278
- //#region src/use-presenter.ts
279
- const CHANNEL_NAME = "reslide-presenter";
280
- /**
281
- * Hook for syncing presentation state across windows via BroadcastChannel.
282
- * The main presentation window broadcasts state changes.
283
- */
284
- function usePresenterSync(currentSlide, clickStep, onReceive) {
285
- const channelRef = useRef(null);
286
- useEffect(() => {
287
- if (typeof BroadcastChannel === "undefined") return;
288
- const channel = new BroadcastChannel(CHANNEL_NAME);
289
- channelRef.current = channel;
290
- if (onReceive) channel.onmessage = (e) => {
291
- onReceive(e.data);
292
- };
293
- return () => {
294
- channel.close();
295
- channelRef.current = null;
296
- };
297
- }, [onReceive]);
298
- useEffect(() => {
299
- channelRef.current?.postMessage({
300
- type: "sync",
301
- currentSlide,
302
- clickStep
303
- });
304
- }, [currentSlide, clickStep]);
305
- }
306
- /**
307
- * Opens the presenter window at the /presenter route.
308
- */
309
- function openPresenterWindow() {
310
- const url = new URL(window.location.href);
311
- url.searchParams.set("presenter", "true");
312
- window.open(url.toString(), "reslide-presenter", "width=1024,height=768,menubar=no,toolbar=no");
313
- }
314
- /**
315
- * Check if the current window is the presenter view.
316
- */
317
- function isPresenterView() {
318
- if (typeof window === "undefined") return false;
319
- return new URLSearchParams(window.location.search).get("presenter") === "true";
320
- }
321
- //#endregion
322
759
  //#region src/Deck.tsx
323
760
  function Deck({ children, transition = "none" }) {
324
761
  const containerRef = useRef(null);
@@ -329,7 +766,6 @@ function Deck({ children, transition = "none" }) {
329
766
  const [isPrinting, setIsPrinting] = useState(false);
330
767
  const [clickStepsMap, setClickStepsMap] = useState({});
331
768
  const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
332
- usePresenterSync(currentSlide, clickStep);
333
769
  const totalSlides = Children.count(children);
334
770
  const totalClickSteps = clickStepsMap[currentSlide] ?? 0;
335
771
  const registerClickSteps = useCallback((slideIndex, count) => {
@@ -376,6 +812,15 @@ function Deck({ children, transition = "none" }) {
376
812
  if (isOverview) setIsOverview(false);
377
813
  }
378
814
  }, [totalSlides, isOverview]);
815
+ usePresenterSync(currentSlide, clickStep, useMemo(() => ({
816
+ next,
817
+ prev,
818
+ goTo
819
+ }), [
820
+ next,
821
+ prev,
822
+ goTo
823
+ ]));
379
824
  const toggleOverview = useCallback(() => {
380
825
  setIsOverview((v) => !v);
381
826
  }, []);
@@ -489,11 +934,15 @@ function Deck({ children, transition = "none" }) {
489
934
  onNext: next,
490
935
  disabled: isDrawing
491
936
  }),
492
- !isOverview && !isPrinting && /* @__PURE__ */ jsx(SlideNumber, {
493
- current: currentSlide + 1,
494
- total: totalSlides
937
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(ProgressBar, {}),
938
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, {
939
+ active: isDrawing,
940
+ currentSlide
495
941
  }),
496
- !isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, { active: isDrawing })
942
+ !isPrinting && /* @__PURE__ */ jsx(NavigationBar, {
943
+ isDrawing,
944
+ onToggleDrawing: () => setIsDrawing((v) => !v)
945
+ })
497
946
  ]
498
947
  })
499
948
  });
@@ -540,24 +989,6 @@ function OverviewGrid({ children, totalSlides, goTo }) {
540
989
  }, i))
541
990
  });
542
991
  }
543
- function SlideNumber({ current, total }) {
544
- return /* @__PURE__ */ jsxs("div", {
545
- className: "reslide-slide-number",
546
- style: {
547
- position: "absolute",
548
- bottom: "1rem",
549
- right: "1rem",
550
- fontSize: "0.875rem",
551
- opacity: .6,
552
- fontVariantNumeric: "tabular-nums"
553
- },
554
- children: [
555
- current,
556
- " / ",
557
- total
558
- ]
559
- });
560
- }
561
992
  //#endregion
562
993
  //#region src/Slide.tsx
563
994
  const baseStyle = {
@@ -755,19 +1186,19 @@ function ClickSteps({ count, slideIndex }) {
755
1186
  //#endregion
756
1187
  //#region src/Mark.tsx
757
1188
  const markStyles = {
758
- highlight: (color) => ({
759
- backgroundColor: `var(--mark-${color}, ${color})`,
1189
+ highlight: (colorName, resolvedColor) => ({
1190
+ backgroundColor: `var(--mark-${colorName}, ${resolvedColor})`,
760
1191
  padding: "0.1em 0.2em",
761
1192
  borderRadius: "0.2em"
762
1193
  }),
763
- underline: (color) => ({
1194
+ underline: (colorName, resolvedColor) => ({
764
1195
  textDecoration: "underline",
765
- textDecorationColor: `var(--mark-${color}, ${color})`,
1196
+ textDecorationColor: `var(--mark-${colorName}, ${resolvedColor})`,
766
1197
  textDecorationThickness: "0.15em",
767
1198
  textUnderlineOffset: "0.15em"
768
1199
  }),
769
- circle: (color) => ({
770
- border: `0.15em solid var(--mark-${color}, ${color})`,
1200
+ circle: (colorName, resolvedColor) => ({
1201
+ border: `0.15em solid var(--mark-${colorName}, ${resolvedColor})`,
771
1202
  borderRadius: "50%",
772
1203
  padding: "0.1em 0.3em"
773
1204
  })
@@ -785,7 +1216,7 @@ function Mark({ children, type = "highlight", color = "yellow" }) {
785
1216
  const styleFn = markStyles[type] ?? markStyles.highlight;
786
1217
  return /* @__PURE__ */ jsx("span", {
787
1218
  className: `reslide-mark reslide-mark-${type}`,
788
- style: styleFn(resolvedColor),
1219
+ style: styleFn(color, resolvedColor),
789
1220
  children
790
1221
  });
791
1222
  }
@@ -827,6 +1258,8 @@ SlotRight.__reslideSlot = "right";
827
1258
  /**
828
1259
  * Presenter view that syncs with the main presentation window.
829
1260
  * Shows: current slide, next slide preview, notes, and timer.
1261
+ * Supports bidirectional control — navigate from this window to
1262
+ * drive the main presentation.
830
1263
  */
831
1264
  function PresenterView({ children, notes }) {
832
1265
  const [currentSlide, setCurrentSlide] = useState(0);
@@ -834,17 +1267,28 @@ function PresenterView({ children, notes }) {
834
1267
  const [elapsed, setElapsed] = useState(0);
835
1268
  const slides = Children.toArray(children);
836
1269
  const totalSlides = slides.length;
1270
+ const { next, prev, goTo } = usePresenterChannel((msg) => {
1271
+ setCurrentSlide(msg.currentSlide);
1272
+ setClickStep(msg.clickStep);
1273
+ });
837
1274
  useEffect(() => {
838
- if (typeof BroadcastChannel === "undefined") return;
839
- const channel = new BroadcastChannel("reslide-presenter");
840
- channel.onmessage = (e) => {
841
- if (e.data.type === "sync") {
842
- setCurrentSlide(e.data.currentSlide);
843
- setClickStep(e.data.clickStep);
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;
844
1287
  }
845
- };
846
- return () => channel.close();
847
- }, []);
1288
+ }
1289
+ window.addEventListener("keydown", handleKeyDown);
1290
+ return () => window.removeEventListener("keydown", handleKeyDown);
1291
+ }, [next, prev]);
848
1292
  useEffect(() => {
849
1293
  const start = Date.now();
850
1294
  const interval = setInterval(() => {
@@ -857,21 +1301,31 @@ function PresenterView({ children, notes }) {
857
1301
  const s = seconds % 60;
858
1302
  return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
859
1303
  };
1304
+ const noopReg = useCallback((_i, _c) => {}, []);
860
1305
  const noop = useCallback(() => {}, []);
861
- const contextValue = {
1306
+ const contextValue = useMemo(() => ({
862
1307
  currentSlide,
863
1308
  totalSlides,
864
1309
  clickStep,
865
1310
  totalClickSteps: 0,
866
1311
  isOverview: false,
867
1312
  isFullscreen: false,
868
- next: noop,
869
- prev: noop,
870
- goTo: useCallback((_n) => {}, []),
1313
+ next,
1314
+ prev,
1315
+ goTo,
871
1316
  toggleOverview: noop,
872
1317
  toggleFullscreen: noop,
873
- registerClickSteps: useCallback((_i, _c) => {}, [])
874
- };
1318
+ registerClickSteps: noopReg
1319
+ }), [
1320
+ currentSlide,
1321
+ totalSlides,
1322
+ clickStep,
1323
+ next,
1324
+ prev,
1325
+ goTo,
1326
+ noop,
1327
+ noopReg
1328
+ ]);
875
1329
  return /* @__PURE__ */ jsx(DeckContext.Provider, {
876
1330
  value: contextValue,
877
1331
  children: /* @__PURE__ */ jsxs("div", {
@@ -965,37 +1419,88 @@ function PresenterView({ children, notes }) {
965
1419
  backgroundColor: "#0f172a",
966
1420
  borderRadius: "0.5rem"
967
1421
  },
968
- children: [/* @__PURE__ */ jsx("div", {
969
- style: {
970
- fontSize: "1.5rem",
971
- fontVariantNumeric: "tabular-nums",
972
- fontWeight: 700
973
- },
974
- children: formatTime(elapsed)
975
- }), /* @__PURE__ */ jsxs("div", {
976
- style: {
977
- fontSize: "1.125rem",
978
- fontVariantNumeric: "tabular-nums"
979
- },
980
- children: [
981
- currentSlide + 1,
982
- " / ",
983
- totalSlides,
984
- clickStep > 0 && /* @__PURE__ */ jsxs("span", {
985
- style: { color: "#94a3b8" },
986
- children: [
987
- " (click ",
988
- clickStep,
989
- ")"
990
- ]
991
- })
992
- ]
993
- })]
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
+ ]
994
1471
  })
995
1472
  ]
996
1473
  })
997
1474
  });
998
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
+ }
999
1504
  //#endregion
1000
1505
  //#region src/CodeEditor.tsx
1001
1506
  /**
@@ -1187,6 +1692,165 @@ function Draggable({ children, x = 0, y = 0, style }) {
1187
1692
  });
1188
1693
  }
1189
1694
  //#endregion
1695
+ //#region src/Toc.tsx
1696
+ /**
1697
+ * Extract heading text (h1/h2) from a React element tree.
1698
+ * Traverses children recursively to find the first h1 or h2.
1699
+ */
1700
+ function extractHeading(node) {
1701
+ if (!isValidElement(node)) return null;
1702
+ const el = node;
1703
+ const type = el.type;
1704
+ if (type === "h1" || type === "h2") return extractText(el.props.children);
1705
+ const children = el.props.children;
1706
+ if (children == null) return null;
1707
+ let result = null;
1708
+ Children.forEach(children, (child) => {
1709
+ if (result) return;
1710
+ const found = extractHeading(child);
1711
+ if (found) result = found;
1712
+ });
1713
+ return result;
1714
+ }
1715
+ /** Extract plain text from a React node tree */
1716
+ function extractText(node) {
1717
+ if (node == null) return "";
1718
+ if (typeof node === "string") return node;
1719
+ if (typeof node === "number") return String(node);
1720
+ if (Array.isArray(node)) return node.map(extractText).join("");
1721
+ if (isValidElement(node)) return extractText(node.props.children);
1722
+ return "";
1723
+ }
1724
+ /**
1725
+ * Table of Contents component that renders a clickable list of slides
1726
+ * with their heading text extracted from h1/h2 elements.
1727
+ *
1728
+ * Must be rendered inside a `<Deck>` component.
1729
+ *
1730
+ * ```tsx
1731
+ * <Toc>
1732
+ * <Slide><h1>Introduction</h1></Slide>
1733
+ * <Slide><h2>Agenda</h2></Slide>
1734
+ * </Toc>
1735
+ * ```
1736
+ */
1737
+ function Toc({ children, className, style }) {
1738
+ const { currentSlide, goTo } = useDeck();
1739
+ const items = Children.toArray(children).map((slide, index) => {
1740
+ return {
1741
+ index,
1742
+ title: extractHeading(slide) || `Slide ${index + 1}`
1743
+ };
1744
+ });
1745
+ return /* @__PURE__ */ jsx("nav", {
1746
+ className: `reslide-toc${className ? ` ${className}` : ""}`,
1747
+ style: {
1748
+ display: "flex",
1749
+ flexDirection: "column",
1750
+ gap: "0.25rem",
1751
+ padding: "0.5rem 0",
1752
+ ...style
1753
+ },
1754
+ children: items.map((item) => {
1755
+ const isActive = item.index === currentSlide;
1756
+ return /* @__PURE__ */ jsxs("button", {
1757
+ type: "button",
1758
+ onClick: () => goTo(item.index),
1759
+ style: {
1760
+ display: "flex",
1761
+ alignItems: "center",
1762
+ gap: "0.5rem",
1763
+ padding: "0.5rem 1rem",
1764
+ border: "none",
1765
+ borderRadius: "0.375rem",
1766
+ cursor: "pointer",
1767
+ textAlign: "left",
1768
+ fontSize: "0.875rem",
1769
+ lineHeight: 1.4,
1770
+ fontFamily: "inherit",
1771
+ color: isActive ? "var(--toc-active-text, var(--slide-accent, #3b82f6))" : "var(--toc-text, var(--slide-text, #1a1a1a))",
1772
+ backgroundColor: isActive ? "var(--toc-active-bg, rgba(59, 130, 246, 0.1))" : "transparent",
1773
+ fontWeight: isActive ? 600 : 400,
1774
+ transition: "background-color 0.15s, color 0.15s"
1775
+ },
1776
+ onMouseEnter: (e) => {
1777
+ if (!isActive) e.currentTarget.style.backgroundColor = "var(--toc-hover-bg, rgba(0, 0, 0, 0.05))";
1778
+ },
1779
+ onMouseLeave: (e) => {
1780
+ e.currentTarget.style.backgroundColor = isActive ? "var(--toc-active-bg, rgba(59, 130, 246, 0.1))" : "transparent";
1781
+ },
1782
+ children: [/* @__PURE__ */ jsx("span", {
1783
+ style: {
1784
+ minWidth: "1.5rem",
1785
+ textAlign: "right",
1786
+ opacity: .5,
1787
+ fontSize: "0.75rem",
1788
+ fontVariantNumeric: "tabular-nums"
1789
+ },
1790
+ children: item.index + 1
1791
+ }), /* @__PURE__ */ jsx("span", { children: item.title })]
1792
+ }, item.index);
1793
+ })
1794
+ });
1795
+ }
1796
+ //#endregion
1797
+ //#region src/Mermaid.tsx
1798
+ /**
1799
+ * Renders a Mermaid diagram.
1800
+ *
1801
+ * Usage in MDX:
1802
+ * ```mdx
1803
+ * <Mermaid>
1804
+ * graph TD
1805
+ * A --> B
1806
+ * </Mermaid>
1807
+ * ```
1808
+ *
1809
+ * The mermaid library is dynamically imported on the client side,
1810
+ * so it does not need to be installed as a project dependency.
1811
+ */
1812
+ function Mermaid({ children }) {
1813
+ const containerRef = useRef(null);
1814
+ const [svg, setSvg] = useState("");
1815
+ const [error, setError] = useState("");
1816
+ const id = useId().replace(/:/g, "_");
1817
+ useEffect(() => {
1818
+ let cancelled = false;
1819
+ async function render() {
1820
+ try {
1821
+ const mermaid = await import("mermaid");
1822
+ mermaid.default.initialize({
1823
+ startOnLoad: false,
1824
+ theme: "default",
1825
+ securityLevel: "loose"
1826
+ });
1827
+ const code = typeof children === "string" ? children.trim() : "";
1828
+ if (!code) return;
1829
+ const { svg: rendered } = await mermaid.default.render(`mermaid-${id}`, code);
1830
+ if (!cancelled) {
1831
+ setSvg(rendered);
1832
+ setError("");
1833
+ }
1834
+ } catch (err) {
1835
+ if (!cancelled) setError(err instanceof Error ? err.message : "Failed to render diagram");
1836
+ }
1837
+ }
1838
+ render();
1839
+ return () => {
1840
+ cancelled = true;
1841
+ };
1842
+ }, [children, id]);
1843
+ if (error) return /* @__PURE__ */ jsx("div", {
1844
+ className: "reslide-mermaid reslide-mermaid--error",
1845
+ children: /* @__PURE__ */ jsx("pre", { children: error })
1846
+ });
1847
+ return /* @__PURE__ */ jsx("div", {
1848
+ ref: containerRef,
1849
+ className: "reslide-mermaid",
1850
+ dangerouslySetInnerHTML: { __html: svg }
1851
+ });
1852
+ }
1853
+ //#endregion
1190
1854
  //#region src/ReslideEmbed.tsx
1191
1855
  /** Built-in reslide components available in MDX */
1192
1856
  const builtinComponents = {
@@ -1258,4 +1922,4 @@ function ReslideEmbed({ code, transition, components: userComponents, className,
1258
1922
  });
1259
1923
  }
1260
1924
  //#endregion
1261
- export { Click, ClickNavigation, ClickSteps, CodeEditor, Deck, DeckContext, Draggable, DrawingLayer, GlobalLayer, Mark, Notes, PresenterView, PrintView, ReslideEmbed, Slide, SlideIndexContext, SlotRight, 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.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-agnostic React presentation components",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -9,33 +9,38 @@
9
9
  ],
10
10
  "type": "module",
11
11
  "exports": {
12
- ".": "./dist/index.mjs",
12
+ ".": "./src/index.ts",
13
+ "./mdx-components": "./src/mdx-components.ts",
14
+ "./themes/default.css": "./src/themes/default.css",
15
+ "./themes/dark.css": "./src/themes/dark.css",
16
+ "./transitions.css": "./src/transitions.css",
17
+ "./print.css": "./src/themes/print.css",
13
18
  "./package.json": "./package.json"
14
19
  },
15
- "scripts": {
16
- "build": "vp pack",
17
- "dev": "vp pack --watch",
18
- "test": "vp test"
20
+ "publishConfig": {
21
+ "access": "public"
19
22
  },
20
23
  "dependencies": {
21
- "@mdx-js/mdx": "^3.1.1"
24
+ "@mdx-js/mdx": "3.1.1"
22
25
  },
23
26
  "devDependencies": {
24
- "@testing-library/dom": "catalog:",
25
- "@testing-library/react": "catalog:",
26
- "@types/react": "^19",
27
- "@types/react-dom": "^19",
28
- "jsdom": "catalog:",
29
- "react": "^19.1.0",
30
- "react-dom": "^19.1.0",
31
- "vite-plus": "catalog:",
32
- "vitest": "catalog:"
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
+ "vite-plus": "latest",
35
+ "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
33
36
  },
34
37
  "peerDependencies": {
35
- "react": "^19.0.0",
36
- "react-dom": "^19.0.0"
38
+ "react": ">=19.0.0",
39
+ "react-dom": ">=19.0.0"
37
40
  },
38
- "publishConfig": {
39
- "access": "public"
41
+ "scripts": {
42
+ "build": "vp pack",
43
+ "dev": "vp pack --watch",
44
+ "test": "vp test"
40
45
  }
41
- }
46
+ }
@@ -75,6 +75,29 @@
75
75
  padding: 0;
76
76
  }
77
77
 
78
+ .reslide-slide table {
79
+ width: 100%;
80
+ border-collapse: collapse;
81
+ margin-bottom: 1rem;
82
+ font-size: 0.9em;
83
+ }
84
+
85
+ .reslide-slide th,
86
+ .reslide-slide td {
87
+ padding: 0.5rem 0.75rem;
88
+ text-align: left;
89
+ border-bottom: 1px solid #334155;
90
+ }
91
+
92
+ .reslide-slide th {
93
+ font-weight: 600;
94
+ border-bottom: 2px solid #475569;
95
+ }
96
+
97
+ .reslide-slide tr:last-child td {
98
+ border-bottom: none;
99
+ }
100
+
78
101
  .reslide-slide a {
79
102
  color: var(--slide-accent);
80
103
  text-decoration: underline;
@@ -75,6 +75,29 @@
75
75
  padding: 0;
76
76
  }
77
77
 
78
+ .reslide-slide table {
79
+ width: 100%;
80
+ border-collapse: collapse;
81
+ margin-bottom: 1rem;
82
+ font-size: 0.9em;
83
+ }
84
+
85
+ .reslide-slide th,
86
+ .reslide-slide td {
87
+ padding: 0.5rem 0.75rem;
88
+ text-align: left;
89
+ border-bottom: 1px solid #e2e8f0;
90
+ }
91
+
92
+ .reslide-slide th {
93
+ font-weight: 600;
94
+ border-bottom: 2px solid #cbd5e1;
95
+ }
96
+
97
+ .reslide-slide tr:last-child td {
98
+ border-bottom: none;
99
+ }
100
+
78
101
  .reslide-slide a {
79
102
  color: var(--slide-accent);
80
103
  text-decoration: underline;