@open-slide/core 1.1.0 → 1.3.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.
Files changed (55) hide show
  1. package/dist/{build-DSqSio-T.js → build-_276DMmJ.js} +2 -2
  2. package/dist/cli/bin.js +5 -5
  3. package/dist/{config-KdiYeWtK.js → config-BAwKWNtW.js} +888 -229
  4. package/dist/{config-C7vMYzFD.d.ts → config-D9cZ1A0X.d.ts} +2 -1
  5. package/dist/{dev-B_GVbr11.js → dev-BoqeVXVq.js} +2 -2
  6. package/dist/en-CDKzoZvf.js +351 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +229 -39
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +166 -326
  11. package/dist/{preview-D_mxhj7w.js → preview-BLPxspc9.js} +2 -2
  12. package/dist/sync-j9_QPovT.js +3 -0
  13. package/dist/{types-DYgVpIGo.d.ts → types-JYG1cmwC.d.ts} +59 -5
  14. package/dist/vite/index.d.ts +2 -2
  15. package/dist/vite/index.js +2 -2
  16. package/package.json +9 -1
  17. package/skills/create-slide/SKILL.md +1 -1
  18. package/skills/create-theme/SKILL.md +60 -12
  19. package/skills/current-slide/SKILL.md +110 -0
  20. package/skills/slide-authoring/SKILL.md +59 -1
  21. package/src/app/app.tsx +11 -1
  22. package/src/app/components/asset-view.tsx +1 -13
  23. package/src/app/components/image-placeholder.tsx +123 -1
  24. package/src/app/components/inspector/image-crop-dialog.tsx +64 -20
  25. package/src/app/components/inspector/inspector-panel.tsx +163 -19
  26. package/src/app/components/inspector/inspector-provider.tsx +60 -7
  27. package/src/app/components/notes-drawer.tsx +117 -0
  28. package/src/app/components/player.tsx +11 -7
  29. package/src/app/components/present/overview-grid.tsx +2 -2
  30. package/src/app/components/sidebar/folder-item.tsx +16 -5
  31. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  32. package/src/app/components/sidebar/sidebar.tsx +10 -0
  33. package/src/app/components/themes/theme-detail.tsx +300 -0
  34. package/src/app/components/themes/themes-gallery.tsx +146 -0
  35. package/src/app/components/thumbnail-rail.tsx +136 -29
  36. package/src/app/components/ui/context-menu.tsx +237 -0
  37. package/src/app/lib/assets.ts +55 -2
  38. package/src/app/lib/inspector/use-notes.ts +134 -0
  39. package/src/app/lib/sdk.ts +1 -0
  40. package/src/app/lib/slides.ts +10 -1
  41. package/src/app/lib/themes.ts +22 -0
  42. package/src/app/lib/use-agent-socket.ts +18 -0
  43. package/src/app/routes/home-shell.tsx +173 -0
  44. package/src/app/routes/home.tsx +108 -204
  45. package/src/app/routes/slide.tsx +333 -68
  46. package/src/app/routes/themes.tsx +34 -0
  47. package/src/app/virtual.d.ts +20 -0
  48. package/src/locale/en.ts +61 -7
  49. package/src/locale/ja.ts +62 -7
  50. package/src/locale/types.ts +62 -5
  51. package/src/locale/zh-cn.ts +61 -7
  52. package/src/locale/zh-tw.ts +61 -7
  53. package/dist/sync-B4eLo2H6.js +0 -3
  54. /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
  55. /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { defaultDesign } from "./design-C13iz9_4.js";
1
+ import { defaultDesign } from "./design-cpzS8aud.js";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -7,11 +7,12 @@ import { existsSync } from "node:fs";
7
7
  import tailwindcss from "@tailwindcss/vite";
8
8
  import react from "@vitejs/plugin-react";
9
9
  import { parse } from "@babel/parser";
10
+ import * as t$2 from "@babel/types";
10
11
  import * as t$1 from "@babel/types";
11
12
  import * as t from "@babel/types";
12
13
  import { isJSXElement, isJSXFragment } from "@babel/types";
13
14
  import fg from "fast-glob";
14
- import { loadConfigFromFile } from "vite";
15
+ import { loadConfigFromFile, normalizePath } from "vite";
15
16
 
16
17
  //#region src/vite/babel-walk.ts
17
18
  const SKIP_KEYS = new Set([
@@ -57,7 +58,7 @@ function walkAll(ast, visit) {
57
58
  //#endregion
58
59
  //#region src/vite/comments-plugin.ts
59
60
  const MARKER_RE = /\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
60
- const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
61
+ const SLIDE_ID_RE$4 = /^[a-z0-9_-]+$/i;
61
62
  function b64urlEncode(s) {
62
63
  return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
63
64
  }
@@ -65,7 +66,7 @@ function b64urlDecode(s) {
65
66
  const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
66
67
  return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
67
68
  }
68
- async function readBody$2(req) {
69
+ async function readBody$3(req) {
69
70
  return await new Promise((resolve, reject) => {
70
71
  const chunks = [];
71
72
  req.on("data", (c) => chunks.push(c));
@@ -81,13 +82,13 @@ async function readBody$2(req) {
81
82
  req.on("error", reject);
82
83
  });
83
84
  }
84
- function json$2(res, status, body) {
85
+ function json$3(res, status, body) {
85
86
  res.statusCode = status;
86
87
  res.setHeader("content-type", "application/json");
87
88
  res.end(JSON.stringify(body));
88
89
  }
89
- function resolveSlidePath$1(userCwd, slidesDir, slideId) {
90
- if (!SLIDE_ID_RE$2.test(slideId)) return null;
90
+ function resolveSlidePath$2(userCwd, slidesDir, slideId) {
91
+ if (!SLIDE_ID_RE$4.test(slideId)) return null;
91
92
  const slidesRoot = path.resolve(userCwd, slidesDir);
92
93
  const full = path.resolve(slidesRoot, slideId, "index.tsx");
93
94
  if (!full.startsWith(slidesRoot + path.sep)) return null;
@@ -135,7 +136,7 @@ function lineIndent(source, lineNumber) {
135
136
  function findJsxAncestors(ast, line, column) {
136
137
  const hits = [];
137
138
  walkJsx(ast, (n) => {
138
- if (!n.loc || !t$1.isJSXElement(n) && !t$1.isJSXFragment(n)) return;
139
+ if (!n.loc || !t$2.isJSXElement(n) && !t$2.isJSXFragment(n)) return;
139
140
  const s = n.loc.start;
140
141
  const e = n.loc.end;
141
142
  const afterStart = line > s.line || line === s.line && column >= s.column;
@@ -149,7 +150,7 @@ function findJsxAncestors(ast, line, column) {
149
150
  return hits.map((h) => h.node);
150
151
  }
151
152
  function planInsertion(source, target) {
152
- if (t$1.isJSXFragment(target)) {
153
+ if (t$2.isJSXFragment(target)) {
153
154
  const opening = target.openingFragment;
154
155
  const startLine = target.loc?.start.line ?? 1;
155
156
  return {
@@ -157,7 +158,7 @@ function planInsertion(source, target) {
157
158
  indent: `${lineIndent(source, startLine)} `
158
159
  };
159
160
  }
160
- if (t$1.isJSXElement(target)) {
161
+ if (t$2.isJSXElement(target)) {
161
162
  const opening = target.openingElement;
162
163
  if (opening.selfClosing) return null;
163
164
  const startLine = target.loc?.start.line ?? 1;
@@ -169,7 +170,7 @@ function planInsertion(source, target) {
169
170
  return null;
170
171
  }
171
172
  function findInsertion(source, line, column) {
172
- const ast = parseSource$1(source);
173
+ const ast = parseSource$2(source);
173
174
  if (!ast) return null;
174
175
  const col = column ?? 0;
175
176
  const ancestors = findJsxAncestors(ast, line, col);
@@ -184,7 +185,7 @@ function offsetToLine(source, offset) {
184
185
  for (let i = 0; i < offset && i < source.length; i++) if (source[i] === "\n") line++;
185
186
  return line;
186
187
  }
187
- function parseSource$1(source) {
188
+ function parseSource$2(source) {
188
189
  try {
189
190
  return parse(source, {
190
191
  sourceType: "module",
@@ -198,13 +199,13 @@ function parseSource$1(source) {
198
199
  function findInnermostJsxElement(ast, line, column) {
199
200
  const exact = findJsxByStart(ast, line, column);
200
201
  if (exact) return exact;
201
- for (const n of findJsxAncestors(ast, line, column)) if (t$1.isJSXElement(n)) return n;
202
+ for (const n of findJsxAncestors(ast, line, column)) if (t$2.isJSXElement(n)) return n;
202
203
  return null;
203
204
  }
204
205
  function findJsxByStart(ast, line, column) {
205
206
  let hit = null;
206
207
  walkJsx(ast, (n) => {
207
- if (!t$1.isJSXElement(n) || !n.loc) return;
208
+ if (!t$2.isJSXElement(n) || !n.loc) return;
208
209
  const s = n.loc.start;
209
210
  if (s.line === line && s.column === column) {
210
211
  hit = n;
@@ -217,10 +218,10 @@ function jsString$1(s) {
217
218
  return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
218
219
  }
219
220
  function jsxAttrName(attr) {
220
- return t$1.isJSXIdentifier(attr.name) ? attr.name.name : null;
221
+ return t$2.isJSXIdentifier(attr.name) ? attr.name.name : null;
221
222
  }
222
223
  function findJsxAttr(opening, name) {
223
- for (const attr of opening.attributes) if (t$1.isJSXAttribute(attr) && jsxAttrName(attr) === name) return attr;
224
+ for (const attr of opening.attributes) if (t$2.isJSXAttribute(attr) && jsxAttrName(attr) === name) return attr;
224
225
  return null;
225
226
  }
226
227
  function buildStyleSplice(source, element, ops) {
@@ -229,15 +230,15 @@ function buildStyleSplice(source, element, ops) {
229
230
  const style = new Map();
230
231
  if (existing) {
231
232
  const value = existing.value;
232
- if (!value || !t$1.isJSXExpressionContainer(value)) return { error: "style attribute has unsupported form" };
233
+ if (!value || !t$2.isJSXExpressionContainer(value)) return { error: "style attribute has unsupported form" };
233
234
  const expr = value.expression;
234
- if (!t$1.isObjectExpression(expr)) return { error: "style is not a literal object" };
235
+ if (!t$2.isObjectExpression(expr)) return { error: "style is not a literal object" };
235
236
  for (const prop of expr.properties) {
236
- if (!t$1.isObjectProperty(prop)) return { error: "style contains spread or method" };
237
+ if (!t$2.isObjectProperty(prop)) return { error: "style contains spread or method" };
237
238
  if (prop.computed) return { error: "style has computed key" };
238
239
  let keyName = null;
239
- if (t$1.isIdentifier(prop.key)) keyName = prop.key.name;
240
- else if (t$1.isStringLiteral(prop.key)) keyName = prop.key.value;
240
+ if (t$2.isIdentifier(prop.key)) keyName = prop.key.name;
241
+ else if (t$2.isStringLiteral(prop.key)) keyName = prop.key.value;
241
242
  if (!keyName) return { error: "style has unsupported key" };
242
243
  const v = prop.value;
243
244
  if (typeof v.start !== "number" || typeof v.end !== "number") return { error: "style value missing source range" };
@@ -275,7 +276,7 @@ function formatJsxText(value) {
275
276
  }
276
277
  function meaningfulChildren(parent) {
277
278
  return parent.children.filter((c) => {
278
- if (t$1.isJSXText(c)) return c.value.trim() !== "";
279
+ if (t$2.isJSXText(c)) return c.value.trim() !== "";
279
280
  return true;
280
281
  });
281
282
  }
@@ -291,7 +292,7 @@ function wrapSplice(parent, text) {
291
292
  function collectTextCandidates(element, out) {
292
293
  const meaningful = meaningfulChildren(element);
293
294
  const isSole = meaningful.length === 1;
294
- for (const child of meaningful) if (t$1.isJSXText(child)) {
295
+ for (const child of meaningful) if (t$2.isJSXText(child)) {
295
296
  const current = child.value.trim();
296
297
  if (!current) continue;
297
298
  out.push({
@@ -302,9 +303,9 @@ function collectTextCandidates(element, out) {
302
303
  text: formatJsxText(v)
303
304
  }
304
305
  });
305
- } else if (t$1.isJSXExpressionContainer(child)) {
306
+ } else if (t$2.isJSXExpressionContainer(child)) {
306
307
  const expr = child.expression;
307
- if (t$1.isStringLiteral(expr) || t$1.isNumericLiteral(expr)) {
308
+ if (t$2.isStringLiteral(expr) || t$2.isNumericLiteral(expr)) {
308
309
  const current = String(expr.value);
309
310
  out.push({
310
311
  current,
@@ -315,14 +316,14 @@ function collectTextCandidates(element, out) {
315
316
  }
316
317
  });
317
318
  }
318
- } else if (t$1.isJSXElement(child) || t$1.isJSXFragment(child)) collectTextCandidates(child, out);
319
+ } else if (t$2.isJSXElement(child) || t$2.isJSXFragment(child)) collectTextCandidates(child, out);
319
320
  }
320
321
  function propPassthroughName(element) {
321
322
  const meaningful = meaningfulChildren(element);
322
323
  if (meaningful.length !== 1) return null;
323
324
  const child = meaningful[0];
324
- if (!t$1.isJSXExpressionContainer(child)) return null;
325
- return t$1.isIdentifier(child.expression) ? child.expression.name : null;
325
+ if (!t$2.isJSXExpressionContainer(child)) return null;
326
+ return t$2.isIdentifier(child.expression) ? child.expression.name : null;
326
327
  }
327
328
  function findEnclosingComponent(ast, target) {
328
329
  let best = null;
@@ -344,17 +345,17 @@ function findEnclosingComponent(ast, target) {
344
345
  }
345
346
  };
346
347
  const visitDecl = (decl) => {
347
- if (t$1.isFunctionDeclaration(decl) && decl.id) consider(decl.id.name, decl);
348
- else if (t$1.isVariableDeclaration(decl)) for (const d of decl.declarations) {
349
- if (!t$1.isVariableDeclarator(d) || !t$1.isIdentifier(d.id) || !d.init) continue;
350
- if (t$1.isArrowFunctionExpression(d.init) || t$1.isFunctionExpression(d.init)) consider(d.id.name, d.init);
348
+ if (t$2.isFunctionDeclaration(decl) && decl.id) consider(decl.id.name, decl);
349
+ else if (t$2.isVariableDeclaration(decl)) for (const d of decl.declarations) {
350
+ if (!t$2.isVariableDeclarator(d) || !t$2.isIdentifier(d.id) || !d.init) continue;
351
+ if (t$2.isArrowFunctionExpression(d.init) || t$2.isFunctionExpression(d.init)) consider(d.id.name, d.init);
351
352
  }
352
353
  };
353
354
  for (const decl of ast.program.body) {
354
355
  visitDecl(decl);
355
- if (t$1.isExportNamedDeclaration(decl) || t$1.isExportDefaultDeclaration(decl)) {
356
+ if (t$2.isExportNamedDeclaration(decl) || t$2.isExportDefaultDeclaration(decl)) {
356
357
  const inner = decl.declaration;
357
- if (inner && (t$1.isStatement(inner) || t$1.isFunctionDeclaration(inner))) visitDecl(inner);
358
+ if (inner && (t$2.isStatement(inner) || t$2.isFunctionDeclaration(inner))) visitDecl(inner);
358
359
  }
359
360
  }
360
361
  return best;
@@ -362,21 +363,21 @@ function findEnclosingComponent(ast, target) {
362
363
  function componentDestructuresProp(fn, propName) {
363
364
  if (fn.params.length === 0) return false;
364
365
  let first = fn.params[0];
365
- if (t$1.isAssignmentPattern(first)) first = first.left;
366
- if (!t$1.isObjectPattern(first)) return false;
366
+ if (t$2.isAssignmentPattern(first)) first = first.left;
367
+ if (!t$2.isObjectPattern(first)) return false;
367
368
  for (const prop of first.properties) {
368
- if (!t$1.isObjectProperty(prop)) continue;
369
- if (t$1.isIdentifier(prop.key) && prop.key.name === propName) return true;
370
- if (t$1.isStringLiteral(prop.key) && prop.key.value === propName) return true;
369
+ if (!t$2.isObjectProperty(prop)) continue;
370
+ if (t$2.isIdentifier(prop.key) && prop.key.name === propName) return true;
371
+ if (t$2.isStringLiteral(prop.key) && prop.key.value === propName) return true;
371
372
  }
372
373
  return false;
373
374
  }
374
375
  function collectCallSiteCandidates(ast, componentName) {
375
376
  const out = [];
376
377
  walkJsx(ast, (n) => {
377
- if (!t$1.isJSXElement(n)) return;
378
+ if (!t$2.isJSXElement(n)) return;
378
379
  const elName = n.openingElement.name;
379
- if (t$1.isJSXIdentifier(elName) && elName.name === componentName) collectTextCandidates(n, out);
380
+ if (t$2.isJSXIdentifier(elName) && elName.name === componentName) collectTextCandidates(n, out);
380
381
  });
381
382
  return out;
382
383
  }
@@ -394,19 +395,19 @@ function spliceRange(node, text) {
394
395
  function collectPropCallSiteCandidates(ast, componentName, propName) {
395
396
  const out = [];
396
397
  walkJsx(ast, (n) => {
397
- if (!t$1.isJSXElement(n)) return;
398
+ if (!t$2.isJSXElement(n)) return;
398
399
  const elName = n.openingElement.name;
399
- if (!t$1.isJSXIdentifier(elName) || elName.name !== componentName) return;
400
+ if (!t$2.isJSXIdentifier(elName) || elName.name !== componentName) return;
400
401
  const attr = findJsxAttr(n.openingElement, propName);
401
402
  if (!attr?.value) return;
402
403
  const v = attr.value;
403
- if (t$1.isStringLiteral(v)) out.push({
404
+ if (t$2.isStringLiteral(v)) out.push({
404
405
  current: v.value,
405
406
  splice: (s) => spliceRange(v, formatJsxAttrValue(s))
406
407
  });
407
- else if (t$1.isJSXExpressionContainer(v)) {
408
+ else if (t$2.isJSXExpressionContainer(v)) {
408
409
  const expr = v.expression;
409
- if (t$1.isStringLiteral(expr) || t$1.isNumericLiteral(expr)) out.push({
410
+ if (t$2.isStringLiteral(expr) || t$2.isNumericLiteral(expr)) out.push({
410
411
  current: String(expr.value),
411
412
  splice: (s) => spliceRange(v, formatJsxAttrValue(s))
412
413
  });
@@ -419,17 +420,17 @@ function findEnclosingMapCallback(ast, target) {
419
420
  const targetStart = target.start ?? 0;
420
421
  const targetEnd = target.end ?? 0;
421
422
  walkAll(ast, (node) => {
422
- if (!t$1.isCallExpression(node)) return;
423
+ if (!t$2.isCallExpression(node)) return;
423
424
  const callee = node.callee;
424
- if (!t$1.isMemberExpression(callee) || callee.computed) return;
425
- if (!t$1.isIdentifier(callee.property)) return;
425
+ if (!t$2.isMemberExpression(callee) || callee.computed) return;
426
+ if (!t$2.isIdentifier(callee.property)) return;
426
427
  if (callee.property.name !== "map" && callee.property.name !== "flatMap") return;
427
428
  const fn = node.arguments[0];
428
- if (!fn || !t$1.isArrowFunctionExpression(fn) && !t$1.isFunctionExpression(fn)) return;
429
+ if (!fn || !t$2.isArrowFunctionExpression(fn) && !t$2.isFunctionExpression(fn)) return;
429
430
  const fnStart = fn.start ?? 0;
430
431
  const fnEnd = fn.end ?? 0;
431
432
  if (fnStart > targetStart || fnEnd < targetEnd) return;
432
- if (!t$1.isExpression(callee.object)) return;
433
+ if (!t$2.isExpression(callee.object)) return;
433
434
  const size = fnEnd - fnStart;
434
435
  if (!best || size < best.size) best = {
435
436
  fn,
@@ -446,26 +447,26 @@ function findEnclosingMapCallback(ast, target) {
446
447
  }
447
448
  function resolveArrayLiteralElements(ast, expr) {
448
449
  const dropHoles = (arr) => arr.elements.filter((e) => e != null);
449
- if (t$1.isArrayExpression(expr)) return dropHoles(expr);
450
- if (!t$1.isIdentifier(expr)) return null;
450
+ if (t$2.isArrayExpression(expr)) return dropHoles(expr);
451
+ if (!t$2.isIdentifier(expr)) return null;
451
452
  const name = expr.name;
452
453
  const useStart = expr.start ?? 0;
453
454
  let init = null;
454
455
  walkAll(ast, (node) => {
455
- if (!t$1.isVariableDeclarator(node)) return;
456
- if (!t$1.isIdentifier(node.id) || node.id.name !== name) return;
457
- if (!node.init || !t$1.isArrayExpression(node.init)) return;
456
+ if (!t$2.isVariableDeclarator(node)) return;
457
+ if (!t$2.isIdentifier(node.id) || node.id.name !== name) return;
458
+ if (!node.init || !t$2.isArrayExpression(node.init)) return;
458
459
  if ((node.init.start ?? 0) > useStart) return;
459
460
  init = node.init;
460
461
  });
461
462
  return init ? dropHoles(init) : null;
462
463
  }
463
464
  function findObjectProperty(obj, name) {
464
- if (!t$1.isObjectExpression(obj)) return null;
465
+ if (!t$2.isObjectExpression(obj)) return null;
465
466
  for (const prop of obj.properties) {
466
- if (!t$1.isObjectProperty(prop) || prop.computed) continue;
467
- if (t$1.isIdentifier(prop.key) && prop.key.name === name) return prop;
468
- if (t$1.isStringLiteral(prop.key) && prop.key.value === name) return prop;
467
+ if (!t$2.isObjectProperty(prop) || prop.computed) continue;
468
+ if (t$2.isIdentifier(prop.key) && prop.key.name === name) return prop;
469
+ if (t$2.isStringLiteral(prop.key) && prop.key.value === name) return prop;
469
470
  }
470
471
  return null;
471
472
  }
@@ -473,22 +474,22 @@ function decodeMapPassthrough(element, callbackParam) {
473
474
  const meaningful = meaningfulChildren(element);
474
475
  if (meaningful.length !== 1) return null;
475
476
  const child = meaningful[0];
476
- if (!t$1.isJSXExpressionContainer(child)) return null;
477
+ if (!t$2.isJSXExpressionContainer(child)) return null;
477
478
  const expr = child.expression;
478
- if (t$1.isMemberExpression(expr)) {
479
+ if (t$2.isMemberExpression(expr)) {
479
480
  if (expr.computed) return null;
480
- if (!t$1.isIdentifier(expr.object) || !t$1.isIdentifier(expr.property)) return null;
481
- if (!callbackParam || !t$1.isIdentifier(callbackParam)) return null;
481
+ if (!t$2.isIdentifier(expr.object) || !t$2.isIdentifier(expr.property)) return null;
482
+ if (!callbackParam || !t$2.isIdentifier(callbackParam)) return null;
482
483
  if (callbackParam.name !== expr.object.name) return null;
483
484
  return expr.property.name;
484
485
  }
485
- if (t$1.isIdentifier(expr)) {
486
+ if (t$2.isIdentifier(expr)) {
486
487
  const fieldName = expr.name;
487
- if (!callbackParam || !t$1.isObjectPattern(callbackParam)) return null;
488
+ if (!callbackParam || !t$2.isObjectPattern(callbackParam)) return null;
488
489
  for (const prop of callbackParam.properties) {
489
- if (!t$1.isObjectProperty(prop) || prop.computed) continue;
490
- if (!t$1.isIdentifier(prop.key) || prop.key.name !== fieldName) continue;
491
- return t$1.isIdentifier(prop.value) && prop.value.name === fieldName ? fieldName : null;
490
+ if (!t$2.isObjectProperty(prop) || prop.computed) continue;
491
+ if (!t$2.isIdentifier(prop.key) || prop.key.name !== fieldName) continue;
492
+ return t$2.isIdentifier(prop.value) && prop.value.name === fieldName ? fieldName : null;
492
493
  }
493
494
  }
494
495
  return null;
@@ -505,11 +506,11 @@ function collectArrayMapCandidates(ast, element) {
505
506
  const prop = findObjectProperty(obj, fieldName);
506
507
  if (!prop) continue;
507
508
  const v = prop.value;
508
- if (t$1.isStringLiteral(v)) out.push({
509
+ if (t$2.isStringLiteral(v)) out.push({
509
510
  current: v.value,
510
511
  splice: (s) => spliceRange(v, jsString$1(s))
511
512
  });
512
- else if (t$1.isNumericLiteral(v)) out.push({
513
+ else if (t$2.isNumericLiteral(v)) out.push({
513
514
  current: String(v.value),
514
515
  splice: (s) => spliceRange(v, jsString$1(s))
515
516
  });
@@ -538,9 +539,9 @@ function buildTextSplice(ast, element, value, prevText) {
538
539
  function findImports$1(ast) {
539
540
  const out = [];
540
541
  for (const node of ast.program.body) {
541
- if (!t$1.isImportDeclaration(node)) continue;
542
+ if (!t$2.isImportDeclaration(node)) continue;
542
543
  let def = null;
543
- for (const spec of node.specifiers) if (t$1.isImportDefaultSpecifier(spec)) {
544
+ for (const spec of node.specifiers) if (t$2.isImportDefaultSpecifier(spec)) {
544
545
  def = spec.local.name;
545
546
  break;
546
547
  }
@@ -556,7 +557,7 @@ function collectTopLevelIdentifiers(ast) {
556
557
  const names = new Set();
557
558
  for (const imp of findImports$1(ast)) {
558
559
  if (imp.defaultIdent) names.add(imp.defaultIdent);
559
- for (const spec of imp.node.specifiers) if (!t$1.isImportDefaultSpecifier(spec)) names.add(spec.local.name);
560
+ for (const spec of imp.node.specifiers) if (!t$2.isImportDefaultSpecifier(spec)) names.add(spec.local.name);
560
561
  }
561
562
  return names;
562
563
  }
@@ -625,21 +626,21 @@ function readJsxStringAttr(opening, name) {
625
626
  const attr = findJsxAttr(opening, name);
626
627
  const v = attr?.value;
627
628
  if (!v) return null;
628
- if (t$1.isStringLiteral(v)) return v.value;
629
- if (t$1.isJSXExpressionContainer(v) && t$1.isStringLiteral(v.expression)) return v.expression.value;
629
+ if (t$2.isStringLiteral(v)) return v.value;
630
+ if (t$2.isJSXExpressionContainer(v) && t$2.isStringLiteral(v.expression)) return v.expression.value;
630
631
  return null;
631
632
  }
632
633
  function readJsxNumberAttr(opening, name) {
633
634
  const attr = findJsxAttr(opening, name);
634
635
  const v = attr?.value;
635
- if (!v || !t$1.isJSXExpressionContainer(v)) return null;
636
- if (!t$1.isNumericLiteral(v.expression)) return null;
636
+ if (!v || !t$2.isJSXExpressionContainer(v)) return null;
637
+ if (!t$2.isNumericLiteral(v.expression)) return null;
637
638
  const n = v.expression.value;
638
639
  return Number.isFinite(n) ? n : null;
639
640
  }
640
641
  function planReplacePlaceholder(ast, element, assetPath) {
641
642
  const opening = element.openingElement;
642
- if (!t$1.isJSXIdentifier(opening.name) || opening.name.name !== "ImagePlaceholder") return { error: "not a placeholder" };
643
+ if (!t$2.isJSXIdentifier(opening.name) || opening.name.name !== "ImagePlaceholder") return { error: "not a placeholder" };
643
644
  if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
644
645
  const hint = readJsxStringAttr(opening, "hint") ?? "";
645
646
  const width = readJsxNumberAttr(opening, "width");
@@ -663,7 +664,7 @@ function applyEdit(source, line, column, ops) {
663
664
  ok: true,
664
665
  source
665
666
  };
666
- const ast = parseSource$1(source);
667
+ const ast = parseSource$2(source);
667
668
  if (!ast) return {
668
669
  ok: false,
669
670
  status: 422,
@@ -759,38 +760,38 @@ function commentsPlugin(opts) {
759
760
  if (method !== "POST") return next();
760
761
  try {
761
762
  if (url.pathname === "/") {
762
- const body = await readBody$2(req);
763
+ const body = await readBody$3(req);
763
764
  const slideId = body.slideId ?? "";
764
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
765
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
766
- if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
767
- if (!Array.isArray(body.ops)) return json$2(res, 400, { error: "missing ops" });
765
+ const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
766
+ if (!file) return json$3(res, 400, { error: "invalid slideId" });
767
+ if (!body.line || body.line < 1) return json$3(res, 400, { error: "invalid line" });
768
+ if (!Array.isArray(body.ops)) return json$3(res, 400, { error: "missing ops" });
768
769
  let source;
769
770
  try {
770
771
  source = await fs.readFile(file, "utf8");
771
772
  } catch {
772
- return json$2(res, 404, { error: "slide not found" });
773
+ return json$3(res, 404, { error: "slide not found" });
773
774
  }
774
775
  const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
775
- if (!result.ok) return json$2(res, result.status, { error: result.error });
776
+ if (!result.ok) return json$3(res, result.status, { error: result.error });
776
777
  const changed = result.source !== source;
777
778
  if (changed) await fs.writeFile(file, result.source, "utf8");
778
- return json$2(res, 200, {
779
+ return json$3(res, 200, {
779
780
  ok: true,
780
781
  changed
781
782
  });
782
783
  }
783
784
  if (url.pathname === "/batch") {
784
- const body = await readBody$2(req);
785
+ const body = await readBody$3(req);
785
786
  const slideId = body.slideId ?? "";
786
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
787
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
788
- if (!Array.isArray(body.edits)) return json$2(res, 400, { error: "missing edits" });
787
+ const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
788
+ if (!file) return json$3(res, 400, { error: "invalid slideId" });
789
+ if (!Array.isArray(body.edits)) return json$3(res, 400, { error: "missing edits" });
789
790
  let source;
790
791
  try {
791
792
  source = await fs.readFile(file, "utf8");
792
793
  } catch {
793
- return json$2(res, 404, { error: "slide not found" });
794
+ return json$3(res, 404, { error: "slide not found" });
794
795
  }
795
796
  const original = source;
796
797
  const results = [];
@@ -813,7 +814,7 @@ function commentsPlugin(opts) {
813
814
  }
814
815
  const changed = source !== original;
815
816
  if (changed) await fs.writeFile(file, source, "utf8");
816
- return json$2(res, 200, {
817
+ return json$3(res, 200, {
817
818
  ok: true,
818
819
  changed,
819
820
  results
@@ -821,7 +822,7 @@ function commentsPlugin(opts) {
821
822
  }
822
823
  return next();
823
824
  } catch (err) {
824
- json$2(res, 500, { error: String(err.message ?? err) });
825
+ json$3(res, 500, { error: String(err.message ?? err) });
825
826
  }
826
827
  });
827
828
  server.middlewares.use("/__comments", async (req, res, next) => {
@@ -830,31 +831,31 @@ function commentsPlugin(opts) {
830
831
  try {
831
832
  if (method === "GET" && url.pathname === "/") {
832
833
  const slideId = url.searchParams.get("slideId") ?? "";
833
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
834
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
834
+ const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
835
+ if (!file) return json$3(res, 400, { error: "invalid slideId" });
835
836
  let source;
836
837
  try {
837
838
  source = await fs.readFile(file, "utf8");
838
839
  } catch {
839
- return json$2(res, 404, { error: "slide not found" });
840
+ return json$3(res, 404, { error: "slide not found" });
840
841
  }
841
- return json$2(res, 200, { comments: parseMarkers(source) });
842
+ return json$3(res, 200, { comments: parseMarkers(source) });
842
843
  }
843
844
  if (method === "POST" && url.pathname === "/add") {
844
- const body = await readBody$2(req);
845
+ const body = await readBody$3(req);
845
846
  const slideId = body.slideId ?? "";
846
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
847
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
848
- if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
849
- if (!body.text || typeof body.text !== "string") return json$2(res, 400, { error: "missing text" });
847
+ const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
848
+ if (!file) return json$3(res, 400, { error: "invalid slideId" });
849
+ if (!body.line || body.line < 1) return json$3(res, 400, { error: "invalid line" });
850
+ if (!body.text || typeof body.text !== "string") return json$3(res, 400, { error: "missing text" });
850
851
  let source;
851
852
  try {
852
853
  source = await fs.readFile(file, "utf8");
853
854
  } catch {
854
- return json$2(res, 404, { error: "slide not found" });
855
+ return json$3(res, 404, { error: "slide not found" });
855
856
  }
856
857
  const plan = findInsertion(source, body.line, body.column);
857
- if (!plan) return json$2(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
858
+ if (!plan) return json$3(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
858
859
  const id = newId();
859
860
  const ts = new Date().toISOString();
860
861
  const payload = b64urlEncode(JSON.stringify({
@@ -865,44 +866,118 @@ function commentsPlugin(opts) {
865
866
  const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
866
867
  await fs.writeFile(file, next$1, "utf8");
867
868
  const markerLine = offsetToLine(next$1, plan.offset + 1);
868
- return json$2(res, 200, {
869
+ return json$3(res, 200, {
869
870
  id,
870
871
  line: markerLine
871
872
  });
872
873
  }
873
874
  if (method === "DELETE" && url.pathname.startsWith("/")) {
874
875
  const id = url.pathname.slice(1);
875
- if (!/^c-[a-f0-9]+$/.test(id)) return json$2(res, 400, { error: "invalid id" });
876
+ if (!/^c-[a-f0-9]+$/.test(id)) return json$3(res, 400, { error: "invalid id" });
876
877
  const slideId = url.searchParams.get("slideId") ?? "";
877
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
878
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
878
+ const file = resolveSlidePath$2(userCwd, slidesDir, slideId);
879
+ if (!file) return json$3(res, 400, { error: "invalid slideId" });
879
880
  let source;
880
881
  try {
881
882
  source = await fs.readFile(file, "utf8");
882
883
  } catch {
883
- return json$2(res, 404, { error: "slide not found" });
884
+ return json$3(res, 404, { error: "slide not found" });
884
885
  }
885
886
  const lines = source.split("\n");
886
887
  const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
887
888
  const hit = lines.findIndex((l) => idRe.test(l));
888
- if (hit === -1) return json$2(res, 404, { error: "marker not found" });
889
+ if (hit === -1) return json$3(res, 404, { error: "marker not found" });
889
890
  lines.splice(hit, 1);
890
891
  await fs.writeFile(file, lines.join("\n"), "utf8");
891
- return json$2(res, 200, { ok: true });
892
+ return json$3(res, 200, { ok: true });
892
893
  }
893
894
  next();
894
895
  } catch (err) {
895
- json$2(res, 500, { error: String(err.message ?? err) });
896
+ json$3(res, 500, { error: String(err.message ?? err) });
896
897
  }
897
898
  });
898
899
  }
899
900
  };
900
901
  }
901
902
 
903
+ //#endregion
904
+ //#region src/vite/current-plugin.ts
905
+ const SLIDE_ID_RE$3 = /^[a-z0-9_-]+$/i;
906
+ const TEXT_SNIPPET_MAX = 120;
907
+ function parseSelection(raw) {
908
+ if (raw == null || typeof raw !== "object") return null;
909
+ const sel = raw;
910
+ if (typeof sel.line !== "number" || !Number.isFinite(sel.line)) return null;
911
+ if (typeof sel.column !== "number" || !Number.isFinite(sel.column)) return null;
912
+ const tagName = typeof sel.tagName === "string" ? sel.tagName.toLowerCase().slice(0, 32) : "unknown";
913
+ const text = typeof sel.text === "string" ? sel.text.replace(/\s+/g, " ").trim().slice(0, TEXT_SNIPPET_MAX) : "";
914
+ return {
915
+ line: Math.max(1, Math.floor(sel.line)),
916
+ column: Math.max(0, Math.floor(sel.column)),
917
+ tagName,
918
+ text
919
+ };
920
+ }
921
+ function currentPlugin(opts) {
922
+ const userCwd = opts.userCwd;
923
+ const slidesDir = opts.slidesDir ?? "slides";
924
+ const outDir = path.join(userCwd, "node_modules", ".open-slide");
925
+ const outFile = path.join(outDir, "current.json");
926
+ const tmpFile = `${outFile}.tmp`;
927
+ let cached = null;
928
+ return {
929
+ name: "open-slide:current",
930
+ apply: "serve",
931
+ configureServer(server) {
932
+ server.ws.on("open-slide:current", async (raw) => {
933
+ const next = cached ? { ...cached } : {
934
+ slideId: "",
935
+ pageIndex: 0,
936
+ pageNumber: 1,
937
+ totalPages: 1,
938
+ slideTitle: "",
939
+ view: "slides",
940
+ pagePath: "",
941
+ selection: null
942
+ };
943
+ if (typeof raw?.slideId === "string") {
944
+ if (!SLIDE_ID_RE$3.test(raw.slideId)) return;
945
+ const totalPages = typeof raw.totalPages === "number" && Number.isFinite(raw.totalPages) && raw.totalPages > 0 ? Math.floor(raw.totalPages) : 1;
946
+ const rawIndex = typeof raw.pageIndex === "number" && Number.isFinite(raw.pageIndex) ? Math.floor(raw.pageIndex) : 0;
947
+ const pageIndex = Math.max(0, Math.min(totalPages - 1, rawIndex));
948
+ const slideTitle = typeof raw.slideTitle === "string" ? raw.slideTitle : raw.slideId;
949
+ const view = raw.view === "assets" ? "assets" : "slides";
950
+ const pagePath = path.join(slidesDir, raw.slideId, "index.tsx").split(path.sep).join("/");
951
+ if (cached?.slideId !== raw.slideId || cached?.pageIndex !== pageIndex) next.selection = null;
952
+ next.slideId = raw.slideId;
953
+ next.pageIndex = pageIndex;
954
+ next.pageNumber = pageIndex + 1;
955
+ next.totalPages = totalPages;
956
+ next.slideTitle = slideTitle;
957
+ next.view = view;
958
+ next.pagePath = pagePath;
959
+ }
960
+ if ("selection" in raw) next.selection = parseSelection(raw.selection);
961
+ if (!next.slideId) return;
962
+ cached = next;
963
+ const body = {
964
+ ...next,
965
+ updatedAt: new Date().toISOString()
966
+ };
967
+ try {
968
+ await fs.mkdir(outDir, { recursive: true });
969
+ await fs.writeFile(tmpFile, `${JSON.stringify(body, null, 2)}\n`, "utf8");
970
+ await fs.rename(tmpFile, outFile);
971
+ } catch {}
972
+ });
973
+ }
974
+ };
975
+ }
976
+
902
977
  //#endregion
903
978
  //#region src/vite/design-plugin.ts
904
- const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
905
- async function readBody$1(req) {
979
+ const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
980
+ async function readBody$2(req) {
906
981
  return await new Promise((resolve, reject) => {
907
982
  const chunks = [];
908
983
  req.on("data", (c) => chunks.push(c));
@@ -918,19 +993,19 @@ async function readBody$1(req) {
918
993
  req.on("error", reject);
919
994
  });
920
995
  }
921
- function json$1(res, status, body) {
996
+ function json$2(res, status, body) {
922
997
  res.statusCode = status;
923
998
  res.setHeader("content-type", "application/json");
924
999
  res.end(JSON.stringify(body));
925
1000
  }
926
- function resolveSlidePath(userCwd, slidesDir, slideId) {
927
- if (!SLIDE_ID_RE$1.test(slideId)) return null;
1001
+ function resolveSlidePath$1(userCwd, slidesDir, slideId) {
1002
+ if (!SLIDE_ID_RE$2.test(slideId)) return null;
928
1003
  const slidesRoot = path.resolve(userCwd, slidesDir);
929
1004
  const full = path.resolve(slidesRoot, slideId, "index.tsx");
930
1005
  if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
931
1006
  return full;
932
1007
  }
933
- function parseSource(source) {
1008
+ function parseSource$1(source) {
934
1009
  try {
935
1010
  return parse(source, {
936
1011
  sourceType: "module",
@@ -1068,7 +1143,7 @@ function serializeDesign(design) {
1068
1143
  return serializeValue(design, 0);
1069
1144
  }
1070
1145
  function parseSlideDesign(source) {
1071
- const ast = parseSource(source);
1146
+ const ast = parseSource$1(source);
1072
1147
  if (!ast) return {
1073
1148
  ok: false,
1074
1149
  exists: true,
@@ -1210,7 +1285,7 @@ function applyDesignWrite(source, next) {
1210
1285
  error: `serialize failed: ${err.message}`
1211
1286
  };
1212
1287
  }
1213
- const ast = parseSource(source);
1288
+ const ast = parseSource$1(source);
1214
1289
  if (!ast) return {
1215
1290
  ok: false,
1216
1291
  status: 422,
@@ -1226,7 +1301,7 @@ function applyDesignWrite(source, next) {
1226
1301
  };
1227
1302
  }
1228
1303
  const withImport = ensureDesignSystemImport(source, ast);
1229
- const ast2 = parseSource(withImport.source);
1304
+ const ast2 = parseSource$1(withImport.source);
1230
1305
  if (!ast2) return {
1231
1306
  ok: false,
1232
1307
  status: 422,
@@ -1252,51 +1327,51 @@ function designPlugin(opts) {
1252
1327
  const url = new URL(req.url ?? "/", "http://local");
1253
1328
  const method = req.method ?? "GET";
1254
1329
  const slideId = url.searchParams.get("slideId") ?? "";
1255
- const file = resolveSlidePath(userCwd, slidesDir, slideId);
1256
- if (!file) return json$1(res, 400, { error: "invalid slideId" });
1330
+ const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
1331
+ if (!file) return json$2(res, 400, { error: "invalid slideId" });
1257
1332
  try {
1258
1333
  if (method === "GET" && url.pathname === "/") {
1259
1334
  let source;
1260
1335
  try {
1261
1336
  source = await fs.readFile(file, "utf8");
1262
1337
  } catch {
1263
- return json$1(res, 404, { error: "slide not found" });
1338
+ return json$2(res, 404, { error: "slide not found" });
1264
1339
  }
1265
1340
  const parsed = parseSlideDesign(source);
1266
- if (parsed.ok) return json$1(res, 200, {
1341
+ if (parsed.ok) return json$2(res, 200, {
1267
1342
  design: parsed.design,
1268
1343
  exists: true,
1269
1344
  warning: null
1270
1345
  });
1271
- if (parsed.exists === false) return json$1(res, 200, {
1346
+ if (parsed.exists === false) return json$2(res, 200, {
1272
1347
  design: defaultDesign,
1273
1348
  exists: false,
1274
1349
  warning: null
1275
1350
  });
1276
- return json$1(res, 200, {
1351
+ return json$2(res, 200, {
1277
1352
  design: defaultDesign,
1278
1353
  exists: true,
1279
1354
  warning: parsed.error
1280
1355
  });
1281
1356
  }
1282
1357
  if (method === "PUT" && url.pathname === "/") {
1283
- const body = await readBody$1(req);
1358
+ const body = await readBody$2(req);
1284
1359
  const patch = body.patch;
1285
- if (!patch || typeof patch !== "object") return json$1(res, 400, { error: "missing patch object" });
1360
+ if (!patch || typeof patch !== "object") return json$2(res, 400, { error: "missing patch object" });
1286
1361
  let source;
1287
1362
  try {
1288
1363
  source = await fs.readFile(file, "utf8");
1289
1364
  } catch {
1290
- return json$1(res, 404, { error: "slide not found" });
1365
+ return json$2(res, 404, { error: "slide not found" });
1291
1366
  }
1292
1367
  const parsed = parseSlideDesign(source);
1293
1368
  const baseDesign = parsed.ok ? parsed.design : defaultDesign;
1294
- if (!parsed.ok && parsed.exists) return json$1(res, 422, { error: parsed.error });
1369
+ if (!parsed.ok && parsed.exists) return json$2(res, 422, { error: parsed.error });
1295
1370
  const merged = mergeDesign(baseDesign, patch);
1296
1371
  const written = applyDesignWrite(source, merged);
1297
- if (!written.ok) return json$1(res, written.status, { error: written.error });
1372
+ if (!written.ok) return json$2(res, written.status, { error: written.error });
1298
1373
  if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
1299
- return json$1(res, 200, {
1374
+ return json$2(res, 200, {
1300
1375
  ok: true,
1301
1376
  design: merged,
1302
1377
  created: written.created
@@ -1307,12 +1382,12 @@ function designPlugin(opts) {
1307
1382
  try {
1308
1383
  source = await fs.readFile(file, "utf8");
1309
1384
  } catch {
1310
- return json$1(res, 404, { error: "slide not found" });
1385
+ return json$2(res, 404, { error: "slide not found" });
1311
1386
  }
1312
1387
  const written = applyDesignWrite(source, defaultDesign);
1313
- if (!written.ok) return json$1(res, written.status, { error: written.error });
1388
+ if (!written.ok) return json$2(res, written.status, { error: written.error });
1314
1389
  if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
1315
- return json$1(res, 200, {
1390
+ return json$2(res, 200, {
1316
1391
  ok: true,
1317
1392
  design: defaultDesign,
1318
1393
  created: written.created
@@ -1320,7 +1395,7 @@ function designPlugin(opts) {
1320
1395
  }
1321
1396
  return next();
1322
1397
  } catch (err) {
1323
- json$1(res, 500, { error: String(err.message ?? err) });
1398
+ json$2(res, 500, { error: String(err.message ?? err) });
1324
1399
  }
1325
1400
  });
1326
1401
  }
@@ -1330,7 +1405,7 @@ function designPlugin(opts) {
1330
1405
  //#endregion
1331
1406
  //#region src/vite/files-plugin.ts
1332
1407
  const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
1333
- const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
1408
+ const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
1334
1409
  const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
1335
1410
  const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
1336
1411
  const ASSET_MAX_BYTES = 25 * 1024 * 1024;
@@ -1371,7 +1446,7 @@ function validateAssetName(v) {
1371
1446
  if (dot <= 0 || dot === trimmed.length - 1) return null;
1372
1447
  return trimmed;
1373
1448
  }
1374
- async function readBody(req) {
1449
+ async function readBody$1(req) {
1375
1450
  return await new Promise((resolve, reject) => {
1376
1451
  const chunks = [];
1377
1452
  req.on("data", (c) => chunks.push(c));
@@ -1387,7 +1462,7 @@ async function readBody(req) {
1387
1462
  req.on("error", reject);
1388
1463
  });
1389
1464
  }
1390
- function json(res, status, body) {
1465
+ function json$1(res, status, body) {
1391
1466
  res.statusCode = status;
1392
1467
  res.setHeader("content-type", "application/json");
1393
1468
  res.end(JSON.stringify(body));
@@ -1431,7 +1506,7 @@ function validateSlideName(v) {
1431
1506
  return trimmed;
1432
1507
  }
1433
1508
  async function rmSlideDir(slidesRoot, slideId) {
1434
- if (!SLIDE_ID_RE.test(slideId)) return false;
1509
+ if (!SLIDE_ID_RE$1.test(slideId)) return false;
1435
1510
  const dir = path.resolve(slidesRoot, slideId);
1436
1511
  if (!dir.startsWith(slidesRoot + path.sep)) return false;
1437
1512
  try {
@@ -1445,7 +1520,7 @@ async function rmSlideDir(slidesRoot, slideId) {
1445
1520
  }
1446
1521
  }
1447
1522
  function resolveAssetsDir(slidesRoot, slideId) {
1448
- if (!SLIDE_ID_RE.test(slideId)) return null;
1523
+ if (!SLIDE_ID_RE$1.test(slideId)) return null;
1449
1524
  const slideDir = path.resolve(slidesRoot, slideId);
1450
1525
  if (!slideDir.startsWith(slidesRoot + path.sep)) return null;
1451
1526
  const assetsDir = path.resolve(slideDir, "assets");
@@ -1461,7 +1536,7 @@ function resolveAssetFile(slidesRoot, slideId, filename) {
1461
1536
  return file;
1462
1537
  }
1463
1538
  function resolveSlideEntry(slidesRoot, slideId) {
1464
- if (!SLIDE_ID_RE.test(slideId)) return null;
1539
+ if (!SLIDE_ID_RE$1.test(slideId)) return null;
1465
1540
  const dir = path.resolve(slidesRoot, slideId);
1466
1541
  if (!dir.startsWith(slidesRoot + path.sep)) return null;
1467
1542
  return path.join(dir, "index.tsx");
@@ -1602,6 +1677,158 @@ function reorderDefaultExportPagesInSource(source, order) {
1602
1677
  rebuilt += suffix;
1603
1678
  return source.slice(0, arrayStart) + rebuilt + source.slice(arrayEnd);
1604
1679
  }
1680
+ function findNotesArray(source) {
1681
+ let ast;
1682
+ try {
1683
+ ast = parse(source, {
1684
+ sourceType: "module",
1685
+ plugins: ["typescript", "jsx"],
1686
+ errorRecovery: true
1687
+ });
1688
+ } catch {
1689
+ return "invalid";
1690
+ }
1691
+ const body = ast.program?.body ?? [];
1692
+ for (const stmt of body) {
1693
+ if (stmt.type !== "ExportNamedDeclaration") continue;
1694
+ const decl = stmt.declaration;
1695
+ if (!decl || decl.type !== "VariableDeclaration") continue;
1696
+ const declarations = decl.declarations ?? [];
1697
+ for (const d of declarations) {
1698
+ const id = d.id;
1699
+ if (!id || id.type !== "Identifier" || id.name !== "notes") continue;
1700
+ const init = d.init;
1701
+ if (!init || init.type !== "ArrayExpression") return "invalid";
1702
+ const arrayStart = init.start;
1703
+ const arrayEnd = init.end;
1704
+ if (typeof arrayStart !== "number" || typeof arrayEnd !== "number") return "invalid";
1705
+ const rawElements = init.elements ?? [];
1706
+ const elementTexts = [];
1707
+ for (const el of rawElements) {
1708
+ if (el === null) {
1709
+ elementTexts.push("undefined");
1710
+ continue;
1711
+ }
1712
+ if (el.type === "SpreadElement") return "invalid";
1713
+ const start = el.start;
1714
+ const end = el.end;
1715
+ if (typeof start !== "number" || typeof end !== "number") return "invalid";
1716
+ elementTexts.push(source.slice(start, end));
1717
+ }
1718
+ return {
1719
+ arrayStart,
1720
+ arrayEnd,
1721
+ elementTexts
1722
+ };
1723
+ }
1724
+ }
1725
+ return null;
1726
+ }
1727
+ /**
1728
+ * Reorder `export const notes = [...]` to follow the page-array reorder.
1729
+ *
1730
+ * `order[i]` is the original page index that should land at new position `i`.
1731
+ * The notes array is index-aligned with the pages array but may be shorter
1732
+ * (trailing `undefined` slots are routinely trimmed). Missing elements are
1733
+ * treated as `undefined`, and trailing `undefined` is trimmed again after
1734
+ * reordering to keep the file tidy.
1735
+ *
1736
+ * Returns the rewritten source, the original source if no `notes` export
1737
+ * exists or the reorder is a no-op, or `null` if the `notes` export's shape
1738
+ * is too surprising to touch safely.
1739
+ */
1740
+ function reorderNotesArrayInSource(source, order) {
1741
+ for (const idx of order) if (!Number.isInteger(idx) || idx < 0) return null;
1742
+ const found = findNotesArray(source);
1743
+ if (found === "invalid") return null;
1744
+ if (found === null) return source;
1745
+ const { arrayStart, arrayEnd, elementTexts } = found;
1746
+ const pick = (i) => i >= 0 && i < elementTexts.length ? elementTexts[i] : "undefined";
1747
+ const reordered = order.map(pick);
1748
+ while (reordered.length > 0 && reordered[reordered.length - 1] === "undefined") reordered.pop();
1749
+ const replacement = reordered.length === 0 ? "[]" : `[\n${reordered.map((s) => ` ${s},`).join("\n")}\n]`;
1750
+ if (replacement === source.slice(arrayStart, arrayEnd)) return source;
1751
+ return source.slice(0, arrayStart) + replacement + source.slice(arrayEnd);
1752
+ }
1753
+ /**
1754
+ * Remove the element at `index` from `export default [...]`.
1755
+ *
1756
+ * Preserves the source slice of every other element, dropping the separator
1757
+ * immediately following the removed element (or the preceding one when the
1758
+ * removed element is the last). Returns `null` when the default export isn't
1759
+ * an array literal or `index` is out of range.
1760
+ */
1761
+ function removePageFromDefaultExportInSource(source, index) {
1762
+ const found = findDefaultExportArray(source);
1763
+ if (!found) return null;
1764
+ const { elements, arrayStart, arrayEnd } = found;
1765
+ const n = elements.length;
1766
+ if (!Number.isInteger(index) || index < 0 || index >= n) return null;
1767
+ if (n === 1) return `${source.slice(0, arrayStart)}[]${source.slice(arrayEnd)}`;
1768
+ const prefix = source.slice(arrayStart, elements[0].start);
1769
+ const suffix = source.slice(elements[n - 1].end, arrayEnd);
1770
+ const separators = [];
1771
+ for (let i = 0; i < n - 1; i++) separators.push(source.slice(elements[i].end, elements[i + 1].start));
1772
+ const elementText = elements.map((el) => source.slice(el.start, el.end));
1773
+ const keptElements = [];
1774
+ const keptSeparators = [];
1775
+ for (let i = 0; i < n; i++) {
1776
+ if (i === index) continue;
1777
+ keptElements.push(elementText[i]);
1778
+ }
1779
+ for (let i = 0; i < n - 1; i++) {
1780
+ if (index === n - 1 ? i === n - 2 : i === index) continue;
1781
+ keptSeparators.push(separators[i]);
1782
+ }
1783
+ let rebuilt = prefix + keptElements[0];
1784
+ for (let i = 1; i < keptElements.length; i++) rebuilt += keptSeparators[i - 1] + keptElements[i];
1785
+ rebuilt += suffix;
1786
+ return source.slice(0, arrayStart) + rebuilt + source.slice(arrayEnd);
1787
+ }
1788
+ function chooseInsertSeparator(prefix, existingSeparators) {
1789
+ const sample = existingSeparators.find((s) => s.includes(","));
1790
+ if (sample) return sample;
1791
+ if (prefix.includes("\n")) {
1792
+ const m = prefix.match(/\n([ \t]*)$/);
1793
+ const indent$1 = m ? m[1] : " ";
1794
+ return `,\n${indent$1}`;
1795
+ }
1796
+ return ", ";
1797
+ }
1798
+ /**
1799
+ * Duplicate the element at `index` in `export default [...]`, inserting the
1800
+ * copy immediately after the original. Reuses an existing inter-element
1801
+ * separator when one is available so the cloned entry matches the surrounding
1802
+ * indentation. Returns `null` when the default export isn't an array literal
1803
+ * or `index` is out of range.
1804
+ */
1805
+ function duplicatePageInDefaultExportInSource(source, index) {
1806
+ const found = findDefaultExportArray(source);
1807
+ if (!found) return null;
1808
+ const { elements, arrayStart, arrayEnd } = found;
1809
+ const n = elements.length;
1810
+ if (!Number.isInteger(index) || index < 0 || index >= n) return null;
1811
+ const prefix = source.slice(arrayStart, elements[0].start);
1812
+ const suffix = source.slice(elements[n - 1].end, arrayEnd);
1813
+ const separators = [];
1814
+ for (let i = 0; i < n - 1; i++) separators.push(source.slice(elements[i].end, elements[i + 1].start));
1815
+ const elementText = elements.map((el) => source.slice(el.start, el.end));
1816
+ const insertSep = chooseInsertSeparator(prefix, separators);
1817
+ const newElements = [];
1818
+ const newSeparators = [];
1819
+ for (let i = 0; i < n; i++) {
1820
+ newElements.push(elementText[i]);
1821
+ if (i === index) {
1822
+ newElements.push(elementText[i]);
1823
+ newSeparators.push(insertSep);
1824
+ }
1825
+ if (i < n - 1) newSeparators.push(separators[i]);
1826
+ }
1827
+ let rebuilt = prefix + newElements[0];
1828
+ for (let i = 1; i < newElements.length; i++) rebuilt += newSeparators[i - 1] + newElements[i];
1829
+ rebuilt += suffix;
1830
+ return source.slice(0, arrayStart) + rebuilt + source.slice(arrayEnd);
1831
+ }
1605
1832
  function validateIcon(v) {
1606
1833
  if (!v || typeof v !== "object") return null;
1607
1834
  const icon = v;
@@ -1644,7 +1871,7 @@ function filesPlugin(opts) {
1644
1871
  const parts = rel.split(path.sep);
1645
1872
  if (parts.length < 3 || parts[1] !== "assets") return;
1646
1873
  const slideId = parts[0];
1647
- if (!SLIDE_ID_RE.test(slideId)) return;
1874
+ if (!SLIDE_ID_RE$1.test(slideId)) return;
1648
1875
  server.ws.send({
1649
1876
  type: "custom",
1650
1877
  event: "open-slide:assets-changed",
@@ -1661,52 +1888,81 @@ function filesPlugin(opts) {
1661
1888
  const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
1662
1889
  if (reorderMatch && method === "PUT") {
1663
1890
  const slideId$1 = reorderMatch[1];
1664
- if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
1665
- const body = await readBody(req);
1666
- if (!Array.isArray(body.order)) return json(res, 400, { error: "invalid order" });
1891
+ if (!SLIDE_ID_RE$1.test(slideId$1)) return json$1(res, 400, { error: "invalid slideId" });
1892
+ const body = await readBody$1(req);
1893
+ if (!Array.isArray(body.order)) return json$1(res, 400, { error: "invalid order" });
1667
1894
  const order = [];
1668
1895
  for (const v of body.order) {
1669
- if (!Number.isInteger(v)) return json(res, 400, { error: "invalid order" });
1896
+ if (!Number.isInteger(v)) return json$1(res, 400, { error: "invalid order" });
1670
1897
  order.push(v);
1671
1898
  }
1672
1899
  const entry = resolveSlideEntry(slidesRoot, slideId$1);
1673
- if (!entry) return json(res, 400, { error: "invalid slideId" });
1900
+ if (!entry) return json$1(res, 400, { error: "invalid slideId" });
1674
1901
  let source;
1675
1902
  try {
1676
1903
  source = await fs.readFile(entry, "utf8");
1677
1904
  } catch {
1678
- return json(res, 404, { error: "slide not found" });
1905
+ return json$1(res, 404, { error: "slide not found" });
1679
1906
  }
1680
- const updated = reorderDefaultExportPagesInSource(source, order);
1681
- if (updated === null) return json(res, 422, { error: "could not reorder pages — order must be a permutation of the existing array" });
1682
- if (updated !== source) await fs.writeFile(entry, updated, "utf8");
1683
- return json(res, 200, {
1907
+ const reordered = reorderDefaultExportPagesInSource(source, order);
1908
+ if (reordered === null) return json$1(res, 422, { error: "could not reorder pages — order must be a permutation of the existing array" });
1909
+ const withNotes = reorderNotesArrayInSource(reordered, order);
1910
+ if (withNotes === null) return json$1(res, 422, { error: "could not reorder pages — `notes` export has an unexpected shape" });
1911
+ if (withNotes !== source) await fs.writeFile(entry, withNotes, "utf8");
1912
+ return json$1(res, 200, {
1684
1913
  ok: true,
1685
1914
  slideId: slideId$1,
1686
1915
  order
1687
1916
  });
1688
1917
  }
1918
+ const pageOpMatch = url.pathname.match(/^\/([^/]+)\/pages\/(\d+)(?:\/([a-z]+))?$/);
1919
+ if (pageOpMatch) {
1920
+ const slideId$1 = pageOpMatch[1];
1921
+ const pageIndex = Number.parseInt(pageOpMatch[2], 10);
1922
+ const op = pageOpMatch[3];
1923
+ if (!SLIDE_ID_RE$1.test(slideId$1)) return json$1(res, 400, { error: "invalid slideId" });
1924
+ if (!Number.isInteger(pageIndex) || pageIndex < 0) return json$1(res, 400, { error: "invalid page index" });
1925
+ const isDelete = method === "DELETE" && !op;
1926
+ const isDuplicate = method === "POST" && op === "duplicate";
1927
+ if (!isDelete && !isDuplicate) return next();
1928
+ const entry = resolveSlideEntry(slidesRoot, slideId$1);
1929
+ if (!entry) return json$1(res, 400, { error: "invalid slideId" });
1930
+ let source;
1931
+ try {
1932
+ source = await fs.readFile(entry, "utf8");
1933
+ } catch {
1934
+ return json$1(res, 404, { error: "slide not found" });
1935
+ }
1936
+ const updated = isDelete ? removePageFromDefaultExportInSource(source, pageIndex) : duplicatePageInDefaultExportInSource(source, pageIndex);
1937
+ if (updated === null) return json$1(res, 422, { error: isDelete ? "could not delete page — index out of range or default export is not an array" : "could not duplicate page — index out of range or default export is not an array" });
1938
+ if (updated !== source) await fs.writeFile(entry, updated, "utf8");
1939
+ return json$1(res, 200, {
1940
+ ok: true,
1941
+ slideId: slideId$1,
1942
+ index: pageIndex
1943
+ });
1944
+ }
1689
1945
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
1690
1946
  if (!idMatch) return next();
1691
1947
  const slideId = idMatch[1];
1692
- if (!SLIDE_ID_RE.test(slideId)) return json(res, 400, { error: "invalid slideId" });
1948
+ if (!SLIDE_ID_RE$1.test(slideId)) return json$1(res, 400, { error: "invalid slideId" });
1693
1949
  if (method === "PATCH") {
1694
- const body = await readBody(req);
1950
+ const body = await readBody$1(req);
1695
1951
  const name = validateSlideName(body.name);
1696
- if (!name) return json(res, 400, { error: "invalid name" });
1952
+ if (!name) return json$1(res, 400, { error: "invalid name" });
1697
1953
  const entry = resolveSlideEntry(slidesRoot, slideId);
1698
- if (!entry) return json(res, 400, { error: "invalid slideId" });
1954
+ if (!entry) return json$1(res, 400, { error: "invalid slideId" });
1699
1955
  let source;
1700
1956
  try {
1701
1957
  source = await fs.readFile(entry, "utf8");
1702
1958
  } catch {
1703
- return json(res, 404, { error: "slide not found" });
1959
+ return json$1(res, 404, { error: "slide not found" });
1704
1960
  }
1705
1961
  const updated = updateMetaTitleInSource(source, name);
1706
- if (updated === null) return json(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
1962
+ if (updated === null) return json$1(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
1707
1963
  if (updated !== source) await fs.writeFile(entry, updated, "utf8");
1708
1964
  server.ws.send({ type: "full-reload" });
1709
- return json(res, 200, {
1965
+ return json$1(res, 200, {
1710
1966
  ok: true,
1711
1967
  slideId,
1712
1968
  name
@@ -1714,15 +1970,15 @@ function filesPlugin(opts) {
1714
1970
  }
1715
1971
  if (method === "DELETE") {
1716
1972
  const removed = await rmSlideDir(slidesRoot, slideId);
1717
- if (!removed) return json(res, 404, { error: "slide not found" });
1973
+ if (!removed) return json$1(res, 404, { error: "slide not found" });
1718
1974
  const manifest = await readManifest(manifestPath);
1719
1975
  delete manifest.assignments[slideId];
1720
1976
  await writeManifest(manifestPath, manifest);
1721
- return json(res, 200, { ok: true });
1977
+ return json$1(res, 200, { ok: true });
1722
1978
  }
1723
1979
  return next();
1724
1980
  } catch (err) {
1725
- json(res, 500, { error: String(err.message ?? err) });
1981
+ json$1(res, 500, { error: String(err.message ?? err) });
1726
1982
  }
1727
1983
  });
1728
1984
  server.middlewares.use("/__assets", async (req, res, next) => {
@@ -1734,12 +1990,12 @@ function filesPlugin(opts) {
1734
1990
  if (listMatch && method === "GET") {
1735
1991
  const slideId = listMatch[1];
1736
1992
  const assetsDir = resolveAssetsDir(slidesRoot, slideId);
1737
- if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
1993
+ if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
1738
1994
  let entries;
1739
1995
  try {
1740
1996
  entries = await fs.readdir(assetsDir);
1741
1997
  } catch (err) {
1742
- if (err.code === "ENOENT") return json(res, 200, { assets: [] });
1998
+ if (err.code === "ENOENT") return json$1(res, 200, { assets: [] });
1743
1999
  throw err;
1744
2000
  }
1745
2001
  const assets = [];
@@ -1756,13 +2012,13 @@ function filesPlugin(opts) {
1756
2012
  });
1757
2013
  }
1758
2014
  assets.sort((a, b) => a.name.localeCompare(b.name));
1759
- return json(res, 200, { assets });
2015
+ return json$1(res, 200, { assets });
1760
2016
  }
1761
2017
  if (fileMatch) {
1762
2018
  const slideId = fileMatch[1];
1763
2019
  const filename = decodeURIComponent(fileMatch[2]);
1764
2020
  const file = resolveAssetFile(slidesRoot, slideId, filename);
1765
- if (!file) return json(res, 400, { error: "invalid path" });
2021
+ if (!file) return json$1(res, 400, { error: "invalid path" });
1766
2022
  if (method === "GET") try {
1767
2023
  const buf = await fs.readFile(file);
1768
2024
  res.statusCode = 200;
@@ -1771,20 +2027,20 @@ function filesPlugin(opts) {
1771
2027
  res.end(buf);
1772
2028
  return;
1773
2029
  } catch (err) {
1774
- if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2030
+ if (err.code === "ENOENT") return json$1(res, 404, { error: "asset not found" });
1775
2031
  throw err;
1776
2032
  }
1777
2033
  if (method === "POST") {
1778
2034
  const overwrite = url.searchParams.get("overwrite") === "1";
1779
2035
  const lenHeader = req.headers["content-length"];
1780
2036
  const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
1781
- if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json(res, 413, { error: "file too large" });
2037
+ if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json$1(res, 413, { error: "file too large" });
1782
2038
  if (!overwrite) try {
1783
2039
  await fs.access(file);
1784
- return json(res, 409, { error: "asset exists" });
2040
+ return json$1(res, 409, { error: "asset exists" });
1785
2041
  } catch {}
1786
2042
  const assetsDir = resolveAssetsDir(slidesRoot, slideId);
1787
- if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
2043
+ if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
1788
2044
  await fs.mkdir(assetsDir, { recursive: true });
1789
2045
  const chunks = [];
1790
2046
  let total = 0;
@@ -1802,9 +2058,9 @@ function filesPlugin(opts) {
1802
2058
  req.on("end", () => resolve());
1803
2059
  req.on("error", reject);
1804
2060
  });
1805
- if (oversized) return json(res, 413, { error: "file too large" });
2061
+ if (oversized) return json$1(res, 413, { error: "file too large" });
1806
2062
  await fs.writeFile(file, Buffer.concat(chunks));
1807
- return json(res, 200, {
2063
+ return json$1(res, 200, {
1808
2064
  ok: true,
1809
2065
  name: filename,
1810
2066
  size: total,
@@ -1813,26 +2069,26 @@ function filesPlugin(opts) {
1813
2069
  });
1814
2070
  }
1815
2071
  if (method === "PATCH") {
1816
- const body = await readBody(req);
2072
+ const body = await readBody$1(req);
1817
2073
  const target = validateAssetName(body.name);
1818
- if (!target) return json(res, 400, { error: "invalid name" });
1819
- if (target === filename) return json(res, 200, {
2074
+ if (!target) return json$1(res, 400, { error: "invalid name" });
2075
+ if (target === filename) return json$1(res, 200, {
1820
2076
  ok: true,
1821
2077
  name: filename
1822
2078
  });
1823
2079
  const dest = resolveAssetFile(slidesRoot, slideId, target);
1824
- if (!dest) return json(res, 400, { error: "invalid name" });
2080
+ if (!dest) return json$1(res, 400, { error: "invalid name" });
1825
2081
  try {
1826
2082
  await fs.access(dest);
1827
- return json(res, 409, { error: "target exists" });
2083
+ return json$1(res, 409, { error: "target exists" });
1828
2084
  } catch {}
1829
2085
  try {
1830
2086
  await fs.rename(file, dest);
1831
2087
  } catch (err) {
1832
- if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2088
+ if (err.code === "ENOENT") return json$1(res, 404, { error: "asset not found" });
1833
2089
  throw err;
1834
2090
  }
1835
- return json(res, 200, {
2091
+ return json$1(res, 200, {
1836
2092
  ok: true,
1837
2093
  name: target
1838
2094
  });
@@ -1841,15 +2097,15 @@ function filesPlugin(opts) {
1841
2097
  try {
1842
2098
  await fs.unlink(file);
1843
2099
  } catch (err) {
1844
- if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2100
+ if (err.code === "ENOENT") return json$1(res, 404, { error: "asset not found" });
1845
2101
  throw err;
1846
2102
  }
1847
- return json(res, 200, { ok: true });
2103
+ return json$1(res, 200, { ok: true });
1848
2104
  }
1849
2105
  }
1850
2106
  return next();
1851
2107
  } catch (err) {
1852
- json(res, 500, { error: String(err.message ?? err) });
2108
+ json$1(res, 500, { error: String(err.message ?? err) });
1853
2109
  }
1854
2110
  });
1855
2111
  server.middlewares.use("/__svgl", async (req, res, next) => {
@@ -1868,16 +2124,16 @@ function filesPlugin(opts) {
1868
2124
  target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
1869
2125
  } else if (reqUrl.pathname === "/svg") {
1870
2126
  const u = reqUrl.searchParams.get("u");
1871
- if (!u) return json(res, 400, { error: "missing u" });
2127
+ if (!u) return json$1(res, 400, { error: "missing u" });
1872
2128
  let parsed;
1873
2129
  try {
1874
2130
  parsed = new URL(u);
1875
2131
  } catch {
1876
- return json(res, 400, { error: "invalid u" });
2132
+ return json$1(res, 400, { error: "invalid u" });
1877
2133
  }
1878
- if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
2134
+ if (parsed.protocol !== "https:") return json$1(res, 400, { error: "https only" });
1879
2135
  const host = parsed.hostname.toLowerCase();
1880
- if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json(res, 400, { error: "host not allowed" });
2136
+ if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json$1(res, 400, { error: "host not allowed" });
1881
2137
  target = parsed.toString();
1882
2138
  } else return next();
1883
2139
  const upstream = await fetch(target);
@@ -1888,7 +2144,7 @@ function filesPlugin(opts) {
1888
2144
  const buf = Buffer.from(await upstream.arrayBuffer());
1889
2145
  res.end(buf);
1890
2146
  } catch (err) {
1891
- json(res, 502, { error: String(err.message ?? err) });
2147
+ json$1(res, 502, { error: String(err.message ?? err) });
1892
2148
  }
1893
2149
  });
1894
2150
  server.middlewares.use("/__folders", async (req, res, next) => {
@@ -1897,14 +2153,14 @@ function filesPlugin(opts) {
1897
2153
  try {
1898
2154
  if (method === "GET" && url.pathname === "/") {
1899
2155
  const manifest = await readManifest(manifestPath);
1900
- return json(res, 200, manifest);
2156
+ return json$1(res, 200, manifest);
1901
2157
  }
1902
2158
  if (method === "POST" && url.pathname === "/") {
1903
- const body = await readBody(req);
2159
+ const body = await readBody$1(req);
1904
2160
  const name = validateName(body.name);
1905
- if (!name) return json(res, 400, { error: "invalid name" });
2161
+ if (!name) return json$1(res, 400, { error: "invalid name" });
1906
2162
  const icon = validateIcon(body.icon);
1907
- if (!icon) return json(res, 400, { error: "invalid icon" });
2163
+ if (!icon) return json$1(res, 400, { error: "invalid icon" });
1908
2164
  const manifest = await readManifest(manifestPath);
1909
2165
  const folder = {
1910
2166
  id: newFolderId(),
@@ -1913,58 +2169,58 @@ function filesPlugin(opts) {
1913
2169
  };
1914
2170
  manifest.folders.push(folder);
1915
2171
  await writeManifest(manifestPath, manifest);
1916
- return json(res, 200, folder);
2172
+ return json$1(res, 200, folder);
1917
2173
  }
1918
2174
  if (method === "PUT" && url.pathname === "/assign") {
1919
- const body = await readBody(req);
1920
- if (typeof body.slideId !== "string" || !SLIDE_ID_RE.test(body.slideId)) return json(res, 400, { error: "invalid slideId" });
2175
+ const body = await readBody$1(req);
2176
+ if (typeof body.slideId !== "string" || !SLIDE_ID_RE$1.test(body.slideId)) return json$1(res, 400, { error: "invalid slideId" });
1921
2177
  const slideId = body.slideId;
1922
2178
  let folderId;
1923
2179
  if (body.folderId === null) folderId = null;
1924
2180
  else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
1925
- else return json(res, 400, { error: "invalid folderId" });
2181
+ else return json$1(res, 400, { error: "invalid folderId" });
1926
2182
  const manifest = await readManifest(manifestPath);
1927
- if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json(res, 404, { error: "folder not found" });
2183
+ if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json$1(res, 404, { error: "folder not found" });
1928
2184
  if (folderId === null) delete manifest.assignments[slideId];
1929
2185
  else manifest.assignments[slideId] = folderId;
1930
2186
  await writeManifest(manifestPath, manifest);
1931
- return json(res, 200, { ok: true });
2187
+ return json$1(res, 200, { ok: true });
1932
2188
  }
1933
2189
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
1934
2190
  if (idMatch) {
1935
2191
  const id = idMatch[1];
1936
- if (!FOLDER_ID_RE.test(id)) return json(res, 400, { error: "invalid id" });
2192
+ if (!FOLDER_ID_RE.test(id)) return json$1(res, 400, { error: "invalid id" });
1937
2193
  if (method === "PATCH") {
1938
- const body = await readBody(req);
2194
+ const body = await readBody$1(req);
1939
2195
  const manifest = await readManifest(manifestPath);
1940
2196
  const folder = manifest.folders.find((f) => f.id === id);
1941
- if (!folder) return json(res, 404, { error: "folder not found" });
2197
+ if (!folder) return json$1(res, 404, { error: "folder not found" });
1942
2198
  if (body.name !== void 0) {
1943
2199
  const name = validateName(body.name);
1944
- if (!name) return json(res, 400, { error: "invalid name" });
2200
+ if (!name) return json$1(res, 400, { error: "invalid name" });
1945
2201
  folder.name = name;
1946
2202
  }
1947
2203
  if (body.icon !== void 0) {
1948
2204
  const icon = validateIcon(body.icon);
1949
- if (!icon) return json(res, 400, { error: "invalid icon" });
2205
+ if (!icon) return json$1(res, 400, { error: "invalid icon" });
1950
2206
  folder.icon = icon;
1951
2207
  }
1952
2208
  await writeManifest(manifestPath, manifest);
1953
- return json(res, 200, folder);
2209
+ return json$1(res, 200, folder);
1954
2210
  }
1955
2211
  if (method === "DELETE") {
1956
2212
  const manifest = await readManifest(manifestPath);
1957
2213
  const before = manifest.folders.length;
1958
2214
  manifest.folders = manifest.folders.filter((f) => f.id !== id);
1959
- if (manifest.folders.length === before) return json(res, 404, { error: "folder not found" });
2215
+ if (manifest.folders.length === before) return json$1(res, 404, { error: "folder not found" });
1960
2216
  for (const [slideId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[slideId];
1961
2217
  await writeManifest(manifestPath, manifest);
1962
- return json(res, 200, { ok: true });
2218
+ return json$1(res, 200, { ok: true });
1963
2219
  }
1964
2220
  }
1965
2221
  next();
1966
2222
  } catch (err) {
1967
- json(res, 500, { error: String(err.message ?? err) });
2223
+ json$1(res, 500, { error: String(err.message ?? err) });
1968
2224
  }
1969
2225
  });
1970
2226
  }
@@ -1975,11 +2231,11 @@ function filesPlugin(opts) {
1975
2231
  //#region src/vite/loc-tags-plugin.ts
1976
2232
  const FORWARDING_COMPONENTS = new Set(["ImagePlaceholder"]);
1977
2233
  function isTaggableJsxName(name) {
1978
- if (!t.isJSXIdentifier(name)) return false;
2234
+ if (!t$1.isJSXIdentifier(name)) return false;
1979
2235
  return /^[a-z]/.test(name.name) || FORWARDING_COMPONENTS.has(name.name);
1980
2236
  }
1981
2237
  function alreadyTagged(opening) {
1982
- return opening.attributes.some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-slide-loc");
2238
+ return opening.attributes.some((attr) => t$1.isJSXAttribute(attr) && t$1.isJSXIdentifier(attr.name) && attr.name.name === "data-slide-loc");
1983
2239
  }
1984
2240
  function injectLocTags(code) {
1985
2241
  let ast;
@@ -1994,7 +2250,7 @@ function injectLocTags(code) {
1994
2250
  }
1995
2251
  const insertions = [];
1996
2252
  walkJsx(ast, (node) => {
1997
- if (!t.isJSXElement(node) || !node.loc) return;
2253
+ if (!t$1.isJSXElement(node) || !node.loc) return;
1998
2254
  const opening = node.openingElement;
1999
2255
  const name = opening.name;
2000
2256
  if (!isTaggableJsxName(name) || alreadyTagged(opening)) return;
@@ -2029,6 +2285,204 @@ function locTagsPlugin(opts) {
2029
2285
  };
2030
2286
  }
2031
2287
 
2288
+ //#endregion
2289
+ //#region src/vite/notes-plugin.ts
2290
+ const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
2291
+ async function readBody(req) {
2292
+ return await new Promise((resolve, reject) => {
2293
+ const chunks = [];
2294
+ req.on("data", (c) => chunks.push(c));
2295
+ req.on("end", () => {
2296
+ const raw = Buffer.concat(chunks).toString("utf8");
2297
+ if (!raw) return resolve({});
2298
+ try {
2299
+ resolve(JSON.parse(raw));
2300
+ } catch (e) {
2301
+ reject(e);
2302
+ }
2303
+ });
2304
+ req.on("error", reject);
2305
+ });
2306
+ }
2307
+ function json(res, status, body) {
2308
+ res.statusCode = status;
2309
+ res.setHeader("content-type", "application/json");
2310
+ res.end(JSON.stringify(body));
2311
+ }
2312
+ function resolveSlidePath(userCwd, slidesDir, slideId) {
2313
+ if (!SLIDE_ID_RE.test(slideId)) return null;
2314
+ const slidesRoot = path.resolve(userCwd, slidesDir);
2315
+ const full = path.resolve(slidesRoot, slideId, "index.tsx");
2316
+ if (!full.startsWith(slidesRoot + path.sep)) return null;
2317
+ return full;
2318
+ }
2319
+ function parseSource(source) {
2320
+ try {
2321
+ return parse(source, {
2322
+ sourceType: "module",
2323
+ plugins: ["typescript", "jsx"],
2324
+ errorRecovery: true
2325
+ });
2326
+ } catch {
2327
+ return null;
2328
+ }
2329
+ }
2330
+ function findNotesExport(ast) {
2331
+ for (const stmt of ast.program.body) {
2332
+ if (!t.isExportNamedDeclaration(stmt)) continue;
2333
+ const decl = stmt.declaration;
2334
+ if (!decl || !t.isVariableDeclaration(decl)) continue;
2335
+ for (const d of decl.declarations) {
2336
+ if (!t.isVariableDeclarator(d)) continue;
2337
+ if (!t.isIdentifier(d.id) || d.id.name !== "notes") continue;
2338
+ if (!d.init) return { error: "`notes` export has no initializer" };
2339
+ if (!t.isArrayExpression(d.init)) return { error: "`notes` export is not an array literal" };
2340
+ const arr = d.init;
2341
+ if (typeof stmt.start !== "number" || typeof stmt.end !== "number") return { error: "`notes` export missing source range" };
2342
+ if (typeof arr.start !== "number" || typeof arr.end !== "number") return { error: "`notes` array missing source range" };
2343
+ return {
2344
+ declStart: stmt.start,
2345
+ declEnd: stmt.end,
2346
+ arrayStart: arr.start,
2347
+ arrayEnd: arr.end,
2348
+ elements: arr.elements
2349
+ };
2350
+ }
2351
+ }
2352
+ return null;
2353
+ }
2354
+ function renderNoteLiteral(text) {
2355
+ if (text === "") return "undefined";
2356
+ const hasNewline = /\n/.test(text);
2357
+ if (hasNewline) {
2358
+ const escaped = text.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
2359
+ return `\`${escaped}\``;
2360
+ }
2361
+ return JSON.stringify(text);
2362
+ }
2363
+ function findInsertionOffset(ast, source) {
2364
+ let lastImportEnd = -1;
2365
+ for (const stmt of ast.program.body) if (t.isImportDeclaration(stmt) && typeof stmt.end === "number") lastImportEnd = Math.max(lastImportEnd, stmt.end);
2366
+ if (lastImportEnd >= 0) return lastImportEnd;
2367
+ return source.length;
2368
+ }
2369
+ function applyNotesEdit(source, index, text) {
2370
+ if (!Number.isInteger(index) || index < 0) return {
2371
+ ok: false,
2372
+ status: 400,
2373
+ error: "invalid index"
2374
+ };
2375
+ const ast = parseSource(source);
2376
+ if (!ast) return {
2377
+ ok: false,
2378
+ status: 422,
2379
+ error: "could not parse source"
2380
+ };
2381
+ const found = findNotesExport(ast);
2382
+ if (found && "error" in found) return {
2383
+ ok: false,
2384
+ status: 422,
2385
+ error: found.error
2386
+ };
2387
+ const literal = renderNoteLiteral(text);
2388
+ if (!found) {
2389
+ if (text === "") return {
2390
+ ok: true,
2391
+ source
2392
+ };
2393
+ const padding = Array.from({ length: index }, () => "undefined");
2394
+ const items = [...padding, literal];
2395
+ const block = [
2396
+ "",
2397
+ "",
2398
+ "export const notes: (string | undefined)[] = [",
2399
+ ...items.map((s) => ` ${s},`),
2400
+ "];",
2401
+ ""
2402
+ ].join("\n");
2403
+ const offset = findInsertionOffset(ast, source);
2404
+ const next$1 = source.slice(0, offset) + block + source.slice(offset);
2405
+ return {
2406
+ ok: true,
2407
+ source: next$1
2408
+ };
2409
+ }
2410
+ const elementTexts = [];
2411
+ for (const el of found.elements) {
2412
+ if (el === null) {
2413
+ elementTexts.push("undefined");
2414
+ continue;
2415
+ }
2416
+ if (typeof el.start !== "number" || typeof el.end !== "number") return {
2417
+ ok: false,
2418
+ status: 422,
2419
+ error: "`notes` element missing source range"
2420
+ };
2421
+ elementTexts.push(source.slice(el.start, el.end));
2422
+ }
2423
+ while (elementTexts.length <= index) elementTexts.push("undefined");
2424
+ elementTexts[index] = literal;
2425
+ while (elementTexts.length > 0 && elementTexts[elementTexts.length - 1] === "undefined") elementTexts.pop();
2426
+ const replacement = elementTexts.length === 0 ? "[]" : `[\n${elementTexts.map((s) => ` ${s},`).join("\n")}\n]`;
2427
+ const next = source.slice(0, found.arrayStart) + replacement + source.slice(found.arrayEnd);
2428
+ return {
2429
+ ok: true,
2430
+ source: next
2431
+ };
2432
+ }
2433
+ function notesPlugin(opts) {
2434
+ const userCwd = opts.userCwd;
2435
+ const slidesDir = opts.slidesDir ?? "slides";
2436
+ const recentWrites = new Map();
2437
+ const RECENT_WRITE_WINDOW_MS = 1500;
2438
+ return {
2439
+ name: "open-slide:notes",
2440
+ apply: "serve",
2441
+ handleHotUpdate(ctx) {
2442
+ const ts = recentWrites.get(ctx.file);
2443
+ if (ts != null && Date.now() - ts < RECENT_WRITE_WINDOW_MS) {
2444
+ recentWrites.delete(ctx.file);
2445
+ return [];
2446
+ }
2447
+ return void 0;
2448
+ },
2449
+ configureServer(server) {
2450
+ server.middlewares.use("/__notes", async (req, res, next) => {
2451
+ const url = new URL(req.url ?? "/", "http://local");
2452
+ const method = req.method ?? "GET";
2453
+ if (method !== "PUT" || url.pathname !== "/") return next();
2454
+ try {
2455
+ const body = await readBody(req);
2456
+ const slideId = body.slideId ?? "";
2457
+ const file = resolveSlidePath(userCwd, slidesDir, slideId);
2458
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2459
+ if (typeof body.index !== "number") return json(res, 400, { error: "missing index" });
2460
+ if (typeof body.text !== "string") return json(res, 400, { error: "missing text" });
2461
+ let source;
2462
+ try {
2463
+ source = await fs.readFile(file, "utf8");
2464
+ } catch {
2465
+ return json(res, 404, { error: "slide not found" });
2466
+ }
2467
+ const result = applyNotesEdit(source, body.index, body.text);
2468
+ if (!result.ok) return json(res, result.status, { error: result.error });
2469
+ const changed = result.source !== source;
2470
+ if (changed) {
2471
+ recentWrites.set(file, Date.now());
2472
+ await fs.writeFile(file, result.source, "utf8");
2473
+ }
2474
+ return json(res, 200, {
2475
+ ok: true,
2476
+ changed
2477
+ });
2478
+ } catch (err) {
2479
+ json(res, 500, { error: String(err.message ?? err) });
2480
+ }
2481
+ });
2482
+ }
2483
+ };
2484
+ }
2485
+
2032
2486
  //#endregion
2033
2487
  //#region src/vite/open-slide-plugin.ts
2034
2488
  const CONFIG_FILE = "open-slide.config.ts";
@@ -2051,7 +2505,7 @@ async function readFoldersManifest(file) {
2051
2505
  throw err;
2052
2506
  }
2053
2507
  }
2054
- function resolved(id) {
2508
+ function resolved$1(id) {
2055
2509
  return `\0${id}`;
2056
2510
  }
2057
2511
  async function findSlides(userCwd, slidesDir) {
@@ -2068,19 +2522,59 @@ function toId(absFile, slidesRoot) {
2068
2522
  const rel = path.relative(slidesRoot, absFile);
2069
2523
  return rel.split(path.sep)[0];
2070
2524
  }
2071
- function generateSlidesModule(files, slidesRoot, isDev) {
2072
- const entries = files.map((abs) => {
2525
+ const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
2526
+ function extractMetaTheme(src) {
2527
+ const metaStart = src.search(/export\s+const\s+meta\b/);
2528
+ if (metaStart === -1) return null;
2529
+ const eqIdx = src.indexOf("=", metaStart);
2530
+ if (eqIdx === -1) return null;
2531
+ const openBrace = src.indexOf("{", eqIdx);
2532
+ if (openBrace === -1) return null;
2533
+ let depth = 0;
2534
+ let closeBrace = -1;
2535
+ for (let i = openBrace; i < src.length; i++) {
2536
+ const ch = src[i];
2537
+ if (ch === "{") depth++;
2538
+ else if (ch === "}") {
2539
+ depth--;
2540
+ if (depth === 0) {
2541
+ closeBrace = i;
2542
+ break;
2543
+ }
2544
+ }
2545
+ }
2546
+ if (closeBrace === -1) return null;
2547
+ const body = src.slice(openBrace + 1, closeBrace);
2548
+ const m = body.match(META_THEME_RE);
2549
+ return m ? m[1] : null;
2550
+ }
2551
+ async function readSlideTheme(abs) {
2552
+ try {
2553
+ const src = await fs.readFile(abs, "utf8");
2554
+ return extractMetaTheme(src);
2555
+ } catch {
2556
+ return null;
2557
+ }
2558
+ }
2559
+ async function generateSlidesModule(files, slidesRoot, isDev) {
2560
+ const entries = await Promise.all(files.map(async (abs) => {
2073
2561
  const id = toId(abs, slidesRoot);
2074
2562
  const importPath = isDev ? `/@fs/${abs.replace(/^\/+/, "")}` : abs;
2563
+ const theme = await readSlideTheme(abs);
2075
2564
  return {
2076
2565
  id,
2077
- importPath
2566
+ importPath,
2567
+ theme
2078
2568
  };
2079
- });
2569
+ }));
2080
2570
  const ids = JSON.stringify(entries.map((e) => e.id).sort());
2571
+ const themesMap = {};
2572
+ for (const e of entries) if (e.theme) themesMap[e.id] = e.theme;
2573
+ const themesJson = JSON.stringify(themesMap);
2081
2574
  const cases = entries.map((e) => ` case ${JSON.stringify(e.id)}: return import(${JSON.stringify(e.importPath)});`).join("\n");
2082
2575
  return `// virtual:open-slide/slides — generated
2083
2576
  export const slideIds = ${ids};
2577
+ export const slideThemes = ${themesJson};
2084
2578
 
2085
2579
  export async function loadSlide(id) {
2086
2580
  switch (id) {
@@ -2103,17 +2597,17 @@ function openSlidePlugin(opts) {
2103
2597
  return { server: { fs: { allow: [userCwd] } } };
2104
2598
  },
2105
2599
  resolveId(id) {
2106
- if (id === SLIDES_VMOD) return resolved(SLIDES_VMOD);
2107
- if (id === CONFIG_VMOD) return resolved(CONFIG_VMOD);
2108
- if (id === FOLDERS_VMOD) return resolved(FOLDERS_VMOD);
2600
+ if (id === SLIDES_VMOD) return resolved$1(SLIDES_VMOD);
2601
+ if (id === CONFIG_VMOD) return resolved$1(CONFIG_VMOD);
2602
+ if (id === FOLDERS_VMOD) return resolved$1(FOLDERS_VMOD);
2109
2603
  return null;
2110
2604
  },
2111
2605
  async load(id) {
2112
- if (id === resolved(SLIDES_VMOD)) {
2606
+ if (id === resolved$1(SLIDES_VMOD)) {
2113
2607
  const files = await findSlides(userCwd, slidesDir);
2114
- return generateSlidesModule(files, slidesRoot, isDev);
2608
+ return await generateSlidesModule(files, slidesRoot, isDev);
2115
2609
  }
2116
- if (id === resolved(CONFIG_VMOD)) {
2610
+ if (id === resolved$1(CONFIG_VMOD)) {
2117
2611
  const userBuild = config.build ?? {};
2118
2612
  const buildResolved = isDev ? {
2119
2613
  showSlideBrowser: true,
@@ -2130,7 +2624,7 @@ function openSlidePlugin(opts) {
2130
2624
  };
2131
2625
  return `export default ${JSON.stringify(resolvedConfig)};\n`;
2132
2626
  }
2133
- if (id === resolved(FOLDERS_VMOD)) {
2627
+ if (id === resolved$1(FOLDERS_VMOD)) {
2134
2628
  const manifest = await readFoldersManifest(foldersManifestPath);
2135
2629
  return `export default ${JSON.stringify(manifest)};\n`;
2136
2630
  }
@@ -2149,24 +2643,36 @@ function openSlidePlugin(opts) {
2149
2643
  if (reloadTimer) clearTimeout(reloadTimer);
2150
2644
  reloadTimer = setTimeout(() => {
2151
2645
  reloadTimer = null;
2152
- const mod = server.moduleGraph.getModuleById(resolved(SLIDES_VMOD));
2646
+ const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
2153
2647
  if (mod) server.moduleGraph.invalidateModule(mod);
2154
2648
  server.ws.send({ type: "full-reload" });
2155
2649
  }, 150);
2156
2650
  };
2157
- server.watcher.add(path.join(slidesRoot, "*/index.{tsx,jsx,ts,js}"));
2651
+ if (existsSync(slidesRoot)) server.watcher.add(slidesRoot);
2158
2652
  server.watcher.on("add", (p) => {
2159
2653
  if (isSlideEntry(p)) reload();
2160
2654
  });
2161
2655
  server.watcher.on("unlink", (p) => {
2162
2656
  if (isSlideEntry(p)) reload();
2163
2657
  });
2658
+ let slideThemeTimer = null;
2659
+ const invalidateSlidesVmod = () => {
2660
+ if (slideThemeTimer) clearTimeout(slideThemeTimer);
2661
+ slideThemeTimer = setTimeout(() => {
2662
+ slideThemeTimer = null;
2663
+ const mod = server.moduleGraph.getModuleById(resolved$1(SLIDES_VMOD));
2664
+ if (mod) server.moduleGraph.invalidateModule(mod);
2665
+ }, 100);
2666
+ };
2667
+ server.watcher.on("change", (p) => {
2668
+ if (isSlideEntry(p)) invalidateSlidesVmod();
2669
+ });
2164
2670
  let foldersTimer = null;
2165
2671
  const invalidateFolders = () => {
2166
2672
  if (foldersTimer) clearTimeout(foldersTimer);
2167
2673
  foldersTimer = setTimeout(() => {
2168
2674
  foldersTimer = null;
2169
- const mod = server.moduleGraph.getModuleById(resolved(FOLDERS_VMOD));
2675
+ const mod = server.moduleGraph.getModuleById(resolved$1(FOLDERS_VMOD));
2170
2676
  if (mod) server.moduleGraph.invalidateModule(mod);
2171
2677
  }, 100);
2172
2678
  };
@@ -2193,6 +2699,144 @@ async function loadUserConfig(userCwd) {
2193
2699
  return loaded?.config ?? {};
2194
2700
  }
2195
2701
 
2702
+ //#endregion
2703
+ //#region src/vite/themes-plugin.ts
2704
+ const THEMES_VMOD = "virtual:open-slide/themes";
2705
+ function resolved(id) {
2706
+ return `\0${id}`;
2707
+ }
2708
+ const FM_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
2709
+ function parseFrontmatter(raw, themeId) {
2710
+ const match = raw.match(FM_RE);
2711
+ const fmText = match ? match[1] : "";
2712
+ const body = match ? match[2] : raw;
2713
+ const data = {};
2714
+ for (const line of fmText.split(/\r?\n/)) {
2715
+ const m = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
2716
+ if (!m) continue;
2717
+ let value = m[2].trim();
2718
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2719
+ data[m[1]] = value;
2720
+ }
2721
+ return {
2722
+ fm: {
2723
+ name: data.name || themeId,
2724
+ description: data.description || ""
2725
+ },
2726
+ body: body.trim()
2727
+ };
2728
+ }
2729
+ async function findThemes(userCwd, themesDir) {
2730
+ const abs = path.resolve(userCwd, themesDir);
2731
+ if (!existsSync(abs)) return [];
2732
+ const hits = await fg("*.md", {
2733
+ cwd: abs,
2734
+ absolute: true,
2735
+ onlyFiles: true
2736
+ });
2737
+ return hits.sort();
2738
+ }
2739
+ async function readTheme(mdAbs, themesRoot) {
2740
+ const id = path.basename(mdAbs, ".md");
2741
+ const raw = await fs.readFile(mdAbs, "utf8");
2742
+ const { fm, body } = parseFrontmatter(raw, id);
2743
+ const demoCandidates = [
2744
+ `${id}.demo.tsx`,
2745
+ `${id}.demo.jsx`,
2746
+ `${id}.demo.ts`,
2747
+ `${id}.demo.js`
2748
+ ];
2749
+ let demoAbs = null;
2750
+ for (const cand of demoCandidates) {
2751
+ const p = path.join(themesRoot, cand);
2752
+ if (existsSync(p)) {
2753
+ demoAbs = p;
2754
+ break;
2755
+ }
2756
+ }
2757
+ return {
2758
+ id,
2759
+ frontmatter: fm,
2760
+ body,
2761
+ demoAbs
2762
+ };
2763
+ }
2764
+ function generateThemesModule(themes, isDev) {
2765
+ const meta = themes.map((t$3) => ({
2766
+ id: t$3.id,
2767
+ name: t$3.frontmatter.name,
2768
+ description: t$3.frontmatter.description,
2769
+ body: t$3.body,
2770
+ hasDemo: t$3.demoAbs !== null
2771
+ }));
2772
+ const cases = themes.flatMap((t$3) => {
2773
+ const abs = t$3.demoAbs;
2774
+ if (!abs) return [];
2775
+ const importPath = isDev ? `/@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
2776
+ return [` case ${JSON.stringify(t$3.id)}: return import(${JSON.stringify(importPath)});`];
2777
+ }).join("\n");
2778
+ return `// virtual:open-slide/themes — generated
2779
+ export const themes = ${JSON.stringify(meta)};
2780
+
2781
+ export async function loadThemeDemo(id) {
2782
+ switch (id) {
2783
+ ${cases}
2784
+ default: throw new Error('Theme demo not found: ' + id);
2785
+ }
2786
+ }
2787
+ `;
2788
+ }
2789
+ function themesPlugin(opts) {
2790
+ const { userCwd, config } = opts;
2791
+ const themesDir = config.themesDir ?? "themes";
2792
+ const themesRoot = path.resolve(userCwd, themesDir);
2793
+ let isDev = false;
2794
+ return {
2795
+ name: "open-slide:themes",
2796
+ config(_c, env) {
2797
+ isDev = env.command === "serve";
2798
+ },
2799
+ resolveId(id) {
2800
+ if (id === THEMES_VMOD) return resolved(THEMES_VMOD);
2801
+ return null;
2802
+ },
2803
+ async load(id) {
2804
+ if (id !== resolved(THEMES_VMOD)) return null;
2805
+ const files = await findThemes(userCwd, themesDir);
2806
+ const themes = await Promise.all(files.map((f) => readTheme(f, themesRoot)));
2807
+ return generateThemesModule(themes, isDev);
2808
+ },
2809
+ configureServer(server) {
2810
+ const isThemeFile = (p) => {
2811
+ const rel = path.relative(themesRoot, p);
2812
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
2813
+ if (rel.includes(path.sep)) return false;
2814
+ return /\.(md|demo\.(tsx|jsx|ts|js))$/.test(rel);
2815
+ };
2816
+ let reloadTimer = null;
2817
+ const reload = () => {
2818
+ if (reloadTimer) clearTimeout(reloadTimer);
2819
+ reloadTimer = setTimeout(() => {
2820
+ reloadTimer = null;
2821
+ const mod = server.moduleGraph.getModuleById(resolved(THEMES_VMOD));
2822
+ if (mod) server.moduleGraph.invalidateModule(mod);
2823
+ server.ws.send({ type: "full-reload" });
2824
+ }, 150);
2825
+ };
2826
+ if (existsSync(themesRoot)) server.watcher.add(themesRoot);
2827
+ server.watcher.on("add", (p) => {
2828
+ if (isThemeFile(p)) reload();
2829
+ });
2830
+ server.watcher.on("unlink", (p) => {
2831
+ if (isThemeFile(p)) reload();
2832
+ });
2833
+ server.watcher.on("change", (p) => {
2834
+ if (isThemeFile(p)) reload();
2835
+ });
2836
+ }
2837
+ };
2838
+ }
2839
+
2196
2840
  //#endregion
2197
2841
  //#region src/vite/config.ts
2198
2842
  function findPackageRoot(fromFile) {
@@ -2209,7 +2853,9 @@ async function createViteConfig(opts) {
2209
2853
  const userCwd = path.resolve(opts.userCwd);
2210
2854
  const config = opts.config ?? await loadUserConfig(userCwd);
2211
2855
  const slidesDir = config.slidesDir ?? "slides";
2856
+ const themesDir = config.themesDir ?? "themes";
2212
2857
  const slidesAbs = path.resolve(userCwd, slidesDir);
2858
+ const themesAbs = path.resolve(userCwd, themesDir);
2213
2859
  return {
2214
2860
  root: APP_ROOT,
2215
2861
  configFile: false,
@@ -2225,14 +2871,26 @@ async function createViteConfig(opts) {
2225
2871
  userCwd,
2226
2872
  config
2227
2873
  }),
2874
+ themesPlugin({
2875
+ userCwd,
2876
+ config
2877
+ }),
2228
2878
  designPlugin({ userCwd }),
2229
2879
  commentsPlugin({
2230
2880
  userCwd,
2231
2881
  slidesDir
2232
2882
  }),
2883
+ notesPlugin({
2884
+ userCwd,
2885
+ slidesDir
2886
+ }),
2233
2887
  filesPlugin({
2234
2888
  userCwd,
2235
2889
  slidesDir
2890
+ }),
2891
+ currentPlugin({
2892
+ userCwd,
2893
+ slidesDir
2236
2894
  })
2237
2895
  ],
2238
2896
  resolve: { alias: { "@": APP_ROOT } },
@@ -2266,7 +2924,8 @@ async function createViteConfig(opts) {
2266
2924
  fs: { allow: [
2267
2925
  APP_ROOT,
2268
2926
  userCwd,
2269
- slidesAbs
2927
+ slidesAbs,
2928
+ themesAbs
2270
2929
  ] }
2271
2930
  },
2272
2931
  build: {