@open-slide/core 0.0.2 → 0.0.3
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/bin.js +2 -0
- package/dist/{build-DJGuOT6x.js → build-Cav2jYyI.js} +1 -1
- package/dist/cli/bin.js +5 -5
- package/dist/{config-Opp2R1Jf.js → config-g-uy_P5U.js} +218 -39
- package/dist/{dev-0SG0ArzD.js → dev-CFmlBbLh.js} +1 -1
- package/dist/index.d.ts +7 -9
- package/dist/{preview-61Aawrlg.js → preview-CotwHU_d.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +5 -3
- package/src/app/App.tsx +2 -2
- package/src/app/components/Player.tsx +4 -4
- package/src/app/components/ThumbnailRail.tsx +5 -5
- package/src/app/components/inspector/InspectOverlay.tsx +4 -4
- package/src/app/components/inspector/InspectorProvider.tsx +5 -5
- package/src/app/components/sidebar/FolderItem.tsx +191 -0
- package/src/app/components/sidebar/IconPicker.tsx +59 -0
- package/src/app/components/sidebar/Sidebar.tsx +118 -0
- package/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/src/app/components/ui/popover.tsx +87 -0
- package/src/app/components/ui/tabs.tsx +89 -0
- package/src/app/lib/folders.ts +130 -0
- package/src/app/lib/inspector/fiber.ts +2 -2
- package/src/app/lib/inspector/useComments.ts +8 -8
- package/src/app/lib/sdk.ts +20 -5
- package/src/app/lib/slides.ts +8 -0
- package/src/app/routes/Home.tsx +151 -62
- package/src/app/routes/{Deck.tsx → Slide.tsx} +17 -17
- package/src/app/virtual.d.ts +4 -4
- package/src/app/lib/decks.ts +0 -8
package/bin.js
ADDED
package/dist/cli/bin.js
CHANGED
|
@@ -4,11 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
//#region src/cli/run.ts
|
|
7
|
-
const HELP = `open-slide — author
|
|
7
|
+
const HELP = `open-slide — author slides, we handle the Vite/React stack
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
10
|
open-slide dev Start dev server
|
|
11
|
-
open-slide build Build a static
|
|
11
|
+
open-slide build Build a static site
|
|
12
12
|
open-slide preview Preview the production build
|
|
13
13
|
open-slide --help Show this message
|
|
14
14
|
open-slide --version Print version
|
|
@@ -30,17 +30,17 @@ async function run(argv) {
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
if (cmd === "dev") {
|
|
33
|
-
const { dev } = await import("../dev-
|
|
33
|
+
const { dev } = await import("../dev-CFmlBbLh.js");
|
|
34
34
|
await dev();
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
if (cmd === "build") {
|
|
38
|
-
const { build } = await import("../build-
|
|
38
|
+
const { build } = await import("../build-Cav2jYyI.js");
|
|
39
39
|
await build();
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
if (cmd === "preview") {
|
|
43
|
-
const { preview } = await import("../preview-
|
|
43
|
+
const { preview } = await import("../preview-CotwHU_d.js");
|
|
44
44
|
await preview();
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
@@ -9,7 +9,7 @@ import fg from "fast-glob";
|
|
|
9
9
|
|
|
10
10
|
//#region src/vite/comments-plugin.ts
|
|
11
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
|
|
12
|
+
const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
|
|
13
13
|
function b64urlEncode(s) {
|
|
14
14
|
return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
15
15
|
}
|
|
@@ -17,7 +17,7 @@ function b64urlDecode(s) {
|
|
|
17
17
|
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
18
18
|
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
|
|
19
19
|
}
|
|
20
|
-
async function readBody(req) {
|
|
20
|
+
async function readBody$1(req) {
|
|
21
21
|
return await new Promise((resolve, reject) => {
|
|
22
22
|
const chunks = [];
|
|
23
23
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -33,15 +33,15 @@ async function readBody(req) {
|
|
|
33
33
|
req.on("error", reject);
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
function json(res, status, body) {
|
|
36
|
+
function json$1(res, status, body) {
|
|
37
37
|
res.statusCode = status;
|
|
38
38
|
res.setHeader("content-type", "application/json");
|
|
39
39
|
res.end(JSON.stringify(body));
|
|
40
40
|
}
|
|
41
|
-
function resolveSlidePath(userCwd, slidesDir,
|
|
42
|
-
if (!
|
|
41
|
+
function resolveSlidePath(userCwd, slidesDir, slideId) {
|
|
42
|
+
if (!SLIDE_ID_RE$1.test(slideId)) return null;
|
|
43
43
|
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
44
|
-
const full = path.resolve(slidesRoot,
|
|
44
|
+
const full = path.resolve(slidesRoot, slideId, "index.tsx");
|
|
45
45
|
if (!full.startsWith(slidesRoot + path.sep)) return null;
|
|
46
46
|
return full;
|
|
47
47
|
}
|
|
@@ -107,33 +107,33 @@ function commentsPlugin(opts) {
|
|
|
107
107
|
const method = req.method ?? "GET";
|
|
108
108
|
try {
|
|
109
109
|
if (method === "GET" && url.pathname === "/") {
|
|
110
|
-
const
|
|
111
|
-
const file = resolveSlidePath(userCwd, slidesDir,
|
|
112
|
-
if (!file) return json(res, 400, { error: "invalid
|
|
110
|
+
const slideId = url.searchParams.get("slideId") ?? "";
|
|
111
|
+
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
112
|
+
if (!file) return json$1(res, 400, { error: "invalid slideId" });
|
|
113
113
|
let source;
|
|
114
114
|
try {
|
|
115
115
|
source = await fs.readFile(file, "utf8");
|
|
116
116
|
} catch {
|
|
117
|
-
return json(res, 404, { error: "
|
|
117
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
118
118
|
}
|
|
119
|
-
return json(res, 200, { comments: parseMarkers(source) });
|
|
119
|
+
return json$1(res, 200, { comments: parseMarkers(source) });
|
|
120
120
|
}
|
|
121
121
|
if (method === "POST" && url.pathname === "/add") {
|
|
122
|
-
const body = await readBody(req);
|
|
123
|
-
const
|
|
124
|
-
const file = resolveSlidePath(userCwd, slidesDir,
|
|
125
|
-
if (!file) return json(res, 400, { error: "invalid
|
|
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" });
|
|
122
|
+
const body = await readBody$1(req);
|
|
123
|
+
const slideId = body.slideId ?? "";
|
|
124
|
+
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
125
|
+
if (!file) return json$1(res, 400, { error: "invalid slideId" });
|
|
126
|
+
if (!body.line || body.line < 1) return json$1(res, 400, { error: "invalid line" });
|
|
127
|
+
if (!body.text || typeof body.text !== "string") return json$1(res, 400, { error: "missing text" });
|
|
128
128
|
let source;
|
|
129
129
|
try {
|
|
130
130
|
source = await fs.readFile(file, "utf8");
|
|
131
131
|
} catch {
|
|
132
|
-
return json(res, 404, { error: "
|
|
132
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
133
133
|
}
|
|
134
134
|
const lines = source.split("\n");
|
|
135
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.` });
|
|
136
|
+
if (idx === null) return json$1(res, 422, { error: `could not find a safe JSX boundary near line ${body.line}. Try clicking a different element.` });
|
|
137
137
|
const indent = lines[idx].match(/^\s*/)?.[0] ?? "";
|
|
138
138
|
const id = newId();
|
|
139
139
|
const ts = new Date().toISOString();
|
|
@@ -144,31 +144,205 @@ function commentsPlugin(opts) {
|
|
|
144
144
|
const marker = `${indent}{/* @slide-comment id="${id}" ts="${ts}" text="${payload}" */}`;
|
|
145
145
|
lines.splice(idx, 0, marker);
|
|
146
146
|
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
147
|
-
return json(res, 200, {
|
|
147
|
+
return json$1(res, 200, {
|
|
148
148
|
id,
|
|
149
149
|
line: idx + 1
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
if (method === "DELETE" && url.pathname.startsWith("/")) {
|
|
153
153
|
const id = url.pathname.slice(1);
|
|
154
|
-
if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
|
|
155
|
-
const
|
|
156
|
-
const file = resolveSlidePath(userCwd, slidesDir,
|
|
157
|
-
if (!file) return json(res, 400, { error: "invalid
|
|
154
|
+
if (!/^c-[a-f0-9]+$/.test(id)) return json$1(res, 400, { error: "invalid id" });
|
|
155
|
+
const slideId = url.searchParams.get("slideId") ?? "";
|
|
156
|
+
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
157
|
+
if (!file) return json$1(res, 400, { error: "invalid slideId" });
|
|
158
158
|
let source;
|
|
159
159
|
try {
|
|
160
160
|
source = await fs.readFile(file, "utf8");
|
|
161
161
|
} catch {
|
|
162
|
-
return json(res, 404, { error: "
|
|
162
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
163
163
|
}
|
|
164
164
|
const lines = source.split("\n");
|
|
165
165
|
const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
|
|
166
166
|
const hit = lines.findIndex((l) => idRe.test(l));
|
|
167
|
-
if (hit === -1) return json(res, 404, { error: "marker not found" });
|
|
167
|
+
if (hit === -1) return json$1(res, 404, { error: "marker not found" });
|
|
168
168
|
lines.splice(hit, 1);
|
|
169
169
|
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
170
|
+
return json$1(res, 200, { ok: true });
|
|
171
|
+
}
|
|
172
|
+
next();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
json$1(res, 500, { error: String(err.message ?? err) });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/vite/folders-plugin.ts
|
|
183
|
+
const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
|
|
184
|
+
const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
|
|
185
|
+
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
186
|
+
async function readBody(req) {
|
|
187
|
+
return await new Promise((resolve, reject) => {
|
|
188
|
+
const chunks = [];
|
|
189
|
+
req.on("data", (c) => chunks.push(c));
|
|
190
|
+
req.on("end", () => {
|
|
191
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
192
|
+
if (!raw) return resolve({});
|
|
193
|
+
try {
|
|
194
|
+
resolve(JSON.parse(raw));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
reject(e);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
req.on("error", reject);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function json(res, status, body) {
|
|
203
|
+
res.statusCode = status;
|
|
204
|
+
res.setHeader("content-type", "application/json");
|
|
205
|
+
res.end(JSON.stringify(body));
|
|
206
|
+
}
|
|
207
|
+
function emptyManifest() {
|
|
208
|
+
return {
|
|
209
|
+
folders: [],
|
|
210
|
+
assignments: {}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function readManifest(file) {
|
|
214
|
+
try {
|
|
215
|
+
const raw = await fs.readFile(file, "utf8");
|
|
216
|
+
const parsed = JSON.parse(raw);
|
|
217
|
+
return {
|
|
218
|
+
folders: Array.isArray(parsed.folders) ? parsed.folders : [],
|
|
219
|
+
assignments: parsed.assignments && typeof parsed.assignments === "object" ? parsed.assignments : {}
|
|
220
|
+
};
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err.code === "ENOENT") return emptyManifest();
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function writeManifest(file, manifest) {
|
|
227
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
228
|
+
await fs.writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
229
|
+
}
|
|
230
|
+
function newFolderId() {
|
|
231
|
+
return `f-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
|
|
232
|
+
}
|
|
233
|
+
function validateName(v) {
|
|
234
|
+
if (typeof v !== "string") return null;
|
|
235
|
+
const trimmed = v.trim();
|
|
236
|
+
if (trimmed.length < 1 || trimmed.length > 40) return null;
|
|
237
|
+
return trimmed;
|
|
238
|
+
}
|
|
239
|
+
function validateIcon(v) {
|
|
240
|
+
if (!v || typeof v !== "object") return null;
|
|
241
|
+
const icon = v;
|
|
242
|
+
if (icon.type === "emoji") {
|
|
243
|
+
if (typeof icon.value !== "string") return null;
|
|
244
|
+
if (icon.value.length < 1 || icon.value.length > 8) return null;
|
|
245
|
+
return {
|
|
246
|
+
type: "emoji",
|
|
247
|
+
value: icon.value
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (icon.type === "color") {
|
|
251
|
+
if (typeof icon.value !== "string" || !COLOR_RE.test(icon.value)) return null;
|
|
252
|
+
return {
|
|
253
|
+
type: "color",
|
|
254
|
+
value: icon.value
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
function foldersPlugin(opts) {
|
|
260
|
+
const userCwd = opts.userCwd;
|
|
261
|
+
const slidesDir = opts.slidesDir ?? "slides";
|
|
262
|
+
const slidesRoot = path.resolve(userCwd, slidesDir);
|
|
263
|
+
const manifestPath = path.join(slidesRoot, ".folders.json");
|
|
264
|
+
return {
|
|
265
|
+
name: "open-slide:folders",
|
|
266
|
+
apply: "serve",
|
|
267
|
+
configureServer(server) {
|
|
268
|
+
server.watcher.add(manifestPath);
|
|
269
|
+
server.watcher.on("change", (p) => {
|
|
270
|
+
if (p === manifestPath) server.ws.send({
|
|
271
|
+
type: "custom",
|
|
272
|
+
event: "open-slide:folders-changed"
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
server.middlewares.use("/__folders", async (req, res, next) => {
|
|
276
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
277
|
+
const method = req.method ?? "GET";
|
|
278
|
+
try {
|
|
279
|
+
if (method === "GET" && url.pathname === "/") {
|
|
280
|
+
const manifest = await readManifest(manifestPath);
|
|
281
|
+
return json(res, 200, manifest);
|
|
282
|
+
}
|
|
283
|
+
if (method === "POST" && url.pathname === "/") {
|
|
284
|
+
const body = await readBody(req);
|
|
285
|
+
const name = validateName(body.name);
|
|
286
|
+
if (!name) return json(res, 400, { error: "invalid name" });
|
|
287
|
+
const icon = validateIcon(body.icon);
|
|
288
|
+
if (!icon) return json(res, 400, { error: "invalid icon" });
|
|
289
|
+
const manifest = await readManifest(manifestPath);
|
|
290
|
+
const folder = {
|
|
291
|
+
id: newFolderId(),
|
|
292
|
+
name,
|
|
293
|
+
icon
|
|
294
|
+
};
|
|
295
|
+
manifest.folders.push(folder);
|
|
296
|
+
await writeManifest(manifestPath, manifest);
|
|
297
|
+
return json(res, 200, folder);
|
|
298
|
+
}
|
|
299
|
+
if (method === "PUT" && url.pathname === "/assign") {
|
|
300
|
+
const body = await readBody(req);
|
|
301
|
+
if (typeof body.slideId !== "string" || !SLIDE_ID_RE.test(body.slideId)) return json(res, 400, { error: "invalid slideId" });
|
|
302
|
+
const slideId = body.slideId;
|
|
303
|
+
let folderId;
|
|
304
|
+
if (body.folderId === null) folderId = null;
|
|
305
|
+
else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
|
|
306
|
+
else return json(res, 400, { error: "invalid folderId" });
|
|
307
|
+
const manifest = await readManifest(manifestPath);
|
|
308
|
+
if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json(res, 404, { error: "folder not found" });
|
|
309
|
+
if (folderId === null) delete manifest.assignments[slideId];
|
|
310
|
+
else manifest.assignments[slideId] = folderId;
|
|
311
|
+
await writeManifest(manifestPath, manifest);
|
|
170
312
|
return json(res, 200, { ok: true });
|
|
171
313
|
}
|
|
314
|
+
const idMatch = url.pathname.match(/^\/([^/]+)$/);
|
|
315
|
+
if (idMatch) {
|
|
316
|
+
const id = idMatch[1];
|
|
317
|
+
if (!FOLDER_ID_RE.test(id)) return json(res, 400, { error: "invalid id" });
|
|
318
|
+
if (method === "PATCH") {
|
|
319
|
+
const body = await readBody(req);
|
|
320
|
+
const manifest = await readManifest(manifestPath);
|
|
321
|
+
const folder = manifest.folders.find((f) => f.id === id);
|
|
322
|
+
if (!folder) return json(res, 404, { error: "folder not found" });
|
|
323
|
+
if (body.name !== void 0) {
|
|
324
|
+
const name = validateName(body.name);
|
|
325
|
+
if (!name) return json(res, 400, { error: "invalid name" });
|
|
326
|
+
folder.name = name;
|
|
327
|
+
}
|
|
328
|
+
if (body.icon !== void 0) {
|
|
329
|
+
const icon = validateIcon(body.icon);
|
|
330
|
+
if (!icon) return json(res, 400, { error: "invalid icon" });
|
|
331
|
+
folder.icon = icon;
|
|
332
|
+
}
|
|
333
|
+
await writeManifest(manifestPath, manifest);
|
|
334
|
+
return json(res, 200, folder);
|
|
335
|
+
}
|
|
336
|
+
if (method === "DELETE") {
|
|
337
|
+
const manifest = await readManifest(manifestPath);
|
|
338
|
+
const before = manifest.folders.length;
|
|
339
|
+
manifest.folders = manifest.folders.filter((f) => f.id !== id);
|
|
340
|
+
if (manifest.folders.length === before) return json(res, 404, { error: "folder not found" });
|
|
341
|
+
for (const [slideId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[slideId];
|
|
342
|
+
await writeManifest(manifestPath, manifest);
|
|
343
|
+
return json(res, 200, { ok: true });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
172
346
|
next();
|
|
173
347
|
} catch (err) {
|
|
174
348
|
json(res, 500, { error: String(err.message ?? err) });
|
|
@@ -180,12 +354,12 @@ function commentsPlugin(opts) {
|
|
|
180
354
|
|
|
181
355
|
//#endregion
|
|
182
356
|
//#region src/vite/open-slide-plugin.ts
|
|
183
|
-
const
|
|
357
|
+
const SLIDES_VMOD = "virtual:open-slide/slides";
|
|
184
358
|
const CONFIG_VMOD = "virtual:open-slide/config";
|
|
185
359
|
function resolved(id) {
|
|
186
360
|
return `\0${id}`;
|
|
187
361
|
}
|
|
188
|
-
async function
|
|
362
|
+
async function findSlides(userCwd, slidesDir) {
|
|
189
363
|
const abs = path.resolve(userCwd, slidesDir);
|
|
190
364
|
if (!existsSync(abs)) return [];
|
|
191
365
|
const hits = await fg("*/index.{tsx,jsx,ts,js}", {
|
|
@@ -199,7 +373,7 @@ function toId(absFile, slidesRoot) {
|
|
|
199
373
|
const rel = path.relative(slidesRoot, absFile);
|
|
200
374
|
return rel.split(path.sep)[0];
|
|
201
375
|
}
|
|
202
|
-
function
|
|
376
|
+
function generateSlidesModule(files, slidesRoot, isDev) {
|
|
203
377
|
const entries = files.map((abs) => {
|
|
204
378
|
const id = toId(abs, slidesRoot);
|
|
205
379
|
const importPath = isDev ? `/@fs${abs}` : abs;
|
|
@@ -210,13 +384,13 @@ function generateDecksModule(files, slidesRoot, isDev) {
|
|
|
210
384
|
});
|
|
211
385
|
const ids = JSON.stringify(entries.map((e) => e.id).sort());
|
|
212
386
|
const cases = entries.map((e) => ` case ${JSON.stringify(e.id)}: return import(${JSON.stringify(e.importPath)});`).join("\n");
|
|
213
|
-
return `// virtual:open-slide/
|
|
214
|
-
export const
|
|
387
|
+
return `// virtual:open-slide/slides — generated
|
|
388
|
+
export const slideIds = ${ids};
|
|
215
389
|
|
|
216
|
-
export async function
|
|
390
|
+
export async function loadSlide(id) {
|
|
217
391
|
switch (id) {
|
|
218
392
|
${cases}
|
|
219
|
-
default: throw new Error('
|
|
393
|
+
default: throw new Error('Slide not found: ' + id);
|
|
220
394
|
}
|
|
221
395
|
}
|
|
222
396
|
`;
|
|
@@ -233,21 +407,21 @@ function openSlidePlugin(opts) {
|
|
|
233
407
|
return { server: { fs: { allow: [userCwd] } } };
|
|
234
408
|
},
|
|
235
409
|
resolveId(id) {
|
|
236
|
-
if (id ===
|
|
410
|
+
if (id === SLIDES_VMOD) return resolved(SLIDES_VMOD);
|
|
237
411
|
if (id === CONFIG_VMOD) return resolved(CONFIG_VMOD);
|
|
238
412
|
return null;
|
|
239
413
|
},
|
|
240
414
|
async load(id) {
|
|
241
|
-
if (id === resolved(
|
|
242
|
-
const files = await
|
|
243
|
-
return
|
|
415
|
+
if (id === resolved(SLIDES_VMOD)) {
|
|
416
|
+
const files = await findSlides(userCwd, slidesDir);
|
|
417
|
+
return generateSlidesModule(files, slidesRoot, isDev);
|
|
244
418
|
}
|
|
245
419
|
if (id === resolved(CONFIG_VMOD)) return `export default ${JSON.stringify(config)};\n`;
|
|
246
420
|
return null;
|
|
247
421
|
},
|
|
248
422
|
configureServer(server) {
|
|
249
423
|
const reload = () => {
|
|
250
|
-
const mod = server.moduleGraph.getModuleById(resolved(
|
|
424
|
+
const mod = server.moduleGraph.getModuleById(resolved(SLIDES_VMOD));
|
|
251
425
|
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
252
426
|
server.ws.send({ type: "full-reload" });
|
|
253
427
|
};
|
|
@@ -302,6 +476,10 @@ async function createViteConfig(opts) {
|
|
|
302
476
|
commentsPlugin({
|
|
303
477
|
userCwd,
|
|
304
478
|
slidesDir
|
|
479
|
+
}),
|
|
480
|
+
foldersPlugin({
|
|
481
|
+
userCwd,
|
|
482
|
+
slidesDir
|
|
305
483
|
})
|
|
306
484
|
],
|
|
307
485
|
resolve: { alias: { "@": APP_ROOT } },
|
|
@@ -313,7 +491,8 @@ async function createViteConfig(opts) {
|
|
|
313
491
|
"lucide-react",
|
|
314
492
|
"clsx",
|
|
315
493
|
"tailwind-merge",
|
|
316
|
-
"class-variance-authority"
|
|
494
|
+
"class-variance-authority",
|
|
495
|
+
"emoji-picker-react"
|
|
317
496
|
]
|
|
318
497
|
},
|
|
319
498
|
server: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { ComponentType } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/app/lib/sdk.d.ts
|
|
4
|
-
type
|
|
5
|
-
type
|
|
4
|
+
type Page = ComponentType;
|
|
5
|
+
type SlideMeta = {
|
|
6
6
|
title?: string;
|
|
7
7
|
theme?: 'light' | 'dark';
|
|
8
8
|
};
|
|
9
|
-
type
|
|
10
|
-
default:
|
|
11
|
-
meta?:
|
|
9
|
+
type SlideModule = {
|
|
10
|
+
default: Page[];
|
|
11
|
+
meta?: SlideMeta;
|
|
12
12
|
};
|
|
13
13
|
declare const CANVAS_WIDTH = 1920;
|
|
14
|
-
declare const CANVAS_HEIGHT = 1080;
|
|
15
|
-
|
|
16
|
-
//#endregion
|
|
17
|
-
export { CANVAS_HEIGHT, CANVAS_WIDTH, DeckMeta, DeckModule, SlidePage };
|
|
14
|
+
declare const CANVAS_HEIGHT = 1080; //#endregion
|
|
15
|
+
export { CANVAS_HEIGHT, CANVAS_WIDTH, Page, SlideMeta, SlideModule };
|
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-slide/core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Runtime and CLI for open-slide — write
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Runtime and CLI for open-slide — write slides in slides/, we handle the rest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"open-slide": "./
|
|
17
|
+
"open-slide": "./bin.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
+
"bin.js",
|
|
20
21
|
"dist",
|
|
21
22
|
"src/app",
|
|
22
23
|
"README.md"
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"@vitejs/plugin-react": "^4.3.3",
|
|
46
47
|
"class-variance-authority": "^0.7.1",
|
|
47
48
|
"clsx": "^2.1.1",
|
|
49
|
+
"emoji-picker-react": "^4.18.0",
|
|
48
50
|
"fast-glob": "^3.3.2",
|
|
49
51
|
"lucide-react": "^1.8.0",
|
|
50
52
|
"radix-ui": "^1.4.3",
|
package/src/app/App.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
2
2
|
import { Home } from './routes/Home';
|
|
3
|
-
import {
|
|
3
|
+
import { Slide } from './routes/Slide';
|
|
4
4
|
|
|
5
5
|
export function App() {
|
|
6
6
|
return (
|
|
7
7
|
<BrowserRouter>
|
|
8
8
|
<Routes>
|
|
9
9
|
<Route path="/" element={<Home />} />
|
|
10
|
-
<Route path="/
|
|
10
|
+
<Route path="/s/:slideId" element={<Slide />} />
|
|
11
11
|
</Routes>
|
|
12
12
|
</BrowserRouter>
|
|
13
13
|
);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Page } from '../lib/sdk';
|
|
3
3
|
import { SlideCanvas } from './SlideCanvas';
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
6
|
-
pages:
|
|
6
|
+
pages: Page[];
|
|
7
7
|
index: number;
|
|
8
8
|
onIndexChange: (index: number) => void;
|
|
9
9
|
onExit: () => void;
|
|
@@ -51,11 +51,11 @@ export function Player({ pages, index, onIndexChange, onExit }: Props) {
|
|
|
51
51
|
return () => window.removeEventListener('keydown', onKey);
|
|
52
52
|
}, [index, pages.length, onIndexChange, onExit]);
|
|
53
53
|
|
|
54
|
-
const
|
|
54
|
+
const PageComp = pages[index];
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
57
|
<div ref={rootRef} className="flex h-screen w-screen items-center justify-center bg-black">
|
|
58
|
-
<SlideCanvas flat>{
|
|
58
|
+
<SlideCanvas flat>{PageComp ? <PageComp /> : null}</SlideCanvas>
|
|
59
59
|
</div>
|
|
60
60
|
);
|
|
61
61
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Page } from '../lib/sdk';
|
|
4
4
|
import { SlideCanvas } from './SlideCanvas';
|
|
5
5
|
import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../lib/sdk';
|
|
6
6
|
|
|
7
7
|
type Props = {
|
|
8
|
-
pages:
|
|
8
|
+
pages: Page[];
|
|
9
9
|
current: number;
|
|
10
10
|
onSelect: (index: number) => void;
|
|
11
11
|
};
|
|
@@ -18,13 +18,13 @@ export function ThumbnailRail({ pages, current, onSelect }: Props) {
|
|
|
18
18
|
return (
|
|
19
19
|
<ScrollArea className="h-full border-r bg-card">
|
|
20
20
|
<aside className="flex flex-col gap-2.5 p-3">
|
|
21
|
-
{pages.map((
|
|
21
|
+
{pages.map((PageComp, i) => {
|
|
22
22
|
const active = i === current;
|
|
23
23
|
return (
|
|
24
24
|
<button
|
|
25
25
|
key={i}
|
|
26
26
|
onClick={() => onSelect(i)}
|
|
27
|
-
aria-label={`Go to
|
|
27
|
+
aria-label={`Go to page ${i + 1}`}
|
|
28
28
|
aria-current={active ? 'true' : undefined}
|
|
29
29
|
className={cn(
|
|
30
30
|
'flex items-center gap-2.5 rounded-lg border-2 border-transparent p-1.5 text-left transition-colors',
|
|
@@ -45,7 +45,7 @@ export function ThumbnailRail({ pages, current, onSelect }: Props) {
|
|
|
45
45
|
style={{ width: THUMB_WIDTH, height: THUMB_HEIGHT }}
|
|
46
46
|
>
|
|
47
47
|
<SlideCanvas scale={THUMB_SCALE} center={false} flat>
|
|
48
|
-
<
|
|
48
|
+
<PageComp />
|
|
49
49
|
</SlideCanvas>
|
|
50
50
|
</div>
|
|
51
51
|
</button>
|
|
@@ -6,7 +6,7 @@ import { useInspector } from './InspectorProvider';
|
|
|
6
6
|
type Highlight = { rect: DOMRect; hit: SlideSourceHit };
|
|
7
7
|
|
|
8
8
|
export function InspectOverlay() {
|
|
9
|
-
const { active,
|
|
9
|
+
const { active, slideId, pending, setPending } = useInspector();
|
|
10
10
|
const overlayRef = useRef<HTMLDivElement>(null);
|
|
11
11
|
const [hover, setHover] = useState<Highlight | null>(null);
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ export function InspectOverlay() {
|
|
|
20
20
|
if (pending) return;
|
|
21
21
|
const el = pickElement(e.clientX, e.clientY);
|
|
22
22
|
if (!el) return setHover(null);
|
|
23
|
-
const hit = findSlideSource(el,
|
|
23
|
+
const hit = findSlideSource(el, slideId);
|
|
24
24
|
if (!hit) return setHover(null);
|
|
25
25
|
setHover({ rect: hit.anchor.getBoundingClientRect(), hit });
|
|
26
26
|
};
|
|
@@ -30,7 +30,7 @@ export function InspectOverlay() {
|
|
|
30
30
|
if (e.target instanceof Element && e.target.closest('[data-inspector-ui]')) return;
|
|
31
31
|
const el = pickElement(e.clientX, e.clientY);
|
|
32
32
|
if (!el) return;
|
|
33
|
-
const hit = findSlideSource(el,
|
|
33
|
+
const hit = findSlideSource(el, slideId);
|
|
34
34
|
if (!hit) return;
|
|
35
35
|
e.preventDefault();
|
|
36
36
|
e.stopPropagation();
|
|
@@ -50,7 +50,7 @@ export function InspectOverlay() {
|
|
|
50
50
|
window.removeEventListener('pointermove', onMove, true);
|
|
51
51
|
window.removeEventListener('click', onClick, true);
|
|
52
52
|
};
|
|
53
|
-
}, [active,
|
|
53
|
+
}, [active, slideId, pending, setPending]);
|
|
54
54
|
|
|
55
55
|
if (!active) return null;
|
|
56
56
|
|
|
@@ -12,7 +12,7 @@ export type PendingTarget = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
type InspectorCtx = {
|
|
15
|
-
|
|
15
|
+
slideId: string;
|
|
16
16
|
active: boolean;
|
|
17
17
|
toggle: () => void;
|
|
18
18
|
comments: SlideComment[];
|
|
@@ -32,10 +32,10 @@ export function useInspector(): InspectorCtx {
|
|
|
32
32
|
return v;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function InspectorProvider({
|
|
35
|
+
export function InspectorProvider({ slideId, children }: { slideId: string; children: ReactNode }) {
|
|
36
36
|
const [active, setActive] = useState(false);
|
|
37
37
|
const [pending, setPending] = useState<PendingTarget | null>(null);
|
|
38
|
-
const { comments, error, refetch, add, remove } = useComments(
|
|
38
|
+
const { comments, error, refetch, add, remove } = useComments(slideId);
|
|
39
39
|
|
|
40
40
|
const toggle = useCallback(() => {
|
|
41
41
|
setActive((a) => {
|
|
@@ -46,7 +46,7 @@ export function InspectorProvider({ deckId, children }: { deckId: string; childr
|
|
|
46
46
|
|
|
47
47
|
const value = useMemo<InspectorCtx>(
|
|
48
48
|
() => ({
|
|
49
|
-
|
|
49
|
+
slideId,
|
|
50
50
|
active,
|
|
51
51
|
toggle,
|
|
52
52
|
comments,
|
|
@@ -57,7 +57,7 @@ export function InspectorProvider({ deckId, children }: { deckId: string; childr
|
|
|
57
57
|
pending,
|
|
58
58
|
setPending,
|
|
59
59
|
}),
|
|
60
|
-
[
|
|
60
|
+
[slideId, active, toggle, comments, error, refetch, add, remove, pending],
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|