@reslide-dev/core 0.0.1 → 0.1.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
@@ -245,6 +245,57 @@ declare function Draggable({
245
245
  style
246
246
  }: DraggableProps): react_jsx_runtime0.JSX.Element;
247
247
  //#endregion
248
+ //#region src/Toc.d.ts
249
+ interface TocProps {
250
+ /** The slide elements to extract headings from */
251
+ children: ReactNode;
252
+ /** Additional CSS class name */
253
+ className?: string;
254
+ /** Additional inline styles */
255
+ style?: CSSProperties;
256
+ }
257
+ /**
258
+ * Table of Contents component that renders a clickable list of slides
259
+ * with their heading text extracted from h1/h2 elements.
260
+ *
261
+ * Must be rendered inside a `<Deck>` component.
262
+ *
263
+ * ```tsx
264
+ * <Toc>
265
+ * <Slide><h1>Introduction</h1></Slide>
266
+ * <Slide><h2>Agenda</h2></Slide>
267
+ * </Toc>
268
+ * ```
269
+ */
270
+ declare function Toc({
271
+ children,
272
+ className,
273
+ style
274
+ }: TocProps): react_jsx_runtime0.JSX.Element;
275
+ //#endregion
276
+ //#region src/Mermaid.d.ts
277
+ interface MermaidProps {
278
+ /** Mermaid diagram code */
279
+ children: string;
280
+ }
281
+ /**
282
+ * Renders a Mermaid diagram.
283
+ *
284
+ * Usage in MDX:
285
+ * ```mdx
286
+ * <Mermaid>
287
+ * graph TD
288
+ * A --> B
289
+ * </Mermaid>
290
+ * ```
291
+ *
292
+ * The mermaid library is dynamically imported on the client side,
293
+ * so it does not need to be installed as a project dependency.
294
+ */
295
+ declare function Mermaid({
296
+ children
297
+ }: MermaidProps): react_jsx_runtime0.JSX.Element;
298
+ //#endregion
248
299
  //#region src/ClickNavigation.d.ts
249
300
  interface ClickNavigationProps {
250
301
  onPrev: () => void;
@@ -263,6 +314,9 @@ declare function ClickNavigation({
263
314
  disabled
264
315
  }: ClickNavigationProps): react_jsx_runtime0.JSX.Element | null;
265
316
  //#endregion
317
+ //#region src/ProgressBar.d.ts
318
+ declare function ProgressBar(): react_jsx_runtime0.JSX.Element;
319
+ //#endregion
266
320
  //#region src/ReslideEmbed.d.ts
267
321
  interface ReslideEmbedProps {
268
322
  /** Compiled MDX code from compileMdxSlides() */
@@ -337,4 +391,4 @@ declare function useDeck(): DeckContextValue;
337
391
  declare const SlideIndexContext: react.Context<number | null>;
338
392
  declare function useSlideIndex(): number;
339
393
  //#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 };
394
+ export { Click, ClickNavigation, type ClickProps, ClickSteps, CodeEditor, type CodeEditorProps, Deck, type DeckActions, DeckContext, type DeckContextValue, type DeckProps, type DeckState, Draggable, type DraggableProps, DrawingLayer, type DrawingLayerProps, GlobalLayer, type GlobalLayerProps, Mark, type MarkProps, Mermaid, type MermaidProps, Notes, type NotesProps, PresenterView, type PresenterViewProps, PrintView, ProgressBar, ReslideEmbed, type ReslideEmbedProps, Slide, SlideIndexContext, type SlideProps, SlotRight, Toc, type TocProps, type TransitionType, isPresenterView, openPresenterWindow, useDeck, useSlideIndex };
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
@@ -188,6 +188,30 @@ function PrintView({ children }) {
188
188
  });
189
189
  }
190
190
  //#endregion
191
+ //#region src/ProgressBar.tsx
192
+ function ProgressBar() {
193
+ const { currentSlide, totalSlides, clickStep, totalClickSteps } = useDeck();
194
+ const slideProgress = totalSlides <= 1 ? 1 : currentSlide / (totalSlides - 1);
195
+ const clickFraction = totalSlides <= 1 ? 0 : totalClickSteps > 0 ? clickStep / totalClickSteps * (1 / (totalSlides - 1)) : 0;
196
+ return /* @__PURE__ */ jsx("div", {
197
+ className: "reslide-progress-bar",
198
+ style: {
199
+ position: "absolute",
200
+ top: 0,
201
+ left: 0,
202
+ width: "100%",
203
+ height: "3px",
204
+ zIndex: 100
205
+ },
206
+ children: /* @__PURE__ */ jsx("div", { style: {
207
+ height: "100%",
208
+ width: `${Math.min(slideProgress + clickFraction, 1) * 100}%`,
209
+ backgroundColor: "var(--slide-accent, #3b82f6)",
210
+ transition: "width 0.3s ease"
211
+ } })
212
+ });
213
+ }
214
+ //#endregion
191
215
  //#region src/SlideTransition.tsx
192
216
  const DURATION = 300;
193
217
  function SlideTransition({ children, currentSlide, transition }) {
@@ -195,14 +219,17 @@ function SlideTransition({ children, currentSlide, transition }) {
195
219
  const [displaySlide, setDisplaySlide] = useState(currentSlide);
196
220
  const [prevSlide, setPrevSlide] = useState(null);
197
221
  const [isAnimating, setIsAnimating] = useState(false);
222
+ const prevCurrentRef = useRef(currentSlide);
198
223
  const timerRef = useRef(null);
199
224
  useEffect(() => {
200
- if (currentSlide === displaySlide) return;
225
+ if (currentSlide === prevCurrentRef.current) return;
226
+ const from = prevCurrentRef.current;
227
+ prevCurrentRef.current = currentSlide;
201
228
  if (transition === "none") {
202
229
  setDisplaySlide(currentSlide);
203
230
  return;
204
231
  }
205
- setPrevSlide(displaySlide);
232
+ setPrevSlide(from);
206
233
  setDisplaySlide(currentSlide);
207
234
  setIsAnimating(true);
208
235
  if (timerRef.current) clearTimeout(timerRef.current);
@@ -213,11 +240,7 @@ function SlideTransition({ children, currentSlide, transition }) {
213
240
  return () => {
214
241
  if (timerRef.current) clearTimeout(timerRef.current);
215
242
  };
216
- }, [
217
- currentSlide,
218
- displaySlide,
219
- transition
220
- ]);
243
+ }, [currentSlide, transition]);
221
244
  const resolvedTransition = resolveTransition(transition, prevSlide, displaySlide);
222
245
  if (transition === "none" || !isAnimating) return /* @__PURE__ */ jsx("div", {
223
246
  className: "reslide-transition-container",
@@ -493,6 +516,7 @@ function Deck({ children, transition = "none" }) {
493
516
  current: currentSlide + 1,
494
517
  total: totalSlides
495
518
  }),
519
+ !isOverview && !isPrinting && /* @__PURE__ */ jsx(ProgressBar, {}),
496
520
  !isOverview && !isPrinting && /* @__PURE__ */ jsx(DrawingLayer, { active: isDrawing })
497
521
  ]
498
522
  })
@@ -755,19 +779,19 @@ function ClickSteps({ count, slideIndex }) {
755
779
  //#endregion
756
780
  //#region src/Mark.tsx
757
781
  const markStyles = {
758
- highlight: (color) => ({
759
- backgroundColor: `var(--mark-${color}, ${color})`,
782
+ highlight: (colorName, resolvedColor) => ({
783
+ backgroundColor: `var(--mark-${colorName}, ${resolvedColor})`,
760
784
  padding: "0.1em 0.2em",
761
785
  borderRadius: "0.2em"
762
786
  }),
763
- underline: (color) => ({
787
+ underline: (colorName, resolvedColor) => ({
764
788
  textDecoration: "underline",
765
- textDecorationColor: `var(--mark-${color}, ${color})`,
789
+ textDecorationColor: `var(--mark-${colorName}, ${resolvedColor})`,
766
790
  textDecorationThickness: "0.15em",
767
791
  textUnderlineOffset: "0.15em"
768
792
  }),
769
- circle: (color) => ({
770
- border: `0.15em solid var(--mark-${color}, ${color})`,
793
+ circle: (colorName, resolvedColor) => ({
794
+ border: `0.15em solid var(--mark-${colorName}, ${resolvedColor})`,
771
795
  borderRadius: "50%",
772
796
  padding: "0.1em 0.3em"
773
797
  })
@@ -785,7 +809,7 @@ function Mark({ children, type = "highlight", color = "yellow" }) {
785
809
  const styleFn = markStyles[type] ?? markStyles.highlight;
786
810
  return /* @__PURE__ */ jsx("span", {
787
811
  className: `reslide-mark reslide-mark-${type}`,
788
- style: styleFn(resolvedColor),
812
+ style: styleFn(color, resolvedColor),
789
813
  children
790
814
  });
791
815
  }
@@ -1187,6 +1211,165 @@ function Draggable({ children, x = 0, y = 0, style }) {
1187
1211
  });
1188
1212
  }
1189
1213
  //#endregion
1214
+ //#region src/Toc.tsx
1215
+ /**
1216
+ * Extract heading text (h1/h2) from a React element tree.
1217
+ * Traverses children recursively to find the first h1 or h2.
1218
+ */
1219
+ function extractHeading(node) {
1220
+ if (!isValidElement(node)) return null;
1221
+ const el = node;
1222
+ const type = el.type;
1223
+ if (type === "h1" || type === "h2") return extractText(el.props.children);
1224
+ const children = el.props.children;
1225
+ if (children == null) return null;
1226
+ let result = null;
1227
+ Children.forEach(children, (child) => {
1228
+ if (result) return;
1229
+ const found = extractHeading(child);
1230
+ if (found) result = found;
1231
+ });
1232
+ return result;
1233
+ }
1234
+ /** Extract plain text from a React node tree */
1235
+ function extractText(node) {
1236
+ if (node == null) return "";
1237
+ if (typeof node === "string") return node;
1238
+ if (typeof node === "number") return String(node);
1239
+ if (Array.isArray(node)) return node.map(extractText).join("");
1240
+ if (isValidElement(node)) return extractText(node.props.children);
1241
+ return "";
1242
+ }
1243
+ /**
1244
+ * Table of Contents component that renders a clickable list of slides
1245
+ * with their heading text extracted from h1/h2 elements.
1246
+ *
1247
+ * Must be rendered inside a `<Deck>` component.
1248
+ *
1249
+ * ```tsx
1250
+ * <Toc>
1251
+ * <Slide><h1>Introduction</h1></Slide>
1252
+ * <Slide><h2>Agenda</h2></Slide>
1253
+ * </Toc>
1254
+ * ```
1255
+ */
1256
+ function Toc({ children, className, style }) {
1257
+ const { currentSlide, goTo } = useDeck();
1258
+ const items = Children.toArray(children).map((slide, index) => {
1259
+ return {
1260
+ index,
1261
+ title: extractHeading(slide) || `Slide ${index + 1}`
1262
+ };
1263
+ });
1264
+ return /* @__PURE__ */ jsx("nav", {
1265
+ className: `reslide-toc${className ? ` ${className}` : ""}`,
1266
+ style: {
1267
+ display: "flex",
1268
+ flexDirection: "column",
1269
+ gap: "0.25rem",
1270
+ padding: "0.5rem 0",
1271
+ ...style
1272
+ },
1273
+ children: items.map((item) => {
1274
+ const isActive = item.index === currentSlide;
1275
+ return /* @__PURE__ */ jsxs("button", {
1276
+ type: "button",
1277
+ onClick: () => goTo(item.index),
1278
+ style: {
1279
+ display: "flex",
1280
+ alignItems: "center",
1281
+ gap: "0.5rem",
1282
+ padding: "0.5rem 1rem",
1283
+ border: "none",
1284
+ borderRadius: "0.375rem",
1285
+ cursor: "pointer",
1286
+ textAlign: "left",
1287
+ fontSize: "0.875rem",
1288
+ lineHeight: 1.4,
1289
+ fontFamily: "inherit",
1290
+ color: isActive ? "var(--toc-active-text, var(--slide-accent, #3b82f6))" : "var(--toc-text, var(--slide-text, #1a1a1a))",
1291
+ backgroundColor: isActive ? "var(--toc-active-bg, rgba(59, 130, 246, 0.1))" : "transparent",
1292
+ fontWeight: isActive ? 600 : 400,
1293
+ transition: "background-color 0.15s, color 0.15s"
1294
+ },
1295
+ onMouseEnter: (e) => {
1296
+ if (!isActive) e.currentTarget.style.backgroundColor = "var(--toc-hover-bg, rgba(0, 0, 0, 0.05))";
1297
+ },
1298
+ onMouseLeave: (e) => {
1299
+ e.currentTarget.style.backgroundColor = isActive ? "var(--toc-active-bg, rgba(59, 130, 246, 0.1))" : "transparent";
1300
+ },
1301
+ children: [/* @__PURE__ */ jsx("span", {
1302
+ style: {
1303
+ minWidth: "1.5rem",
1304
+ textAlign: "right",
1305
+ opacity: .5,
1306
+ fontSize: "0.75rem",
1307
+ fontVariantNumeric: "tabular-nums"
1308
+ },
1309
+ children: item.index + 1
1310
+ }), /* @__PURE__ */ jsx("span", { children: item.title })]
1311
+ }, item.index);
1312
+ })
1313
+ });
1314
+ }
1315
+ //#endregion
1316
+ //#region src/Mermaid.tsx
1317
+ /**
1318
+ * Renders a Mermaid diagram.
1319
+ *
1320
+ * Usage in MDX:
1321
+ * ```mdx
1322
+ * <Mermaid>
1323
+ * graph TD
1324
+ * A --> B
1325
+ * </Mermaid>
1326
+ * ```
1327
+ *
1328
+ * The mermaid library is dynamically imported on the client side,
1329
+ * so it does not need to be installed as a project dependency.
1330
+ */
1331
+ function Mermaid({ children }) {
1332
+ const containerRef = useRef(null);
1333
+ const [svg, setSvg] = useState("");
1334
+ const [error, setError] = useState("");
1335
+ const id = useId().replace(/:/g, "_");
1336
+ useEffect(() => {
1337
+ let cancelled = false;
1338
+ async function render() {
1339
+ try {
1340
+ const mermaid = await import("mermaid");
1341
+ mermaid.default.initialize({
1342
+ startOnLoad: false,
1343
+ theme: "default",
1344
+ securityLevel: "loose"
1345
+ });
1346
+ const code = typeof children === "string" ? children.trim() : "";
1347
+ if (!code) return;
1348
+ const { svg: rendered } = await mermaid.default.render(`mermaid-${id}`, code);
1349
+ if (!cancelled) {
1350
+ setSvg(rendered);
1351
+ setError("");
1352
+ }
1353
+ } catch (err) {
1354
+ if (!cancelled) setError(err instanceof Error ? err.message : "Failed to render diagram");
1355
+ }
1356
+ }
1357
+ render();
1358
+ return () => {
1359
+ cancelled = true;
1360
+ };
1361
+ }, [children, id]);
1362
+ if (error) return /* @__PURE__ */ jsx("div", {
1363
+ className: "reslide-mermaid reslide-mermaid--error",
1364
+ children: /* @__PURE__ */ jsx("pre", { children: error })
1365
+ });
1366
+ return /* @__PURE__ */ jsx("div", {
1367
+ ref: containerRef,
1368
+ className: "reslide-mermaid",
1369
+ dangerouslySetInnerHTML: { __html: svg }
1370
+ });
1371
+ }
1372
+ //#endregion
1190
1373
  //#region src/ReslideEmbed.tsx
1191
1374
  /** Built-in reslide components available in MDX */
1192
1375
  const builtinComponents = {
@@ -1258,4 +1441,4 @@ function ReslideEmbed({ code, transition, components: userComponents, className,
1258
1441
  });
1259
1442
  }
1260
1443
  //#endregion
1261
- export { Click, ClickNavigation, ClickSteps, CodeEditor, Deck, DeckContext, Draggable, DrawingLayer, GlobalLayer, Mark, Notes, PresenterView, PrintView, ReslideEmbed, Slide, SlideIndexContext, SlotRight, isPresenterView, openPresenterWindow, useDeck, useSlideIndex };
1444
+ export { Click, ClickNavigation, ClickSteps, CodeEditor, Deck, DeckContext, Draggable, DrawingLayer, GlobalLayer, Mark, Mermaid, Notes, PresenterView, PrintView, ProgressBar, ReslideEmbed, Slide, SlideIndexContext, SlotRight, Toc, isPresenterView, openPresenterWindow, useDeck, useSlideIndex };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reslide-dev/core",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Framework-agnostic React presentation components",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -10,32 +10,37 @@
10
10
  "type": "module",
11
11
  "exports": {
12
12
  ".": "./dist/index.mjs",
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
24
  "@mdx-js/mdx": "^3.1.1"
22
25
  },
23
26
  "devDependencies": {
24
- "@testing-library/dom": "catalog:",
25
- "@testing-library/react": "catalog:",
27
+ "@testing-library/dom": "^10.4.1",
28
+ "@testing-library/react": "^16.3.2",
26
29
  "@types/react": "^19",
27
30
  "@types/react-dom": "^19",
28
- "jsdom": "catalog:",
31
+ "jsdom": "^29.0.0",
29
32
  "react": "^19.1.0",
30
33
  "react-dom": "^19.1.0",
31
- "vite-plus": "catalog:",
32
- "vitest": "catalog:"
34
+ "vite-plus": "latest",
35
+ "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
33
36
  },
34
37
  "peerDependencies": {
35
38
  "react": "^19.0.0",
36
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;