@open-slide/core 0.0.8 → 0.0.9
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-CXY2DSzy.js → build-pqF4W1Yi.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-BYTf0qVz.js → config-CtwxMYv9.js} +365 -44
- package/dist/{dev-BxCKugi3.js → dev-CJX97uiy.js} +1 -1
- package/dist/{preview-C1F-rHfx.js → preview-IuLPcL5y.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +3 -1
- package/src/app/App.tsx +2 -0
- package/src/app/components/PdfProgressToast.tsx +23 -0
- package/src/app/components/inspector/CommentWidget.tsx +1 -1
- package/src/app/components/inspector/InspectOverlay.tsx +81 -41
- package/src/app/components/inspector/InspectorPanel.tsx +805 -0
- package/src/app/components/inspector/InspectorProvider.tsx +199 -13
- package/src/app/components/inspector/SaveBar.tsx +77 -0
- package/src/app/components/ui/input.tsx +21 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/select.tsx +190 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +38 -0
- package/src/app/components/ui/textarea.tsx +18 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +55 -0
- package/src/app/lib/export-pdf.ts +197 -0
- package/src/app/lib/inspector/fiber.ts +40 -5
- package/src/app/lib/inspector/useEditor.ts +61 -0
- package/src/app/lib/print-ready.ts +58 -0
- package/src/app/routes/Slide.tsx +47 -3
- package/src/app/components/inspector/CommentPopover.tsx +0 -94
package/dist/cli/bin.js
CHANGED
|
@@ -22,15 +22,15 @@ async function run(argv) {
|
|
|
22
22
|
const program = new Command();
|
|
23
23
|
program.name("open-slide").description("Author slides — we handle the Vite/React stack.").version(version, "-v, --version", "print version").helpOption("-h, --help", "show help").showHelpAfterError(chalk.dim("(run `open-slide --help` for usage)"));
|
|
24
24
|
program.command("dev").description("Start the dev server").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").action(async (flags) => {
|
|
25
|
-
const { dev } = await import("../dev-
|
|
25
|
+
const { dev } = await import("../dev-CJX97uiy.js");
|
|
26
26
|
await dev(flags);
|
|
27
27
|
});
|
|
28
28
|
program.command("build").description("Build a static site").option("--out-dir <dir>", "output directory (defaults to `dist`)").action(async (flags) => {
|
|
29
|
-
const { build } = await import("../build-
|
|
29
|
+
const { build } = await import("../build-pqF4W1Yi.js");
|
|
30
30
|
await build(flags);
|
|
31
31
|
});
|
|
32
32
|
program.command("preview").description("Preview the production build").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").action(async (flags) => {
|
|
33
|
-
const { preview } = await import("../preview-
|
|
33
|
+
const { preview } = await import("../preview-IuLPcL5y.js");
|
|
34
34
|
await preview(flags);
|
|
35
35
|
});
|
|
36
36
|
await program.parseAsync(argv, { from: "user" });
|
|
@@ -9,6 +9,42 @@ import { parse } from "@babel/parser";
|
|
|
9
9
|
import fg from "fast-glob";
|
|
10
10
|
import { loadConfigFromFile } from "vite";
|
|
11
11
|
|
|
12
|
+
//#region src/vite/babel-walk.ts
|
|
13
|
+
const SKIP_KEYS = new Set([
|
|
14
|
+
"loc",
|
|
15
|
+
"start",
|
|
16
|
+
"end",
|
|
17
|
+
"type",
|
|
18
|
+
"extra",
|
|
19
|
+
"leadingComments",
|
|
20
|
+
"trailingComments",
|
|
21
|
+
"innerComments"
|
|
22
|
+
]);
|
|
23
|
+
function walkJsx(ast, visit) {
|
|
24
|
+
let stopped = false;
|
|
25
|
+
const walk = (node) => {
|
|
26
|
+
if (stopped || !node || typeof node !== "object") return;
|
|
27
|
+
if (Array.isArray(node)) {
|
|
28
|
+
for (const c of node) walk(c);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const n = node;
|
|
32
|
+
if (typeof n.type !== "string") return;
|
|
33
|
+
if (n.type === "JSXElement" || n.type === "JSXFragment") {
|
|
34
|
+
if (visit(n) === "stop") {
|
|
35
|
+
stopped = true;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const key of Object.keys(n)) {
|
|
40
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
41
|
+
walk(n[key]);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
walk(ast);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
12
48
|
//#region src/vite/comments-plugin.ts
|
|
13
49
|
const MARKER_RE = /\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
|
|
14
50
|
const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
|
|
@@ -86,42 +122,19 @@ function lineIndent(source, lineNumber) {
|
|
|
86
122
|
const m = source.slice(start, start + 200).match(/^[ \t]*/);
|
|
87
123
|
return m?.[0] ?? "";
|
|
88
124
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Walk the AST, collect every JSXElement/JSXFragment whose location encloses
|
|
91
|
-
* the click point, ordered innermost-first.
|
|
92
|
-
*
|
|
93
|
-
* "Encloses" here is inclusive at the start (so a click on the opening `<`
|
|
94
|
-
* counts as inside) and exclusive at the end. We deliberately don't trust
|
|
95
|
-
* Babel's `_debugSource` line/column to be exact — HMR or upstream transforms
|
|
96
|
-
* can shift it slightly — so we treat the click as a probe and pick the
|
|
97
|
-
* tightest JSX container around it.
|
|
98
|
-
*/
|
|
99
125
|
function findJsxAncestors(ast, line, column) {
|
|
100
126
|
const hits = [];
|
|
101
|
-
|
|
102
|
-
if (!
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const afterStart = line > s.line || line === s.line && column >= s.column;
|
|
113
|
-
const beforeEnd = line < e.line || line === e.line && column < e.column;
|
|
114
|
-
if (afterStart && beforeEnd) hits.push({
|
|
115
|
-
node: n,
|
|
116
|
-
size: n.end - n.start
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
for (const key of Object.keys(n)) {
|
|
120
|
-
if (key === "loc" || key === "start" || key === "end" || key === "type" || key === "extra" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
|
|
121
|
-
walk(n[key]);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
walk(ast);
|
|
127
|
+
walkJsx(ast, (n) => {
|
|
128
|
+
if (!n.loc) return;
|
|
129
|
+
const s = n.loc.start;
|
|
130
|
+
const e = n.loc.end;
|
|
131
|
+
const afterStart = line > s.line || line === s.line && column >= s.column;
|
|
132
|
+
const beforeEnd = line < e.line || line === e.line && column < e.column;
|
|
133
|
+
if (afterStart && beforeEnd) hits.push({
|
|
134
|
+
node: n,
|
|
135
|
+
size: n.end - n.start
|
|
136
|
+
});
|
|
137
|
+
});
|
|
125
138
|
hits.sort((a, b) => a.size - b.size);
|
|
126
139
|
return hits.map((h) => h.node);
|
|
127
140
|
}
|
|
@@ -146,16 +159,6 @@ function planInsertion(source, target) {
|
|
|
146
159
|
}
|
|
147
160
|
return null;
|
|
148
161
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Resolve a click on the slide page (line/col from React fiber's
|
|
151
|
-
* `_debugSource`) to an in-source offset where we can safely splice a
|
|
152
|
-
* `@slide-comment` marker.
|
|
153
|
-
*
|
|
154
|
-
* Strategy: parse the file, find every JSX container around the click, and
|
|
155
|
-
* walk innermost → outermost looking for the first one we can insert *inside*
|
|
156
|
-
* (i.e. not self-closing). Self-closing elements like `<img/>` get hoisted to
|
|
157
|
-
* their nearest non-self-closing ancestor.
|
|
158
|
-
*/
|
|
159
162
|
function findInsertion(source, line, column) {
|
|
160
163
|
let ast;
|
|
161
164
|
try {
|
|
@@ -181,6 +184,181 @@ function offsetToLine(source, offset) {
|
|
|
181
184
|
for (let i = 0; i < offset && i < source.length; i++) if (source[i] === "\n") line++;
|
|
182
185
|
return line;
|
|
183
186
|
}
|
|
187
|
+
function parseSource(source) {
|
|
188
|
+
try {
|
|
189
|
+
return parse(source, {
|
|
190
|
+
sourceType: "module",
|
|
191
|
+
plugins: ["typescript", "jsx"],
|
|
192
|
+
errorRecovery: true
|
|
193
|
+
});
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function findInnermostJsxElement(source, line, column) {
|
|
199
|
+
const ast = parseSource(source);
|
|
200
|
+
if (!ast) return null;
|
|
201
|
+
const exact = findJsxByStart(ast, line, column);
|
|
202
|
+
if (exact) return exact;
|
|
203
|
+
const ancestors = findJsxAncestors(ast, line, column);
|
|
204
|
+
for (const n of ancestors) if (n.type === "JSXElement") return n;
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function findJsxByStart(ast, line, column) {
|
|
208
|
+
let hit = null;
|
|
209
|
+
walkJsx(ast, (n) => {
|
|
210
|
+
if (n.type !== "JSXElement" || !n.loc) return;
|
|
211
|
+
const s = n.loc.start;
|
|
212
|
+
if (s.line === line && s.column === column) {
|
|
213
|
+
hit = n;
|
|
214
|
+
return "stop";
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return hit;
|
|
218
|
+
}
|
|
219
|
+
function jsString(s) {
|
|
220
|
+
return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
|
|
221
|
+
}
|
|
222
|
+
function findStyleAttr(opening) {
|
|
223
|
+
const attrs = opening.attributes ?? [];
|
|
224
|
+
for (const attr of attrs) {
|
|
225
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
226
|
+
const name = attr.name;
|
|
227
|
+
if (name?.type === "JSXIdentifier" && name.name === "style") return attr;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
function buildStyleSplice(source, element, ops) {
|
|
232
|
+
const opening = element.openingElement;
|
|
233
|
+
if (!opening) return { error: "no opening element" };
|
|
234
|
+
const existing = findStyleAttr(opening);
|
|
235
|
+
const style = new Map();
|
|
236
|
+
if (existing) {
|
|
237
|
+
const value = existing.value;
|
|
238
|
+
if (!value || value.type !== "JSXExpressionContainer") return { error: "style attribute has unsupported form" };
|
|
239
|
+
const expr = value.expression;
|
|
240
|
+
if (expr.type !== "ObjectExpression") return { error: "style is not a literal object" };
|
|
241
|
+
const properties = expr.properties;
|
|
242
|
+
for (const prop of properties) {
|
|
243
|
+
if (prop.type !== "ObjectProperty") return { error: "style contains spread or method" };
|
|
244
|
+
const p = prop;
|
|
245
|
+
if (p.computed) return { error: "style has computed key" };
|
|
246
|
+
let keyName = null;
|
|
247
|
+
if (p.key.type === "Identifier" && p.key.name) keyName = p.key.name;
|
|
248
|
+
else if (p.key.type === "StringLiteral" && typeof p.key.value === "string") keyName = p.key.value;
|
|
249
|
+
if (!keyName) return { error: "style has unsupported key" };
|
|
250
|
+
style.set(keyName, source.slice(p.value.start, p.value.end));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const op of ops) if (op.value === null) style.delete(op.key);
|
|
254
|
+
else style.set(op.key, jsString(op.value));
|
|
255
|
+
if (style.size === 0) {
|
|
256
|
+
if (!existing) return null;
|
|
257
|
+
let from = existing.start;
|
|
258
|
+
if (from > 0 && source[from - 1] === " ") from -= 1;
|
|
259
|
+
return {
|
|
260
|
+
from,
|
|
261
|
+
to: existing.end,
|
|
262
|
+
text: ""
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const propsText = Array.from(style.entries()).map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
266
|
+
const newAttr = `style={{ ${propsText} }}`;
|
|
267
|
+
if (existing) return {
|
|
268
|
+
from: existing.start,
|
|
269
|
+
to: existing.end,
|
|
270
|
+
text: newAttr
|
|
271
|
+
};
|
|
272
|
+
const name = opening.name;
|
|
273
|
+
return {
|
|
274
|
+
from: name.end,
|
|
275
|
+
to: name.end,
|
|
276
|
+
text: ` ${newAttr}`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function formatJsxText(value) {
|
|
280
|
+
if (/[{}<>]/.test(value) || /^\s|\s$/.test(value) || value === "") return `{${jsString(value)}}`;
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
function buildTextSplice(element, value) {
|
|
284
|
+
const children = element.children ?? [];
|
|
285
|
+
if (children.length === 0) return { error: "element has no children to edit" };
|
|
286
|
+
const meaningful = children.filter((c) => {
|
|
287
|
+
if (c.type === "JSXText") {
|
|
288
|
+
const v = c.value;
|
|
289
|
+
return v.trim() !== "";
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
if (meaningful.length !== 1) return { error: "element has complex children" };
|
|
294
|
+
const child = meaningful[0];
|
|
295
|
+
if (child.type === "JSXText") {
|
|
296
|
+
const first = children[0];
|
|
297
|
+
const last = children[children.length - 1];
|
|
298
|
+
return {
|
|
299
|
+
from: first.start,
|
|
300
|
+
to: last.end,
|
|
301
|
+
text: formatJsxText(value)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (child.type === "JSXExpressionContainer") {
|
|
305
|
+
const expr = child.expression;
|
|
306
|
+
if (expr.type === "StringLiteral" || expr.type === "NumericLiteral") return {
|
|
307
|
+
from: child.start,
|
|
308
|
+
to: child.end,
|
|
309
|
+
text: `{${jsString(value)}}`
|
|
310
|
+
};
|
|
311
|
+
return { error: "element has dynamic expression child" };
|
|
312
|
+
}
|
|
313
|
+
return { error: "element has complex children" };
|
|
314
|
+
}
|
|
315
|
+
function applyEdit(source, line, column, ops) {
|
|
316
|
+
if (ops.length === 0) return {
|
|
317
|
+
ok: true,
|
|
318
|
+
source
|
|
319
|
+
};
|
|
320
|
+
const element = findInnermostJsxElement(source, line, column);
|
|
321
|
+
if (!element) return {
|
|
322
|
+
ok: false,
|
|
323
|
+
status: 422,
|
|
324
|
+
error: "no JSX element at location"
|
|
325
|
+
};
|
|
326
|
+
const splices = [];
|
|
327
|
+
const styleOps = ops.flatMap((op) => op.kind === "set-style" ? [{
|
|
328
|
+
key: op.key,
|
|
329
|
+
value: op.value
|
|
330
|
+
}] : []);
|
|
331
|
+
if (styleOps.length > 0) {
|
|
332
|
+
const result = buildStyleSplice(source, element, styleOps);
|
|
333
|
+
if (result && "error" in result) return {
|
|
334
|
+
ok: false,
|
|
335
|
+
status: 422,
|
|
336
|
+
error: result.error
|
|
337
|
+
};
|
|
338
|
+
if (result) splices.push(result);
|
|
339
|
+
}
|
|
340
|
+
for (const op of ops) {
|
|
341
|
+
if (op.kind !== "set-text") continue;
|
|
342
|
+
const result = buildTextSplice(element, op.value);
|
|
343
|
+
if ("error" in result) return {
|
|
344
|
+
ok: false,
|
|
345
|
+
status: 422,
|
|
346
|
+
error: result.error
|
|
347
|
+
};
|
|
348
|
+
splices.push(result);
|
|
349
|
+
}
|
|
350
|
+
if (splices.length === 0) return {
|
|
351
|
+
ok: true,
|
|
352
|
+
source
|
|
353
|
+
};
|
|
354
|
+
splices.sort((a, b) => b.from - a.from);
|
|
355
|
+
let next = source;
|
|
356
|
+
for (const sp of splices) next = next.slice(0, sp.from) + sp.text + next.slice(sp.to);
|
|
357
|
+
return {
|
|
358
|
+
ok: true,
|
|
359
|
+
source: next
|
|
360
|
+
};
|
|
361
|
+
}
|
|
184
362
|
function commentsPlugin(opts) {
|
|
185
363
|
const userCwd = opts.userCwd;
|
|
186
364
|
const slidesDir = opts.slidesDir ?? "slides";
|
|
@@ -188,6 +366,77 @@ function commentsPlugin(opts) {
|
|
|
188
366
|
name: "open-slide:comments",
|
|
189
367
|
apply: "serve",
|
|
190
368
|
configureServer(server) {
|
|
369
|
+
server.middlewares.use("/__edit", async (req, res, next) => {
|
|
370
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
371
|
+
const method = req.method ?? "GET";
|
|
372
|
+
if (method !== "POST") return next();
|
|
373
|
+
try {
|
|
374
|
+
if (url.pathname === "/") {
|
|
375
|
+
const body = await readBody$1(req);
|
|
376
|
+
const slideId = body.slideId ?? "";
|
|
377
|
+
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
378
|
+
if (!file) return json$1(res, 400, { error: "invalid slideId" });
|
|
379
|
+
if (!body.line || body.line < 1) return json$1(res, 400, { error: "invalid line" });
|
|
380
|
+
if (!Array.isArray(body.ops)) return json$1(res, 400, { error: "missing ops" });
|
|
381
|
+
let source;
|
|
382
|
+
try {
|
|
383
|
+
source = await fs.readFile(file, "utf8");
|
|
384
|
+
} catch {
|
|
385
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
386
|
+
}
|
|
387
|
+
const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
|
|
388
|
+
if (!result.ok) return json$1(res, result.status, { error: result.error });
|
|
389
|
+
const changed = result.source !== source;
|
|
390
|
+
if (changed) await fs.writeFile(file, result.source, "utf8");
|
|
391
|
+
return json$1(res, 200, {
|
|
392
|
+
ok: true,
|
|
393
|
+
changed
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (url.pathname === "/batch") {
|
|
397
|
+
const body = await readBody$1(req);
|
|
398
|
+
const slideId = body.slideId ?? "";
|
|
399
|
+
const file = resolveSlidePath(userCwd, slidesDir, slideId);
|
|
400
|
+
if (!file) return json$1(res, 400, { error: "invalid slideId" });
|
|
401
|
+
if (!Array.isArray(body.edits)) return json$1(res, 400, { error: "missing edits" });
|
|
402
|
+
let source;
|
|
403
|
+
try {
|
|
404
|
+
source = await fs.readFile(file, "utf8");
|
|
405
|
+
} catch {
|
|
406
|
+
return json$1(res, 404, { error: "slide not found" });
|
|
407
|
+
}
|
|
408
|
+
const original = source;
|
|
409
|
+
const results = [];
|
|
410
|
+
for (const edit of body.edits) {
|
|
411
|
+
if (!edit.line || edit.line < 1 || !Array.isArray(edit.ops)) {
|
|
412
|
+
results.push({
|
|
413
|
+
ok: false,
|
|
414
|
+
error: "invalid edit"
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const r = applyEdit(source, edit.line, edit.column ?? 0, edit.ops);
|
|
419
|
+
if (r.ok) {
|
|
420
|
+
source = r.source;
|
|
421
|
+
results.push({ ok: true });
|
|
422
|
+
} else results.push({
|
|
423
|
+
ok: false,
|
|
424
|
+
error: r.error
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const changed = source !== original;
|
|
428
|
+
if (changed) await fs.writeFile(file, source, "utf8");
|
|
429
|
+
return json$1(res, 200, {
|
|
430
|
+
ok: true,
|
|
431
|
+
changed,
|
|
432
|
+
results
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return next();
|
|
436
|
+
} catch (err) {
|
|
437
|
+
json$1(res, 500, { error: String(err.message ?? err) });
|
|
438
|
+
}
|
|
439
|
+
});
|
|
191
440
|
server.middlewares.use("/__comments", async (req, res, next) => {
|
|
192
441
|
const url = new URL(req.url ?? "/", "http://local");
|
|
193
442
|
const method = req.method ?? "GET";
|
|
@@ -564,6 +813,74 @@ function filesPlugin(opts) {
|
|
|
564
813
|
};
|
|
565
814
|
}
|
|
566
815
|
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/vite/loc-tags-plugin.ts
|
|
818
|
+
function isHostJsxName(name) {
|
|
819
|
+
if (!name || typeof name !== "object") return false;
|
|
820
|
+
const n = name;
|
|
821
|
+
return n.type === "JSXIdentifier" && typeof n.name === "string" && /^[a-z]/.test(n.name);
|
|
822
|
+
}
|
|
823
|
+
function alreadyTagged(opening) {
|
|
824
|
+
const attrs = opening.attributes ?? [];
|
|
825
|
+
for (const attr of attrs) {
|
|
826
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
827
|
+
const name = attr.name;
|
|
828
|
+
if (name?.type === "JSXIdentifier" && name.name === "data-slide-loc") return true;
|
|
829
|
+
}
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
function injectLocTags(code) {
|
|
833
|
+
let ast;
|
|
834
|
+
try {
|
|
835
|
+
ast = parse(code, {
|
|
836
|
+
sourceType: "module",
|
|
837
|
+
plugins: ["typescript", "jsx"],
|
|
838
|
+
errorRecovery: true
|
|
839
|
+
});
|
|
840
|
+
} catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
const insertions = [];
|
|
844
|
+
walkJsx(ast, (node) => {
|
|
845
|
+
if (node.type !== "JSXElement") return;
|
|
846
|
+
const opening = node.openingElement;
|
|
847
|
+
if (!opening) return;
|
|
848
|
+
const name = opening.name;
|
|
849
|
+
if (!isHostJsxName(name)) return;
|
|
850
|
+
if (alreadyTagged(opening)) return;
|
|
851
|
+
const loc = node.loc;
|
|
852
|
+
if (!loc) return;
|
|
853
|
+
insertions.push({
|
|
854
|
+
offset: name.end,
|
|
855
|
+
text: ` data-slide-loc="${loc.start.line}:${loc.start.column}"`
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
if (insertions.length === 0) return null;
|
|
859
|
+
insertions.sort((a, b) => b.offset - a.offset);
|
|
860
|
+
let next = code;
|
|
861
|
+
for (const ins of insertions) next = next.slice(0, ins.offset) + ins.text + next.slice(ins.offset);
|
|
862
|
+
return next;
|
|
863
|
+
}
|
|
864
|
+
function locTagsPlugin(opts) {
|
|
865
|
+
const slidesRoot = path.resolve(opts.userCwd, opts.slidesDir ?? "slides");
|
|
866
|
+
return {
|
|
867
|
+
name: "open-slide:loc-tags",
|
|
868
|
+
apply: "serve",
|
|
869
|
+
enforce: "pre",
|
|
870
|
+
transform(code, id) {
|
|
871
|
+
const filePath = id.split("?")[0];
|
|
872
|
+
if (!filePath.startsWith(slidesRoot + path.sep)) return null;
|
|
873
|
+
if (!filePath.endsWith(`${path.sep}index.tsx`)) return null;
|
|
874
|
+
const next = injectLocTags(code);
|
|
875
|
+
if (next === null) return null;
|
|
876
|
+
return {
|
|
877
|
+
code: next,
|
|
878
|
+
map: null
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
567
884
|
//#endregion
|
|
568
885
|
//#region src/vite/open-slide-plugin.ts
|
|
569
886
|
const CONFIG_FILE = "open-slide.config.ts";
|
|
@@ -732,6 +1049,10 @@ async function createViteConfig(opts) {
|
|
|
732
1049
|
root: APP_ROOT,
|
|
733
1050
|
configFile: false,
|
|
734
1051
|
plugins: [
|
|
1052
|
+
locTagsPlugin({
|
|
1053
|
+
userCwd,
|
|
1054
|
+
slidesDir
|
|
1055
|
+
}),
|
|
735
1056
|
react(),
|
|
736
1057
|
tailwindcss(),
|
|
737
1058
|
openSlidePlugin({
|
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-slide/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Runtime and CLI for open-slide — write slides in slides/, we handle the rest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -53,11 +53,13 @@
|
|
|
53
53
|
"fast-glob": "^3.3.2",
|
|
54
54
|
"fflate": "^0.8.2",
|
|
55
55
|
"lucide-react": "^1.8.0",
|
|
56
|
+
"next-themes": "^0.4.6",
|
|
56
57
|
"radix-ui": "^1.4.3",
|
|
57
58
|
"react": "^18.3.1",
|
|
58
59
|
"react-dom": "^18.3.1",
|
|
59
60
|
"react-router-dom": "^6.26.2",
|
|
60
61
|
"shadcn": "^4.3.0",
|
|
62
|
+
"sonner": "^2.0.7",
|
|
61
63
|
"tailwind-merge": "^3.5.0",
|
|
62
64
|
"tailwindcss": "^4.2.2",
|
|
63
65
|
"tw-animate-css": "^1.4.0",
|
package/src/app/App.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
2
2
|
import config from 'virtual:open-slide/config';
|
|
3
|
+
import { Toaster } from './components/ui/sonner';
|
|
3
4
|
import { Home } from './routes/Home';
|
|
4
5
|
import { Slide } from './routes/Slide';
|
|
5
6
|
|
|
@@ -11,6 +12,7 @@ export function App() {
|
|
|
11
12
|
<Route path="/s/:slideId" element={<Slide />} />
|
|
12
13
|
<Route path="*" element={<NotFound />} />
|
|
13
14
|
</Routes>
|
|
15
|
+
<Toaster />
|
|
14
16
|
</BrowserRouter>
|
|
15
17
|
);
|
|
16
18
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Loader2 } from 'lucide-react';
|
|
2
|
+
import type { PdfExportProgress } from '../lib/export-pdf';
|
|
3
|
+
import { Progress } from './ui/progress';
|
|
4
|
+
|
|
5
|
+
export function PdfProgressToast({ progress }: { progress: PdfExportProgress }) {
|
|
6
|
+
const text =
|
|
7
|
+
progress.phase === 'processing'
|
|
8
|
+
? `Processing slide ${progress.current} / ${progress.total}`
|
|
9
|
+
: progress.phase === 'printing'
|
|
10
|
+
? 'Opening print dialog…'
|
|
11
|
+
: 'Done';
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex w-80 items-start gap-3 rounded-md border bg-popover px-4 py-3 text-popover-foreground shadow-lg">
|
|
15
|
+
<Loader2 className="mt-0.5 size-4 shrink-0 animate-spin text-primary" />
|
|
16
|
+
<div className="min-w-0 flex-1">
|
|
17
|
+
<p className="text-sm font-medium">Exporting PDF</p>
|
|
18
|
+
<p className="truncate text-xs text-muted-foreground">{text}</p>
|
|
19
|
+
<Progress value={Math.round(progress.percent)} className="mt-2 h-1.5" />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -8,7 +8,7 @@ export function CommentWidget() {
|
|
|
8
8
|
const count = comments.length;
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
|
-
<div data-inspector-ui className="
|
|
11
|
+
<div data-inspector-ui className="absolute right-4 bottom-4 z-20 flex flex-col items-end gap-2">
|
|
12
12
|
{open && (
|
|
13
13
|
<div className="w-80 rounded-md border bg-card shadow-xl animate-in fade-in-0 slide-in-from-bottom-2 duration-200">
|
|
14
14
|
<div className="flex items-center justify-between border-b px-3 py-2">
|