@open-slide/core 0.0.1
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/build-0xQdMJb7.js +14 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +58 -0
- package/dist/config-Dk8ASJ8X.js +324 -0
- package/dist/dev-BN2k5C-N.js +14 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +6 -0
- package/dist/preview-B-xUqFKf.js +12 -0
- package/dist/vite/index.d.ts +18 -0
- package/dist/vite/index.js +3 -0
- package/package.json +67 -0
- package/src/app/App.tsx +14 -0
- package/src/app/components/Player.tsx +61 -0
- package/src/app/components/SlideCanvas.tsx +70 -0
- package/src/app/components/ThumbnailRail.tsx +57 -0
- package/src/app/components/inspector/CommentPopover.tsx +102 -0
- package/src/app/components/inspector/CommentWidget.tsx +63 -0
- package/src/app/components/inspector/InspectOverlay.tsx +94 -0
- package/src/app/components/inspector/InspectorProvider.tsx +75 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +67 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/index.html +12 -0
- package/src/app/lib/decks.ts +8 -0
- package/src/app/lib/inspector/fiber.ts +39 -0
- package/src/app/lib/inspector/useComments.ts +74 -0
- package/src/app/lib/sdk.ts +16 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +10 -0
- package/src/app/routes/Deck.tsx +185 -0
- package/src/app/routes/Home.tsx +98 -0
- package/src/app/styles.css +130 -0
- package/src/app/virtual.d.ts +14 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createViteConfig } from "./config-Dk8ASJ8X.js";
|
|
2
|
+
import { build as build$1 } from "vite";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/build.ts
|
|
5
|
+
async function build() {
|
|
6
|
+
const config = await createViteConfig({
|
|
7
|
+
userCwd: process.cwd(),
|
|
8
|
+
mode: "build"
|
|
9
|
+
});
|
|
10
|
+
await build$1(config);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { build };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/run.ts
|
|
7
|
+
const HELP = `open-slide — author decks, we handle the Vite/React stack
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
open-slide dev Start dev server
|
|
11
|
+
open-slide build Build a static deck site
|
|
12
|
+
open-slide preview Preview the production build
|
|
13
|
+
open-slide --help Show this message
|
|
14
|
+
open-slide --version Print version
|
|
15
|
+
`;
|
|
16
|
+
async function readVersion() {
|
|
17
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkgPath = path.resolve(here, "..", "..", "package.json");
|
|
19
|
+
const raw = await readFile(pkgPath, "utf8");
|
|
20
|
+
return JSON.parse(raw).version;
|
|
21
|
+
}
|
|
22
|
+
async function run(argv) {
|
|
23
|
+
const [cmd] = argv;
|
|
24
|
+
if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
25
|
+
process.stdout.write(HELP);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
29
|
+
process.stdout.write(`${await readVersion()}\n`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (cmd === "dev") {
|
|
33
|
+
const { dev } = await import("../dev-BN2k5C-N.js");
|
|
34
|
+
await dev();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (cmd === "build") {
|
|
38
|
+
const { build } = await import("../build-0xQdMJb7.js");
|
|
39
|
+
await build();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (cmd === "preview") {
|
|
43
|
+
const { preview } = await import("../preview-B-xUqFKf.js");
|
|
44
|
+
await preview();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
process.stderr.write(`Unknown command: ${cmd}\n\n${HELP}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/cli/bin.ts
|
|
53
|
+
run(process.argv.slice(2)).catch((err) => {
|
|
54
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import fs, { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
6
|
+
import react from "@vitejs/plugin-react";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import fg from "fast-glob";
|
|
9
|
+
|
|
10
|
+
//#region src/vite/comments-plugin.ts
|
|
11
|
+
const MARKER_RE = /\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
|
|
12
|
+
const DECK_ID_RE = /^[a-z0-9_-]+$/i;
|
|
13
|
+
function b64urlEncode(s) {
|
|
14
|
+
return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
15
|
+
}
|
|
16
|
+
function b64urlDecode(s) {
|
|
17
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
18
|
+
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
|
|
19
|
+
}
|
|
20
|
+
async function readBody(req) {
|
|
21
|
+
return await new Promise((resolve, reject) => {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
req.on("data", (c) => chunks.push(c));
|
|
24
|
+
req.on("end", () => {
|
|
25
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
26
|
+
if (!raw) return resolve({});
|
|
27
|
+
try {
|
|
28
|
+
resolve(JSON.parse(raw));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
reject(e);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
req.on("error", reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function json(res, status, body) {
|
|
37
|
+
res.statusCode = status;
|
|
38
|
+
res.setHeader("content-type", "application/json");
|
|
39
|
+
res.end(JSON.stringify(body));
|
|
40
|
+
}
|
|
41
|
+
function resolveSlidePath(userCwd, slidesDir, deckId) {
|
|
42
|
+
if (!DECK_ID_RE.test(deckId)) return null;
|
|
43
|
+
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
44
|
+
const full = path.resolve(slidesRoot, deckId, "index.tsx");
|
|
45
|
+
if (!full.startsWith(slidesRoot + path.sep)) return null;
|
|
46
|
+
return full;
|
|
47
|
+
}
|
|
48
|
+
function parseMarkers(source) {
|
|
49
|
+
const comments = [];
|
|
50
|
+
const lines = source.split("\n");
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
const line = lines[i];
|
|
53
|
+
MARKER_RE.lastIndex = 0;
|
|
54
|
+
const m = MARKER_RE.exec(line);
|
|
55
|
+
if (!m) continue;
|
|
56
|
+
const [, id, ts, textB64] = m;
|
|
57
|
+
try {
|
|
58
|
+
const payload = JSON.parse(b64urlDecode(textB64));
|
|
59
|
+
comments.push({
|
|
60
|
+
id,
|
|
61
|
+
line: i + 1,
|
|
62
|
+
ts,
|
|
63
|
+
note: payload.note,
|
|
64
|
+
hint: payload.hint
|
|
65
|
+
});
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
return comments;
|
|
69
|
+
}
|
|
70
|
+
function newId() {
|
|
71
|
+
return `c-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
|
|
72
|
+
}
|
|
73
|
+
function isJsxOpeningLine(line) {
|
|
74
|
+
const t = line.trimStart();
|
|
75
|
+
if (!t.startsWith("<")) return false;
|
|
76
|
+
if (t.startsWith("</")) return false;
|
|
77
|
+
if (t.startsWith("<!")) return false;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find the line index to insert a JSX comment above.
|
|
82
|
+
*
|
|
83
|
+
* Babel's `_debugSource.lineNumber/columnNumber` points at the `<` of a JSX
|
|
84
|
+
* opening tag, but the value can go stale (HMR races) or, per reports, point
|
|
85
|
+
* at a line that's not actually a JSX boundary — e.g. inside an inline style
|
|
86
|
+
* object. Verify with the source of truth before committing.
|
|
87
|
+
*/
|
|
88
|
+
function findSafeInsertLine(lines, line, column) {
|
|
89
|
+
const idx = line - 1;
|
|
90
|
+
if (idx < 0 || idx >= lines.length) return null;
|
|
91
|
+
if (column !== void 0 && lines[idx].charAt(column) === "<") return idx;
|
|
92
|
+
if (isJsxOpeningLine(lines[idx])) return idx;
|
|
93
|
+
const WINDOW = 30;
|
|
94
|
+
for (let i = idx - 1; i >= Math.max(0, idx - WINDOW); i--) if (isJsxOpeningLine(lines[i])) return i;
|
|
95
|
+
for (let i = idx + 1; i < Math.min(lines.length, idx + WINDOW); i++) if (isJsxOpeningLine(lines[i])) return i;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function commentsPlugin(opts) {
|
|
99
|
+
const userCwd = opts.userCwd;
|
|
100
|
+
const slidesDir = opts.slidesDir ?? "slides";
|
|
101
|
+
return {
|
|
102
|
+
name: "open-slide:comments",
|
|
103
|
+
apply: "serve",
|
|
104
|
+
configureServer(server) {
|
|
105
|
+
server.middlewares.use("/__comments", async (req, res, next) => {
|
|
106
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
107
|
+
const method = req.method ?? "GET";
|
|
108
|
+
try {
|
|
109
|
+
if (method === "GET" && url.pathname === "/") {
|
|
110
|
+
const deckId = url.searchParams.get("deckId") ?? "";
|
|
111
|
+
const file = resolveSlidePath(userCwd, slidesDir, deckId);
|
|
112
|
+
if (!file) return json(res, 400, { error: "invalid deckId" });
|
|
113
|
+
let source;
|
|
114
|
+
try {
|
|
115
|
+
source = await fs.readFile(file, "utf8");
|
|
116
|
+
} catch {
|
|
117
|
+
return json(res, 404, { error: "deck not found" });
|
|
118
|
+
}
|
|
119
|
+
return json(res, 200, { comments: parseMarkers(source) });
|
|
120
|
+
}
|
|
121
|
+
if (method === "POST" && url.pathname === "/add") {
|
|
122
|
+
const body = await readBody(req);
|
|
123
|
+
const deckId = body.deckId ?? "";
|
|
124
|
+
const file = resolveSlidePath(userCwd, slidesDir, deckId);
|
|
125
|
+
if (!file) return json(res, 400, { error: "invalid deckId" });
|
|
126
|
+
if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
|
|
127
|
+
if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
|
|
128
|
+
let source;
|
|
129
|
+
try {
|
|
130
|
+
source = await fs.readFile(file, "utf8");
|
|
131
|
+
} catch {
|
|
132
|
+
return json(res, 404, { error: "deck not found" });
|
|
133
|
+
}
|
|
134
|
+
const lines = source.split("\n");
|
|
135
|
+
const idx = findSafeInsertLine(lines, body.line, body.column);
|
|
136
|
+
if (idx === null) return json(res, 422, { error: `could not find a safe JSX boundary near line ${body.line}. Try clicking a different element.` });
|
|
137
|
+
const indent = lines[idx].match(/^\s*/)?.[0] ?? "";
|
|
138
|
+
const id = newId();
|
|
139
|
+
const ts = new Date().toISOString();
|
|
140
|
+
const payload = b64urlEncode(JSON.stringify({
|
|
141
|
+
note: body.text,
|
|
142
|
+
hint: body.hint
|
|
143
|
+
}));
|
|
144
|
+
const marker = `${indent}{/* @slide-comment id="${id}" ts="${ts}" text="${payload}" */}`;
|
|
145
|
+
lines.splice(idx, 0, marker);
|
|
146
|
+
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
147
|
+
return json(res, 200, {
|
|
148
|
+
id,
|
|
149
|
+
line: idx + 1
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (method === "DELETE" && url.pathname.startsWith("/")) {
|
|
153
|
+
const id = url.pathname.slice(1);
|
|
154
|
+
if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
|
|
155
|
+
const deckId = url.searchParams.get("deckId") ?? "";
|
|
156
|
+
const file = resolveSlidePath(userCwd, slidesDir, deckId);
|
|
157
|
+
if (!file) return json(res, 400, { error: "invalid deckId" });
|
|
158
|
+
let source;
|
|
159
|
+
try {
|
|
160
|
+
source = await fs.readFile(file, "utf8");
|
|
161
|
+
} catch {
|
|
162
|
+
return json(res, 404, { error: "deck not found" });
|
|
163
|
+
}
|
|
164
|
+
const lines = source.split("\n");
|
|
165
|
+
const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
|
|
166
|
+
const hit = lines.findIndex((l) => idRe.test(l));
|
|
167
|
+
if (hit === -1) return json(res, 404, { error: "marker not found" });
|
|
168
|
+
lines.splice(hit, 1);
|
|
169
|
+
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
170
|
+
return json(res, 200, { ok: true });
|
|
171
|
+
}
|
|
172
|
+
next();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/vite/open-slide-plugin.ts
|
|
183
|
+
const DECKS_VMOD = "virtual:open-slide/decks";
|
|
184
|
+
const CONFIG_VMOD = "virtual:open-slide/config";
|
|
185
|
+
function resolved(id) {
|
|
186
|
+
return `\0${id}`;
|
|
187
|
+
}
|
|
188
|
+
async function findDecks(userCwd, slidesDir) {
|
|
189
|
+
const abs = path.resolve(userCwd, slidesDir);
|
|
190
|
+
if (!existsSync(abs)) return [];
|
|
191
|
+
const hits = await fg("*/index.{tsx,jsx,ts,js}", {
|
|
192
|
+
cwd: abs,
|
|
193
|
+
absolute: true,
|
|
194
|
+
onlyFiles: true
|
|
195
|
+
});
|
|
196
|
+
return hits.sort();
|
|
197
|
+
}
|
|
198
|
+
function toId(absFile, slidesRoot) {
|
|
199
|
+
const rel = path.relative(slidesRoot, absFile);
|
|
200
|
+
return rel.split(path.sep)[0];
|
|
201
|
+
}
|
|
202
|
+
function generateDecksModule(files, slidesRoot, isDev) {
|
|
203
|
+
const entries = files.map((abs) => {
|
|
204
|
+
const id = toId(abs, slidesRoot);
|
|
205
|
+
const importPath = isDev ? `/@fs${abs}` : abs;
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
importPath
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
const ids = JSON.stringify(entries.map((e) => e.id).sort());
|
|
212
|
+
const cases = entries.map((e) => ` case ${JSON.stringify(e.id)}: return import(${JSON.stringify(e.importPath)});`).join("\n");
|
|
213
|
+
return `// virtual:open-slide/decks — generated
|
|
214
|
+
export const deckIds = ${ids};
|
|
215
|
+
|
|
216
|
+
export async function loadDeck(id) {
|
|
217
|
+
switch (id) {
|
|
218
|
+
${cases}
|
|
219
|
+
default: throw new Error('Deck not found: ' + id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
function openSlidePlugin(opts) {
|
|
225
|
+
const { userCwd, config } = opts;
|
|
226
|
+
const slidesDir = config.slidesDir ?? "slides";
|
|
227
|
+
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
228
|
+
let isDev = false;
|
|
229
|
+
return {
|
|
230
|
+
name: "open-slide",
|
|
231
|
+
config(_c, env) {
|
|
232
|
+
isDev = env.command === "serve";
|
|
233
|
+
return { server: { fs: { allow: [userCwd] } } };
|
|
234
|
+
},
|
|
235
|
+
resolveId(id) {
|
|
236
|
+
if (id === DECKS_VMOD) return resolved(DECKS_VMOD);
|
|
237
|
+
if (id === CONFIG_VMOD) return resolved(CONFIG_VMOD);
|
|
238
|
+
return null;
|
|
239
|
+
},
|
|
240
|
+
async load(id) {
|
|
241
|
+
if (id === resolved(DECKS_VMOD)) {
|
|
242
|
+
const files = await findDecks(userCwd, slidesDir);
|
|
243
|
+
return generateDecksModule(files, slidesRoot, isDev);
|
|
244
|
+
}
|
|
245
|
+
if (id === resolved(CONFIG_VMOD)) return `export default ${JSON.stringify(config)};\n`;
|
|
246
|
+
return null;
|
|
247
|
+
},
|
|
248
|
+
configureServer(server) {
|
|
249
|
+
const reload = () => {
|
|
250
|
+
const mod = server.moduleGraph.getModuleById(resolved(DECKS_VMOD));
|
|
251
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
252
|
+
server.ws.send({ type: "full-reload" });
|
|
253
|
+
};
|
|
254
|
+
server.watcher.add(path.join(slidesRoot, "*"));
|
|
255
|
+
server.watcher.on("add", (p) => {
|
|
256
|
+
if (p.startsWith(slidesRoot)) reload();
|
|
257
|
+
});
|
|
258
|
+
server.watcher.on("unlink", (p) => {
|
|
259
|
+
if (p.startsWith(slidesRoot)) reload();
|
|
260
|
+
});
|
|
261
|
+
server.middlewares.use("/__open-slide/title", (_req, res) => {
|
|
262
|
+
res.setHeader("content-type", "application/json");
|
|
263
|
+
res.end(JSON.stringify({ title: config.title ?? null }));
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async function loadUserConfig(userCwd) {
|
|
269
|
+
const file = path.join(userCwd, "open-slide.json");
|
|
270
|
+
if (!existsSync(file)) return {};
|
|
271
|
+
const raw = await readFile(file, "utf8");
|
|
272
|
+
return JSON.parse(raw);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/vite/config.ts
|
|
277
|
+
function findPackageRoot(fromFile) {
|
|
278
|
+
let dir = path.dirname(fromFile);
|
|
279
|
+
while (dir !== path.dirname(dir)) {
|
|
280
|
+
if (existsSync(path.join(dir, "package.json"))) return dir;
|
|
281
|
+
dir = path.dirname(dir);
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`Could not find package.json walking up from ${fromFile}`);
|
|
284
|
+
}
|
|
285
|
+
const PKG_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
|
|
286
|
+
const APP_ROOT = path.join(PKG_ROOT, "src", "app");
|
|
287
|
+
async function createViteConfig(opts) {
|
|
288
|
+
const userCwd = path.resolve(opts.userCwd);
|
|
289
|
+
const config = opts.config ?? await loadUserConfig(userCwd);
|
|
290
|
+
const slidesDir = config.slidesDir ?? "slides";
|
|
291
|
+
const slidesAbs = path.resolve(userCwd, slidesDir);
|
|
292
|
+
return {
|
|
293
|
+
root: APP_ROOT,
|
|
294
|
+
configFile: false,
|
|
295
|
+
plugins: [
|
|
296
|
+
react(),
|
|
297
|
+
tailwindcss(),
|
|
298
|
+
openSlidePlugin({
|
|
299
|
+
userCwd,
|
|
300
|
+
config
|
|
301
|
+
}),
|
|
302
|
+
commentsPlugin({
|
|
303
|
+
userCwd,
|
|
304
|
+
slidesDir
|
|
305
|
+
})
|
|
306
|
+
],
|
|
307
|
+
resolve: { alias: { "@": APP_ROOT } },
|
|
308
|
+
server: {
|
|
309
|
+
port: config.port ?? 5173,
|
|
310
|
+
fs: { allow: [
|
|
311
|
+
APP_ROOT,
|
|
312
|
+
userCwd,
|
|
313
|
+
slidesAbs
|
|
314
|
+
] }
|
|
315
|
+
},
|
|
316
|
+
build: {
|
|
317
|
+
outDir: path.resolve(userCwd, "dist"),
|
|
318
|
+
emptyOutDir: true
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
export { createViteConfig };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createViteConfig } from "./config-Dk8ASJ8X.js";
|
|
2
|
+
import { createServer } from "vite";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/dev.ts
|
|
5
|
+
async function dev() {
|
|
6
|
+
const config = await createViteConfig({ userCwd: process.cwd() });
|
|
7
|
+
const server = await createServer(config);
|
|
8
|
+
await server.listen();
|
|
9
|
+
server.printUrls();
|
|
10
|
+
server.bindCLIShortcuts({ print: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { dev };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/app/lib/sdk.d.ts
|
|
4
|
+
type SlidePage = ComponentType;
|
|
5
|
+
type DeckMeta = {
|
|
6
|
+
title?: string;
|
|
7
|
+
theme?: 'light' | 'dark';
|
|
8
|
+
};
|
|
9
|
+
type DeckModule = {
|
|
10
|
+
default: SlidePage[];
|
|
11
|
+
meta?: DeckMeta;
|
|
12
|
+
};
|
|
13
|
+
declare const CANVAS_WIDTH = 1920;
|
|
14
|
+
declare const CANVAS_HEIGHT = 1080;
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { CANVAS_HEIGHT, CANVAS_WIDTH, DeckMeta, DeckModule, SlidePage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createViteConfig } from "./config-Dk8ASJ8X.js";
|
|
2
|
+
import { preview as preview$1 } from "vite";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/preview.ts
|
|
5
|
+
async function preview() {
|
|
6
|
+
const config = await createViteConfig({ userCwd: process.cwd() });
|
|
7
|
+
const server = await preview$1(config);
|
|
8
|
+
server.printUrls();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { preview };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { InlineConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/vite/open-slide-plugin.d.ts
|
|
4
|
+
type OpenSlideConfig = {
|
|
5
|
+
title?: string;
|
|
6
|
+
slidesDir?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
}; //#endregion
|
|
9
|
+
//#region src/vite/config.d.ts
|
|
10
|
+
type CreateViteConfigOptions = {
|
|
11
|
+
userCwd: string;
|
|
12
|
+
config?: OpenSlideConfig;
|
|
13
|
+
mode?: 'serve' | 'build';
|
|
14
|
+
};
|
|
15
|
+
declare function createViteConfig(opts: CreateViteConfigOptions): Promise<InlineConfig>;
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { OpenSlideConfig, createViteConfig };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-slide/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Runtime and CLI for open-slide — write decks in slides/, we handle the rest.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./vite": {
|
|
12
|
+
"types": "./dist/vite/index.d.ts",
|
|
13
|
+
"import": "./dist/vite/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"open-slide": "./dist/cli/bin.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src/app",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsdown",
|
|
26
|
+
"check": "tsc --noEmit",
|
|
27
|
+
"prepack": "pnpm build"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"slides",
|
|
34
|
+
"presentation",
|
|
35
|
+
"react",
|
|
36
|
+
"vite"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@fontsource-variable/geist": "^5.2.8",
|
|
44
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
45
|
+
"@vitejs/plugin-react": "^4.3.3",
|
|
46
|
+
"class-variance-authority": "^0.7.1",
|
|
47
|
+
"clsx": "^2.1.1",
|
|
48
|
+
"fast-glob": "^3.3.2",
|
|
49
|
+
"lucide-react": "^1.8.0",
|
|
50
|
+
"radix-ui": "^1.4.3",
|
|
51
|
+
"react": "^18.3.1",
|
|
52
|
+
"react-dom": "^18.3.1",
|
|
53
|
+
"react-router-dom": "^6.26.2",
|
|
54
|
+
"shadcn": "^4.3.0",
|
|
55
|
+
"tailwind-merge": "^3.5.0",
|
|
56
|
+
"tailwindcss": "^4.2.2",
|
|
57
|
+
"tw-animate-css": "^1.4.0",
|
|
58
|
+
"vite": "^5.4.10"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^22.19.17",
|
|
62
|
+
"@types/react": "^18.3.12",
|
|
63
|
+
"@types/react-dom": "^18.3.1",
|
|
64
|
+
"tsdown": "^0.9.9",
|
|
65
|
+
"typescript": "^5.9.3"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/app/App.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
2
|
+
import { Home } from './routes/Home';
|
|
3
|
+
import { Deck } from './routes/Deck';
|
|
4
|
+
|
|
5
|
+
export function App() {
|
|
6
|
+
return (
|
|
7
|
+
<BrowserRouter>
|
|
8
|
+
<Routes>
|
|
9
|
+
<Route path="/" element={<Home />} />
|
|
10
|
+
<Route path="/d/:deckId" element={<Deck />} />
|
|
11
|
+
</Routes>
|
|
12
|
+
</BrowserRouter>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import type { SlidePage } from '../lib/sdk';
|
|
3
|
+
import { SlideCanvas } from './SlideCanvas';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
pages: SlidePage[];
|
|
7
|
+
index: number;
|
|
8
|
+
onIndexChange: (index: number) => void;
|
|
9
|
+
onExit: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function Player({ pages, index, onIndexChange, onExit }: Props) {
|
|
13
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const el = rootRef.current;
|
|
17
|
+
if (!el) return;
|
|
18
|
+
if (document.fullscreenElement !== el) {
|
|
19
|
+
el.requestFullscreen?.().catch(() => {});
|
|
20
|
+
}
|
|
21
|
+
return () => {
|
|
22
|
+
if (document.fullscreenElement) document.exitFullscreen?.().catch(() => {});
|
|
23
|
+
};
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const onFsChange = () => {
|
|
28
|
+
if (!document.fullscreenElement) onExit();
|
|
29
|
+
};
|
|
30
|
+
document.addEventListener('fullscreenchange', onFsChange);
|
|
31
|
+
return () => document.removeEventListener('fullscreenchange', onFsChange);
|
|
32
|
+
}, [onExit]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const onKey = (e: KeyboardEvent) => {
|
|
36
|
+
if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
if (index < pages.length - 1) onIndexChange(index + 1);
|
|
39
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
if (index > 0) onIndexChange(index - 1);
|
|
42
|
+
} else if (e.key === 'Escape') {
|
|
43
|
+
onExit();
|
|
44
|
+
} else if (e.key === 'Home') {
|
|
45
|
+
onIndexChange(0);
|
|
46
|
+
} else if (e.key === 'End') {
|
|
47
|
+
onIndexChange(pages.length - 1);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
window.addEventListener('keydown', onKey);
|
|
51
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
52
|
+
}, [index, pages.length, onIndexChange, onExit]);
|
|
53
|
+
|
|
54
|
+
const Page = pages[index];
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div ref={rootRef} className="flex h-screen w-screen items-center justify-center bg-black">
|
|
58
|
+
<SlideCanvas flat>{Page ? <Page /> : null}</SlideCanvas>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../lib/sdk';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/** If set, use this scale directly (e.g., thumbnails). Otherwise fit to container. */
|
|
8
|
+
scale?: number;
|
|
9
|
+
/** Center the canvas within the container (default true). */
|
|
10
|
+
center?: boolean;
|
|
11
|
+
/** Flat mode: no rounded corners or drop shadow. */
|
|
12
|
+
flat?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function SlideCanvas({ children, scale, center = true, flat = false, className }: Props) {
|
|
17
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const [fitScale, setFitScale] = useState(1);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (scale !== undefined) return;
|
|
22
|
+
const el = containerRef.current;
|
|
23
|
+
if (!el) return;
|
|
24
|
+
const ro = new ResizeObserver(() => {
|
|
25
|
+
const { width, height } = el.getBoundingClientRect();
|
|
26
|
+
if (width === 0 || height === 0) return;
|
|
27
|
+
setFitScale(Math.min(width / CANVAS_WIDTH, height / CANVAS_HEIGHT));
|
|
28
|
+
});
|
|
29
|
+
ro.observe(el);
|
|
30
|
+
return () => ro.disconnect();
|
|
31
|
+
}, [scale]);
|
|
32
|
+
|
|
33
|
+
const s = scale ?? fitScale;
|
|
34
|
+
const scaledW = CANVAS_WIDTH * s;
|
|
35
|
+
const scaledH = CANVAS_HEIGHT * s;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div ref={containerRef} className={cn('relative h-full w-full overflow-hidden', className)}>
|
|
39
|
+
<div
|
|
40
|
+
className={cn(
|
|
41
|
+
'overflow-hidden bg-white text-black',
|
|
42
|
+
!flat && 'rounded-md shadow-xl ring-1 ring-black/5',
|
|
43
|
+
)}
|
|
44
|
+
style={{
|
|
45
|
+
width: scaledW,
|
|
46
|
+
height: scaledH,
|
|
47
|
+
...(center
|
|
48
|
+
? {
|
|
49
|
+
position: 'absolute',
|
|
50
|
+
left: '50%',
|
|
51
|
+
top: '50%',
|
|
52
|
+
transform: `translate(-50%, -50%)`,
|
|
53
|
+
}
|
|
54
|
+
: {}),
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<div
|
|
58
|
+
style={{
|
|
59
|
+
width: CANVAS_WIDTH,
|
|
60
|
+
height: CANVAS_HEIGHT,
|
|
61
|
+
transform: `scale(${s})`,
|
|
62
|
+
transformOrigin: 'top left',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|