@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 +75 -2
- package/dist/index.mjs +798 -134
- package/package.json +26 -21
- package/src/themes/dark.css +23 -0
- package/src/themes/default.css +23 -0
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
132
|
-
|
|
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
|
|
144
|
-
|
|
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 ===
|
|
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(
|
|
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(
|
|
493
|
-
|
|
494
|
-
|
|
937
|
+
!isOverview && !isPrinting && /* @__PURE__ */ jsx(ProgressBar, {}),
|
|
938
|
+
!isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, {
|
|
939
|
+
active: isDrawing,
|
|
940
|
+
currentSlide
|
|
495
941
|
}),
|
|
496
|
-
!
|
|
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: (
|
|
759
|
-
backgroundColor: `var(--mark-${
|
|
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: (
|
|
1194
|
+
underline: (colorName, resolvedColor) => ({
|
|
764
1195
|
textDecoration: "underline",
|
|
765
|
-
textDecorationColor: `var(--mark-${
|
|
1196
|
+
textDecorationColor: `var(--mark-${colorName}, ${resolvedColor})`,
|
|
766
1197
|
textDecorationThickness: "0.15em",
|
|
767
1198
|
textUnderlineOffset: "0.15em"
|
|
768
1199
|
}),
|
|
769
|
-
circle: (
|
|
770
|
-
border: `0.15em solid var(--mark-${
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
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
|
|
869
|
-
prev
|
|
870
|
-
goTo
|
|
1313
|
+
next,
|
|
1314
|
+
prev,
|
|
1315
|
+
goTo,
|
|
871
1316
|
toggleOverview: noop,
|
|
872
1317
|
toggleFullscreen: noop,
|
|
873
|
-
registerClickSteps:
|
|
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: [
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
|
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
|
-
".": "./
|
|
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
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"dev": "vp pack --watch",
|
|
18
|
-
"test": "vp test"
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
19
22
|
},
|
|
20
23
|
"dependencies": {
|
|
21
|
-
"@mdx-js/mdx": "
|
|
24
|
+
"@mdx-js/mdx": "3.1.1"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
|
-
"@testing-library/dom": "
|
|
25
|
-
"@testing-library/react": "
|
|
26
|
-
"@types/react": "
|
|
27
|
-
"@types/react-dom": "
|
|
28
|
-
"jsdom": "
|
|
29
|
-
"react": "
|
|
30
|
-
"react-dom": "
|
|
31
|
-
"vite-plus": "
|
|
32
|
-
"vitest": "
|
|
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": "
|
|
36
|
-
"react-dom": "
|
|
38
|
+
"react": ">=19.0.0",
|
|
39
|
+
"react-dom": ">=19.0.0"
|
|
37
40
|
},
|
|
38
|
-
"
|
|
39
|
-
"
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "vp pack",
|
|
43
|
+
"dev": "vp pack --watch",
|
|
44
|
+
"test": "vp test"
|
|
40
45
|
}
|
|
41
|
-
}
|
|
46
|
+
}
|
package/src/themes/dark.css
CHANGED
|
@@ -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;
|
package/src/themes/default.css
CHANGED
|
@@ -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;
|