@slidev-react/core 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 slidev-react contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # `@slidev-react/core`
2
+
3
+ `@slidev-react/core` 承接尽量纯的领域能力:
4
+
5
+ - presentation flow
6
+ - export file naming
7
+ - session protocol / shared contracts
8
+
9
+ 这里尽量不放 React 组件、Provider、浏览器视图装配,也不依赖 Vite 编译入口。
@@ -0,0 +1,14 @@
1
+ import { createSlideImageFileName, createSlideSnapshotFileName, resolveExportSlidesBaseName, trimPdfExtension } from "./presentation/export/fileNames.js";
2
+ import { SlideRange, clampSlideSelection, createRangesFromSlides, createSlideSelectionLabel, expandSlideSelection, parseSlideSelection, toPdfPageRanges } from "./presentation/export/selection.js";
3
+ import { PresentationExportMode, buildPrintExportUrl, buildSlidesUrl, resolvePresentationExportMode, resolvePrintExportWithClicks } from "./presentation/export/urls.js";
4
+ import { normalizeConfiguredCueCount, resolveCueTotal } from "./presentation/flow/cue.js";
5
+ import { AdvanceFlowInput, FlowNavigationResult, RetreatFlowInput, canAdvanceFlow, canRetreatFlow, clampCueIndex, resolveAdvanceFlow, resolveRetreatFlow } from "./presentation/flow/navigation.js";
6
+ import { normalizeCueStep } from "./presentation/flow/step.js";
7
+ import { PRESENTATION_PROTOCOL_VERSION, PresentationCursorState, PresentationDrawPoint, PresentationDrawStroke, PresentationDrawingsState, PresentationEnvelope, PresentationHeartbeatEnvelope, PresentationJoinEnvelope, PresentationLeaveEnvelope, PresentationPatchEnvelope, PresentationRole, PresentationSharedState, PresentationSnapshotEnvelope, PresentationSyncMode, SyncedPresentationRole, parsePresentationDrawingsState, parsePresentationEnvelope } from "./presentation/session/protocol.js";
8
+ import { BuiltinLayoutName, LayoutName, layoutNames } from "./slides/layout.js";
9
+ import { TransitionName, transitionNames } from "./slides/transition.js";
10
+ import { SlideComponent, SlideMeta, SlideUnit } from "./slides/slide.js";
11
+ import { DEFAULT_SLIDES_AR, DEFAULT_SLIDES_VIEWPORT, SlidesViewport, formatViewportAspectRatio, isPortraitViewport, resolvePrintPageSize, resolveSlidesViewportMeta } from "./slides/viewport.js";
12
+ import { SlidesDocument, SlidesMeta } from "./slides/slides.js";
13
+ import { CompiledSlide, CompiledSlidesManifest } from "./slides/compiled-slides.js";
14
+ export { AdvanceFlowInput, BuiltinLayoutName, CompiledSlide, CompiledSlidesManifest, DEFAULT_SLIDES_AR, DEFAULT_SLIDES_VIEWPORT, FlowNavigationResult, LayoutName, PRESENTATION_PROTOCOL_VERSION, PresentationCursorState, PresentationDrawPoint, PresentationDrawStroke, PresentationDrawingsState, PresentationEnvelope, PresentationExportMode, PresentationHeartbeatEnvelope, PresentationJoinEnvelope, PresentationLeaveEnvelope, PresentationPatchEnvelope, PresentationRole, PresentationSharedState, PresentationSnapshotEnvelope, PresentationSyncMode, RetreatFlowInput, SlideComponent, SlideMeta, SlideRange, SlideUnit, SlidesDocument, SlidesMeta, SlidesViewport, SyncedPresentationRole, TransitionName, buildPrintExportUrl, buildSlidesUrl, canAdvanceFlow, canRetreatFlow, clampCueIndex, clampSlideSelection, createRangesFromSlides, createSlideImageFileName, createSlideSelectionLabel, createSlideSnapshotFileName, expandSlideSelection, formatViewportAspectRatio, isPortraitViewport, layoutNames, normalizeConfiguredCueCount, normalizeCueStep, parsePresentationDrawingsState, parsePresentationEnvelope, parseSlideSelection, resolveAdvanceFlow, resolveCueTotal, resolveExportSlidesBaseName, resolvePresentationExportMode, resolvePrintExportWithClicks, resolvePrintPageSize, resolveRetreatFlow, resolveSlidesViewportMeta, toPdfPageRanges, transitionNames, trimPdfExtension };
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createSlideImageFileName, createSlideSnapshotFileName, resolveExportSlidesBaseName, trimPdfExtension } from "./presentation/export/fileNames.js";
2
+ import { clampSlideSelection, createRangesFromSlides, createSlideSelectionLabel, expandSlideSelection, parseSlideSelection, toPdfPageRanges } from "./presentation/export/selection.js";
3
+ import { buildPrintExportUrl, buildSlidesUrl, resolvePresentationExportMode, resolvePrintExportWithClicks } from "./presentation/export/urls.js";
4
+ import { normalizeConfiguredCueCount, resolveCueTotal } from "./presentation/flow/cue.js";
5
+ import { canAdvanceFlow, canRetreatFlow, clampCueIndex, resolveAdvanceFlow, resolveRetreatFlow } from "./presentation/flow/navigation.js";
6
+ import { normalizeCueStep } from "./presentation/flow/step.js";
7
+ import { PRESENTATION_PROTOCOL_VERSION, parsePresentationDrawingsState, parsePresentationEnvelope } from "./presentation/session/protocol.js";
8
+ import { layoutNames } from "./slides/layout.js";
9
+ import { transitionNames } from "./slides/transition.js";
10
+ import { DEFAULT_SLIDES_AR, DEFAULT_SLIDES_VIEWPORT, formatViewportAspectRatio, isPortraitViewport, resolvePrintPageSize, resolveSlidesViewportMeta } from "./slides/viewport.js";
11
+ export { DEFAULT_SLIDES_AR, DEFAULT_SLIDES_VIEWPORT, PRESENTATION_PROTOCOL_VERSION, buildPrintExportUrl, buildSlidesUrl, canAdvanceFlow, canRetreatFlow, clampCueIndex, clampSlideSelection, createRangesFromSlides, createSlideImageFileName, createSlideSelectionLabel, createSlideSnapshotFileName, expandSlideSelection, formatViewportAspectRatio, isPortraitViewport, layoutNames, normalizeConfiguredCueCount, normalizeCueStep, parsePresentationDrawingsState, parsePresentationEnvelope, parseSlideSelection, resolveAdvanceFlow, resolveCueTotal, resolveExportSlidesBaseName, resolvePresentationExportMode, resolvePrintExportWithClicks, resolvePrintPageSize, resolveRetreatFlow, resolveSlidesViewportMeta, toPdfPageRanges, transitionNames, trimPdfExtension };
@@ -0,0 +1,21 @@
1
+ //#region src/presentation/export/fileNames.d.ts
2
+ declare function trimPdfExtension(value: string): string;
3
+ declare function resolveExportSlidesBaseName(documentTitle: string): string;
4
+ declare function createSlideImageFileName({
5
+ index,
6
+ title
7
+ }: {
8
+ index: number;
9
+ title?: string;
10
+ }): string;
11
+ declare function createSlideSnapshotFileName({
12
+ index,
13
+ title,
14
+ clickStep
15
+ }: {
16
+ index: number;
17
+ title?: string;
18
+ clickStep?: number | null;
19
+ }): string;
20
+ //#endregion
21
+ export { createSlideImageFileName, createSlideSnapshotFileName, resolveExportSlidesBaseName, trimPdfExtension };
@@ -0,0 +1,25 @@
1
+ //#region src/presentation/export/fileNames.ts
2
+ function slugifySegment(value) {
3
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4
+ }
5
+ function trimPdfExtension(value) {
6
+ return value.replace(/\.pdf$/i, "");
7
+ }
8
+ function resolveExportSlidesBaseName(documentTitle) {
9
+ const slug = slugifySegment(trimPdfExtension(documentTitle.trim()));
10
+ if (slug) return slug;
11
+ return "slide-react-slides";
12
+ }
13
+ function createSlideImageFileName({ index, title }) {
14
+ const safeIndex = Number.isFinite(index) && index > 0 ? Math.floor(index) : 1;
15
+ const suffix = slugifySegment(title ?? "") || `slide-${safeIndex}`;
16
+ return `${String(safeIndex).padStart(3, "0")}-${suffix}.png`;
17
+ }
18
+ function createSlideSnapshotFileName({ index, title, clickStep }) {
19
+ const safeIndex = Number.isFinite(index) && index > 0 ? Math.floor(index) : 1;
20
+ const suffix = slugifySegment(title ?? "") || `slide-${safeIndex}`;
21
+ const clickSuffix = typeof clickStep === "number" && Number.isFinite(clickStep) ? `-click-${clickStep}` : "";
22
+ return `${String(safeIndex).padStart(3, "0")}-${suffix}${clickSuffix}.png`;
23
+ }
24
+ //#endregion
25
+ export { createSlideImageFileName, createSlideSnapshotFileName, resolveExportSlidesBaseName, trimPdfExtension };
@@ -0,0 +1,16 @@
1
+ //#region src/presentation/export/selection.d.ts
2
+ interface SlideRange {
3
+ start: number;
4
+ end: number;
5
+ }
6
+ declare function parseSlideSelection(selection: string | null | undefined): SlideRange[] | null;
7
+ declare function clampSlideSelection(ranges: SlideRange[] | null, totalSlides: number): {
8
+ start: number;
9
+ end: number;
10
+ }[];
11
+ declare function expandSlideSelection(ranges: SlideRange[]): number[];
12
+ declare function createRangesFromSlides(slides: number[]): SlideRange[];
13
+ declare function toPdfPageRanges(ranges: SlideRange[]): string;
14
+ declare function createSlideSelectionLabel(ranges: SlideRange[]): string;
15
+ //#endregion
16
+ export { SlideRange, clampSlideSelection, createRangesFromSlides, createSlideSelectionLabel, expandSlideSelection, parseSlideSelection, toPdfPageRanges };
@@ -0,0 +1,99 @@
1
+ //#region src/presentation/export/selection.ts
2
+ function parsePositiveInt(value) {
3
+ if (!/^\d+$/.test(value)) return null;
4
+ const parsed = Number.parseInt(value, 10);
5
+ if (!Number.isFinite(parsed) || parsed < 1) return null;
6
+ return parsed;
7
+ }
8
+ function normalizeRanges(ranges) {
9
+ const sorted = [...ranges].sort((left, right) => left.start - right.start);
10
+ const merged = [];
11
+ for (const range of sorted) {
12
+ const previous = merged[merged.length - 1];
13
+ if (!previous || range.start > previous.end + 1) {
14
+ merged.push({ ...range });
15
+ continue;
16
+ }
17
+ previous.end = Math.max(previous.end, range.end);
18
+ }
19
+ return merged;
20
+ }
21
+ function parseSlideSelection(selection) {
22
+ const trimmed = selection?.trim() ?? "";
23
+ if (!trimmed) return null;
24
+ const ranges = [];
25
+ const parts = trimmed.split(",");
26
+ for (const part of parts) {
27
+ const token = part.trim();
28
+ if (!token) throw new Error("Slide selection contains an empty segment.");
29
+ const [rawStart, rawEnd] = token.split("-", 2);
30
+ const start = parsePositiveInt(rawStart);
31
+ if (!start) throw new Error(`Invalid slide selection segment: "${token}"`);
32
+ if (rawEnd === void 0) {
33
+ ranges.push({
34
+ start,
35
+ end: start
36
+ });
37
+ continue;
38
+ }
39
+ const end = parsePositiveInt(rawEnd);
40
+ if (!end || end < start) throw new Error(`Invalid slide range: "${token}"`);
41
+ ranges.push({
42
+ start,
43
+ end
44
+ });
45
+ }
46
+ return normalizeRanges(ranges);
47
+ }
48
+ function clampSlideSelection(ranges, totalSlides) {
49
+ if (!ranges) {
50
+ if (totalSlides < 1) return [];
51
+ return [{
52
+ start: 1,
53
+ end: totalSlides
54
+ }];
55
+ }
56
+ return normalizeRanges(ranges.map((range) => ({
57
+ start: Math.max(range.start, 1),
58
+ end: Math.min(range.end, totalSlides)
59
+ })).filter((range) => range.start <= range.end));
60
+ }
61
+ function expandSlideSelection(ranges) {
62
+ const slides = [];
63
+ for (const range of ranges) for (let slide = range.start; slide <= range.end; slide += 1) slides.push(slide);
64
+ return slides;
65
+ }
66
+ function createRangesFromSlides(slides) {
67
+ const uniqueSlides = [...new Set(slides)].sort((left, right) => left - right);
68
+ if (uniqueSlides.length === 0) return [];
69
+ const ranges = [];
70
+ let start = uniqueSlides[0];
71
+ let end = uniqueSlides[0];
72
+ for (let index = 1; index < uniqueSlides.length; index += 1) {
73
+ const slide = uniqueSlides[index];
74
+ if (slide === end + 1) {
75
+ end = slide;
76
+ continue;
77
+ }
78
+ ranges.push({
79
+ start,
80
+ end
81
+ });
82
+ start = slide;
83
+ end = slide;
84
+ }
85
+ ranges.push({
86
+ start,
87
+ end
88
+ });
89
+ return ranges;
90
+ }
91
+ function toPdfPageRanges(ranges) {
92
+ return ranges.map((range) => range.start === range.end ? `${range.start}` : `${range.start}-${range.end}`).join(",");
93
+ }
94
+ function createSlideSelectionLabel(ranges) {
95
+ if (ranges.length === 0) return "slides";
96
+ return `slides-${toPdfPageRanges(ranges).replaceAll(",", "_")}`;
97
+ }
98
+ //#endregion
99
+ export { clampSlideSelection, createRangesFromSlides, createSlideSelectionLabel, expandSlideSelection, parseSlideSelection, toPdfPageRanges };
@@ -0,0 +1,10 @@
1
+ //#region src/presentation/export/urls.d.ts
2
+ type PresentationExportMode = 'print';
3
+ declare function resolvePresentationExportMode(search: string): PresentationExportMode | null;
4
+ declare function resolvePrintExportWithClicks(search: string): boolean;
5
+ declare function buildPrintExportUrl(url: string, options?: {
6
+ withClicks?: boolean;
7
+ }): string;
8
+ declare function buildSlidesUrl(url: string): string;
9
+ //#endregion
10
+ export { PresentationExportMode, buildPrintExportUrl, buildSlidesUrl, resolvePresentationExportMode, resolvePrintExportWithClicks };
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ //#region src/presentation/export/urls.ts
3
+ const presentationExportModeSchema = z.enum(["print"]);
4
+ const printExportWithClicksSchema = z.union([z.literal("1"), z.literal("true")]);
5
+ function resolvePresentationExportMode(search) {
6
+ const result = presentationExportModeSchema.safeParse(new URLSearchParams(search).get("export"));
7
+ return result.success ? result.data : null;
8
+ }
9
+ function resolvePrintExportWithClicks(search) {
10
+ return printExportWithClicksSchema.safeParse(new URLSearchParams(search).get("with-clicks")).success;
11
+ }
12
+ function buildPrintExportUrl(url, options) {
13
+ const next = new URL(url);
14
+ next.searchParams.set("export", "print");
15
+ if (options?.withClicks) next.searchParams.set("with-clicks", "1");
16
+ else next.searchParams.delete("with-clicks");
17
+ next.hash = "";
18
+ return next.toString();
19
+ }
20
+ function buildSlidesUrl(url) {
21
+ const next = new URL(url);
22
+ next.searchParams.delete("export");
23
+ next.hash = "";
24
+ return next.toString();
25
+ }
26
+ //#endregion
27
+ export { buildPrintExportUrl, buildSlidesUrl, resolvePresentationExportMode, resolvePrintExportWithClicks };
@@ -0,0 +1,11 @@
1
+ //#region src/presentation/flow/cue.d.ts
2
+ declare function normalizeConfiguredCueCount(cues: number | undefined): number;
3
+ declare function resolveCueTotal({
4
+ configuredCues,
5
+ detectedCues
6
+ }: {
7
+ configuredCues?: number;
8
+ detectedCues?: number;
9
+ }): number;
10
+ //#endregion
11
+ export { normalizeConfiguredCueCount, resolveCueTotal };
@@ -0,0 +1,10 @@
1
+ //#region src/presentation/flow/cue.ts
2
+ function normalizeConfiguredCueCount(cues) {
3
+ if (typeof cues !== "number" || !Number.isFinite(cues)) return 0;
4
+ return Math.max(Math.floor(cues), 0);
5
+ }
6
+ function resolveCueTotal({ configuredCues, detectedCues }) {
7
+ return Math.max(normalizeConfiguredCueCount(configuredCues), normalizeConfiguredCueCount(detectedCues));
8
+ }
9
+ //#endregion
10
+ export { normalizeConfiguredCueCount, resolveCueTotal };
@@ -0,0 +1,42 @@
1
+ //#region src/presentation/flow/navigation.d.ts
2
+ interface AdvanceFlowInput {
3
+ currentCueIndex: number;
4
+ currentCueTotal: number;
5
+ currentPageIndex: number;
6
+ totalPages: number;
7
+ }
8
+ interface RetreatFlowInput {
9
+ currentCueIndex: number;
10
+ currentPageIndex: number;
11
+ previousCueIndex?: number;
12
+ previousCueTotal?: number;
13
+ }
14
+ interface FlowNavigationResult {
15
+ pageIndex: number;
16
+ cueIndex: number;
17
+ }
18
+ declare function clampCueIndex(next: number, total?: number): number;
19
+ declare function canAdvanceFlow({
20
+ currentCueIndex,
21
+ currentCueTotal,
22
+ currentPageIndex,
23
+ totalPages
24
+ }: AdvanceFlowInput): boolean;
25
+ declare function canRetreatFlow({
26
+ currentCueIndex,
27
+ currentPageIndex
28
+ }: Pick<RetreatFlowInput, "currentCueIndex" | "currentPageIndex">): boolean;
29
+ declare function resolveAdvanceFlow({
30
+ currentCueIndex,
31
+ currentCueTotal,
32
+ currentPageIndex,
33
+ totalPages
34
+ }: AdvanceFlowInput): FlowNavigationResult | null;
35
+ declare function resolveRetreatFlow({
36
+ currentCueIndex,
37
+ currentPageIndex,
38
+ previousCueIndex,
39
+ previousCueTotal
40
+ }: RetreatFlowInput): FlowNavigationResult | null;
41
+ //#endregion
42
+ export { AdvanceFlowInput, FlowNavigationResult, RetreatFlowInput, canAdvanceFlow, canRetreatFlow, clampCueIndex, resolveAdvanceFlow, resolveRetreatFlow };
@@ -0,0 +1,35 @@
1
+ //#region src/presentation/flow/navigation.ts
2
+ function clampCueIndex(next, total) {
3
+ if (total === void 0) return Math.max(next, 0);
4
+ return Math.min(Math.max(next, 0), Math.max(total, 0));
5
+ }
6
+ function canAdvanceFlow({ currentCueIndex, currentCueTotal, currentPageIndex, totalPages }) {
7
+ return currentCueIndex < currentCueTotal || currentPageIndex < totalPages - 1;
8
+ }
9
+ function canRetreatFlow({ currentCueIndex, currentPageIndex }) {
10
+ return currentCueIndex > 0 || currentPageIndex > 0;
11
+ }
12
+ function resolveAdvanceFlow({ currentCueIndex, currentCueTotal, currentPageIndex, totalPages }) {
13
+ if (currentCueIndex < currentCueTotal) return {
14
+ pageIndex: currentPageIndex,
15
+ cueIndex: currentCueIndex + 1
16
+ };
17
+ if (currentPageIndex >= totalPages - 1) return null;
18
+ return {
19
+ pageIndex: currentPageIndex + 1,
20
+ cueIndex: 0
21
+ };
22
+ }
23
+ function resolveRetreatFlow({ currentCueIndex, currentPageIndex, previousCueIndex, previousCueTotal }) {
24
+ if (currentCueIndex > 0) return {
25
+ pageIndex: currentPageIndex,
26
+ cueIndex: currentCueIndex - 1
27
+ };
28
+ if (currentPageIndex <= 0) return null;
29
+ return {
30
+ pageIndex: currentPageIndex - 1,
31
+ cueIndex: previousCueIndex ?? previousCueTotal ?? 0
32
+ };
33
+ }
34
+ //#endregion
35
+ export { canAdvanceFlow, canRetreatFlow, clampCueIndex, resolveAdvanceFlow, resolveRetreatFlow };
@@ -0,0 +1,4 @@
1
+ //#region src/presentation/flow/step.d.ts
2
+ declare function normalizeCueStep(step: number | undefined): number | undefined;
3
+ //#endregion
4
+ export { normalizeCueStep };
@@ -0,0 +1,8 @@
1
+ //#region src/presentation/flow/step.ts
2
+ function normalizeCueStep(step) {
3
+ if (step === void 0) return void 0;
4
+ if (!Number.isFinite(step)) return 1;
5
+ return Math.max(1, Math.floor(step));
6
+ }
7
+ //#endregion
8
+ export { normalizeCueStep };
@@ -0,0 +1,73 @@
1
+ //#region src/presentation/session/protocol.d.ts
2
+ declare const PRESENTATION_PROTOCOL_VERSION: 1;
3
+ type PresentationRole = "standalone" | "presenter" | "viewer";
4
+ type SyncedPresentationRole = Exclude<PresentationRole, "standalone">;
5
+ type PresentationSyncMode = "send" | "receive" | "both" | "off";
6
+ interface PresentationCursorState {
7
+ x: number;
8
+ y: number;
9
+ }
10
+ interface PresentationDrawPoint {
11
+ x: number;
12
+ y: number;
13
+ }
14
+ interface PresentationDrawStroke {
15
+ id: string;
16
+ color: string;
17
+ width: number;
18
+ kind?: "pen" | "circle" | "rectangle";
19
+ points: PresentationDrawPoint[];
20
+ }
21
+ type PresentationDrawingsState = Record<string, PresentationDrawStroke[]>;
22
+ interface PresentationSharedState {
23
+ page: number;
24
+ clicks: number;
25
+ clicksTotal: number;
26
+ timer: number;
27
+ cursor: PresentationCursorState | null;
28
+ drawings: PresentationDrawingsState;
29
+ drawingsRevision: number;
30
+ lastUpdate: number;
31
+ }
32
+ interface PresentationEnvelopeBase {
33
+ version: typeof PRESENTATION_PROTOCOL_VERSION;
34
+ sessionId: string;
35
+ senderId: string;
36
+ seq: number;
37
+ timestamp: number;
38
+ }
39
+ interface PresentationJoinEnvelope extends PresentationEnvelopeBase {
40
+ type: "session/join";
41
+ payload: {
42
+ role: SyncedPresentationRole;
43
+ };
44
+ }
45
+ interface PresentationLeaveEnvelope extends PresentationEnvelopeBase {
46
+ type: "session/leave";
47
+ payload: {
48
+ role: SyncedPresentationRole;
49
+ };
50
+ }
51
+ interface PresentationSnapshotEnvelope extends PresentationEnvelopeBase {
52
+ type: "state/snapshot";
53
+ payload: {
54
+ state: PresentationSharedState;
55
+ };
56
+ }
57
+ interface PresentationPatchEnvelope extends PresentationEnvelopeBase {
58
+ type: "state/patch";
59
+ payload: {
60
+ state: Partial<PresentationSharedState>;
61
+ };
62
+ }
63
+ interface PresentationHeartbeatEnvelope extends PresentationEnvelopeBase {
64
+ type: "heartbeat";
65
+ payload: {
66
+ role: SyncedPresentationRole;
67
+ };
68
+ }
69
+ type PresentationEnvelope = PresentationJoinEnvelope | PresentationLeaveEnvelope | PresentationSnapshotEnvelope | PresentationPatchEnvelope | PresentationHeartbeatEnvelope;
70
+ declare function parsePresentationDrawingsState(value: unknown): PresentationDrawingsState | null;
71
+ declare function parsePresentationEnvelope(value: unknown): PresentationEnvelope | null;
72
+ //#endregion
73
+ export { PRESENTATION_PROTOCOL_VERSION, PresentationCursorState, PresentationDrawPoint, PresentationDrawStroke, PresentationDrawingsState, PresentationEnvelope, PresentationHeartbeatEnvelope, PresentationJoinEnvelope, PresentationLeaveEnvelope, PresentationPatchEnvelope, PresentationRole, PresentationSharedState, PresentationSnapshotEnvelope, PresentationSyncMode, SyncedPresentationRole, parsePresentationDrawingsState, parsePresentationEnvelope };
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ //#region src/presentation/session/protocol.ts
3
+ const PRESENTATION_PROTOCOL_VERSION = 1;
4
+ const presentationCursorStateSchema = z.object({
5
+ x: z.number(),
6
+ y: z.number()
7
+ });
8
+ const presentationDrawPointSchema = z.object({
9
+ x: z.number(),
10
+ y: z.number()
11
+ });
12
+ const presentationDrawStrokeSchema = z.object({
13
+ id: z.string(),
14
+ color: z.string(),
15
+ width: z.number(),
16
+ kind: z.enum([
17
+ "pen",
18
+ "circle",
19
+ "rectangle"
20
+ ]).optional(),
21
+ points: z.array(presentationDrawPointSchema)
22
+ });
23
+ const presentationDrawingsStateSchema = z.record(z.string(), z.array(presentationDrawStrokeSchema));
24
+ const presentationSharedStateSchema = z.object({
25
+ page: z.number(),
26
+ clicks: z.number(),
27
+ clicksTotal: z.number(),
28
+ timer: z.number(),
29
+ cursor: presentationCursorStateSchema.nullable(),
30
+ drawings: presentationDrawingsStateSchema,
31
+ drawingsRevision: z.number(),
32
+ lastUpdate: z.number()
33
+ });
34
+ const presentationSharedStatePatchSchema = presentationSharedStateSchema.partial();
35
+ const syncedPresentationRoleSchema = z.enum(["presenter", "viewer"]);
36
+ const presentationEnvelopeBaseSchema = z.object({
37
+ version: z.literal(1),
38
+ sessionId: z.string(),
39
+ senderId: z.string(),
40
+ seq: z.number(),
41
+ timestamp: z.number()
42
+ });
43
+ const sessionJoinEnvelopeSchema = presentationEnvelopeBaseSchema.extend({
44
+ type: z.literal("session/join"),
45
+ payload: z.object({ role: syncedPresentationRoleSchema })
46
+ });
47
+ const sessionLeaveEnvelopeSchema = presentationEnvelopeBaseSchema.extend({
48
+ type: z.literal("session/leave"),
49
+ payload: z.object({ role: syncedPresentationRoleSchema })
50
+ });
51
+ const heartbeatEnvelopeSchema = presentationEnvelopeBaseSchema.extend({
52
+ type: z.literal("heartbeat"),
53
+ payload: z.object({ role: syncedPresentationRoleSchema })
54
+ });
55
+ const snapshotEnvelopeSchema = presentationEnvelopeBaseSchema.extend({
56
+ type: z.literal("state/snapshot"),
57
+ payload: z.object({ state: presentationSharedStateSchema })
58
+ });
59
+ const patchEnvelopeSchema = presentationEnvelopeBaseSchema.extend({
60
+ type: z.literal("state/patch"),
61
+ payload: z.object({ state: presentationSharedStatePatchSchema })
62
+ });
63
+ const presentationEnvelopeSchema = z.discriminatedUnion("type", [
64
+ sessionJoinEnvelopeSchema,
65
+ sessionLeaveEnvelopeSchema,
66
+ heartbeatEnvelopeSchema,
67
+ snapshotEnvelopeSchema,
68
+ patchEnvelopeSchema
69
+ ]);
70
+ function parsePresentationDrawingsState(value) {
71
+ const result = presentationDrawingsStateSchema.safeParse(value);
72
+ return result.success ? result.data : null;
73
+ }
74
+ function parsePresentationEnvelope(value) {
75
+ const result = presentationEnvelopeSchema.safeParse(value);
76
+ return result.success ? result.data : null;
77
+ }
78
+ //#endregion
79
+ export { PRESENTATION_PROTOCOL_VERSION, parsePresentationDrawingsState, parsePresentationEnvelope };
@@ -0,0 +1,16 @@
1
+ import { SlideComponent, SlideMeta } from "./slide.js";
2
+ import { SlidesMeta } from "./slides.js";
3
+
4
+ //#region src/slides/compiled-slides.d.ts
5
+ interface CompiledSlide {
6
+ id: string;
7
+ component: SlideComponent;
8
+ meta: SlideMeta;
9
+ }
10
+ interface CompiledSlidesManifest {
11
+ meta: SlidesMeta;
12
+ slides: CompiledSlide[];
13
+ sourceHash: string;
14
+ }
15
+ //#endregion
16
+ export { CompiledSlide, CompiledSlidesManifest };
@@ -0,0 +1,6 @@
1
+ //#region src/slides/layout.d.ts
2
+ declare const layoutNames: readonly ["default", "center", "cover", "section", "immersive", "two-cols", "image-right", "statement"];
3
+ type BuiltinLayoutName = (typeof layoutNames)[number];
4
+ type LayoutName = BuiltinLayoutName | (string & {});
5
+ //#endregion
6
+ export { BuiltinLayoutName, LayoutName, layoutNames };
@@ -0,0 +1,13 @@
1
+ //#region src/slides/layout.ts
2
+ const layoutNames = [
3
+ "default",
4
+ "center",
5
+ "cover",
6
+ "section",
7
+ "immersive",
8
+ "two-cols",
9
+ "image-right",
10
+ "statement"
11
+ ];
12
+ //#endregion
13
+ export { layoutNames };
@@ -0,0 +1,25 @@
1
+ import { LayoutName } from "./layout.js";
2
+ import { TransitionName } from "./transition.js";
3
+ import { ComponentType } from "react";
4
+
5
+ //#region src/slides/slide.d.ts
6
+ interface SlideMeta {
7
+ title?: string;
8
+ layout?: LayoutName;
9
+ class?: string;
10
+ background?: string;
11
+ transition?: TransitionName;
12
+ clicks?: number;
13
+ notes?: string;
14
+ src?: string;
15
+ }
16
+ interface SlideUnit {
17
+ id: string;
18
+ index: number;
19
+ meta: SlideMeta;
20
+ source: string;
21
+ hasInlineSource: boolean;
22
+ }
23
+ type SlideComponent = ComponentType<Record<string, unknown>>;
24
+ //#endregion
25
+ export { SlideComponent, SlideMeta, SlideUnit };
@@ -0,0 +1,23 @@
1
+ import { LayoutName } from "./layout.js";
2
+ import { TransitionName } from "./transition.js";
3
+ import { SlideUnit } from "./slide.js";
4
+ import { SlidesViewport } from "./viewport.js";
5
+
6
+ //#region src/slides/slides.d.ts
7
+ interface SlidesMeta {
8
+ title?: string;
9
+ theme?: string;
10
+ addons?: string[];
11
+ layout?: LayoutName;
12
+ background?: string;
13
+ transition?: TransitionName;
14
+ exportFilename?: string;
15
+ ar: string;
16
+ viewport: SlidesViewport;
17
+ }
18
+ interface SlidesDocument {
19
+ meta: SlidesMeta;
20
+ slides: SlideUnit[];
21
+ }
22
+ //#endregion
23
+ export { SlidesDocument, SlidesMeta };
@@ -0,0 +1,5 @@
1
+ //#region src/slides/transition.d.ts
2
+ declare const transitionNames: readonly ["fade", "slide-left", "slide-up", "zoom"];
3
+ type TransitionName = (typeof transitionNames)[number];
4
+ //#endregion
5
+ export { TransitionName, transitionNames };
@@ -0,0 +1,9 @@
1
+ //#region src/slides/transition.ts
2
+ const transitionNames = [
3
+ "fade",
4
+ "slide-left",
5
+ "slide-up",
6
+ "zoom"
7
+ ];
8
+ //#endregion
9
+ export { transitionNames };
@@ -0,0 +1,22 @@
1
+ //#region src/slides/viewport.d.ts
2
+ interface SlidesViewport {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ declare const DEFAULT_SLIDES_AR = "16/9";
7
+ declare const DEFAULT_SLIDES_VIEWPORT: SlidesViewport;
8
+ declare function resolveSlidesViewportMeta(ar: string | undefined): {
9
+ ar: string;
10
+ viewport: {
11
+ width: number;
12
+ height: number;
13
+ };
14
+ };
15
+ declare function formatViewportAspectRatio(viewport: SlidesViewport): string;
16
+ declare function isPortraitViewport(viewport: SlidesViewport): boolean;
17
+ declare function resolvePrintPageSize(viewport: SlidesViewport): {
18
+ widthMm: number;
19
+ heightMm: number;
20
+ };
21
+ //#endregion
22
+ export { DEFAULT_SLIDES_AR, DEFAULT_SLIDES_VIEWPORT, SlidesViewport, formatViewportAspectRatio, isPortraitViewport, resolvePrintPageSize, resolveSlidesViewportMeta };
@@ -0,0 +1,49 @@
1
+ //#region src/slides/viewport.ts
2
+ const DEFAULT_SLIDES_AR = "16/9";
3
+ const DEFAULT_SLIDES_VIEWPORT = {
4
+ width: 1920,
5
+ height: 1080
6
+ };
7
+ const VIEWPORT_LONG_EDGE = 1920;
8
+ const ASPECT_RATIO_PATTERN = /^(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)$/;
9
+ function parseAspectRatioUnits(ar) {
10
+ const match = ar.trim().match(ASPECT_RATIO_PATTERN);
11
+ if (!match) throw new Error(`Invalid slides frontmatter: ar must use the form "width/height", received "${ar}"`);
12
+ const widthUnits = Number.parseFloat(match[1]);
13
+ const heightUnits = Number.parseFloat(match[2]);
14
+ if (!Number.isFinite(widthUnits) || !Number.isFinite(heightUnits) || widthUnits <= 0 || heightUnits <= 0) throw new Error(`Invalid slides frontmatter: ar must be a positive ratio, received "${ar}"`);
15
+ return {
16
+ widthUnits,
17
+ heightUnits
18
+ };
19
+ }
20
+ function resolveSlidesViewportMeta(ar) {
21
+ const { widthUnits, heightUnits } = parseAspectRatioUnits(ar?.trim() || "16/9");
22
+ const scale = VIEWPORT_LONG_EDGE / Math.max(widthUnits, heightUnits);
23
+ return {
24
+ ar: `${widthUnits}/${heightUnits}`,
25
+ viewport: {
26
+ width: Math.round(widthUnits * scale),
27
+ height: Math.round(heightUnits * scale)
28
+ }
29
+ };
30
+ }
31
+ function formatViewportAspectRatio(viewport) {
32
+ return `${viewport.width} / ${viewport.height}`;
33
+ }
34
+ function isPortraitViewport(viewport) {
35
+ return viewport.height > viewport.width;
36
+ }
37
+ function resolvePrintPageSize(viewport) {
38
+ const baseMm = 210;
39
+ if (isPortraitViewport(viewport)) return {
40
+ widthMm: baseMm,
41
+ heightMm: Number((baseMm * viewport.height / viewport.width).toFixed(2))
42
+ };
43
+ return {
44
+ widthMm: Number((baseMm * viewport.width / viewport.height).toFixed(2)),
45
+ heightMm: baseMm
46
+ };
47
+ }
48
+ //#endregion
49
+ export { DEFAULT_SLIDES_AR, DEFAULT_SLIDES_VIEWPORT, formatViewportAspectRatio, isPortraitViewport, resolvePrintPageSize, resolveSlidesViewportMeta };
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@slidev-react/core",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Pure presentation models, flow, and shared contracts for slidev-react",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./presentation/session/protocol": {
19
+ "types": "./dist/presentation/session/protocol.d.ts",
20
+ "import": "./dist/presentation/session/protocol.js"
21
+ },
22
+ "./presentation/flow/navigation": {
23
+ "types": "./dist/presentation/flow/navigation.d.ts",
24
+ "import": "./dist/presentation/flow/navigation.js"
25
+ },
26
+ "./presentation/flow/cue": {
27
+ "types": "./dist/presentation/flow/cue.d.ts",
28
+ "import": "./dist/presentation/flow/cue.js"
29
+ },
30
+ "./presentation/flow/step": {
31
+ "types": "./dist/presentation/flow/step.d.ts",
32
+ "import": "./dist/presentation/flow/step.js"
33
+ },
34
+ "./presentation/export/fileNames": {
35
+ "types": "./dist/presentation/export/fileNames.d.ts",
36
+ "import": "./dist/presentation/export/fileNames.js"
37
+ },
38
+ "./presentation/export/selection": {
39
+ "types": "./dist/presentation/export/selection.d.ts",
40
+ "import": "./dist/presentation/export/selection.js"
41
+ },
42
+ "./presentation/export/urls": {
43
+ "types": "./dist/presentation/export/urls.d.ts",
44
+ "import": "./dist/presentation/export/urls.js"
45
+ },
46
+ "./slides/layout": {
47
+ "types": "./dist/slides/layout.d.ts",
48
+ "import": "./dist/slides/layout.js"
49
+ },
50
+ "./slides/transition": {
51
+ "types": "./dist/slides/transition.d.ts",
52
+ "import": "./dist/slides/transition.js"
53
+ },
54
+ "./slides/viewport": {
55
+ "types": "./dist/slides/viewport.d.ts",
56
+ "import": "./dist/slides/viewport.js"
57
+ },
58
+ "./slides/slide": {
59
+ "types": "./dist/slides/slide.d.ts"
60
+ },
61
+ "./slides/slides": {
62
+ "types": "./dist/slides/slides.d.ts"
63
+ },
64
+ "./slides/compiled-slides": {
65
+ "types": "./dist/slides/compiled-slides.d.ts"
66
+ }
67
+ },
68
+ "dependencies": {
69
+ "react": "19.2.3",
70
+ "zod": "4.3.6"
71
+ },
72
+ "publishConfig": {
73
+ "access": "public"
74
+ },
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "git+ssh://git@github.com/hylarucoder/slidev-react.git",
78
+ "directory": "packages/core"
79
+ },
80
+ "homepage": "https://github.com/hylarucoder/slidev-react/tree/main/packages/core",
81
+ "bugs": {
82
+ "url": "https://github.com/hylarucoder/slidev-react/issues"
83
+ },
84
+ "engines": {
85
+ "node": ">=22"
86
+ },
87
+ "scripts": {
88
+ "build:pkg": "pnpm exec tsdown",
89
+ "dev:pkg": "pnpm exec tsdown --watch"
90
+ }
91
+ }