@open-slide/core 1.0.6 → 1.2.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 (37) hide show
  1. package/dist/{build-4wOJF1l4.js → build-6BeQ3cxb.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-evLWCV1-.js → config-AxZ5OE1u.js} +772 -201
  4. package/dist/{config-D2y1AXaN.d.ts → config-CtT8K4VF.d.ts} +1 -1
  5. package/dist/{dev-BUr0S-Ij.js → dev-C9eLmUEq.js} +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/locale/index.d.ts +1 -1
  8. package/dist/locale/index.js +136 -24
  9. package/dist/{preview-DP_gIphz.js → preview-Cunm-f4i.js} +1 -1
  10. package/dist/{types-BVvl_xup.d.ts → types-CRHIeoNq.d.ts} +37 -4
  11. package/dist/vite/index.d.ts +2 -2
  12. package/dist/vite/index.js +1 -1
  13. package/package.json +5 -1
  14. package/skills/current-slide/SKILL.md +110 -0
  15. package/skills/slide-authoring/SKILL.md +48 -1
  16. package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
  17. package/src/app/components/inspector/inspect-overlay.tsx +17 -2
  18. package/src/app/components/inspector/inspector-panel.tsx +90 -26
  19. package/src/app/components/inspector/inspector-provider.tsx +136 -1
  20. package/src/app/components/notes-drawer.tsx +117 -0
  21. package/src/app/components/player.tsx +26 -8
  22. package/src/app/components/present/overview-grid.tsx +2 -2
  23. package/src/app/components/present/use-idle.ts +6 -4
  24. package/src/app/components/style-panel/design-provider.tsx +13 -0
  25. package/src/app/components/style-panel/style-panel.tsx +23 -11
  26. package/src/app/components/thumbnail-rail.tsx +317 -55
  27. package/src/app/components/ui/context-menu.tsx +237 -0
  28. package/src/app/lib/design-presets.ts +94 -0
  29. package/src/app/lib/inspector/use-notes.ts +134 -0
  30. package/src/app/routes/home.tsx +34 -12
  31. package/src/app/routes/presenter.tsx +27 -24
  32. package/src/app/routes/slide.tsx +238 -51
  33. package/src/locale/en.ts +35 -4
  34. package/src/locale/ja.ts +35 -4
  35. package/src/locale/types.ts +38 -4
  36. package/src/locale/zh-cn.ts +35 -4
  37. package/src/locale/zh-tw.ts +35 -4
@@ -7,6 +7,7 @@ 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";
@@ -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");
@@ -647,8 +648,11 @@ function planReplacePlaceholder(ast, element, assetPath) {
647
648
  const { identifier, importSplice } = planAssetImport(ast, assetPath);
648
649
  const styleParts = [];
649
650
  if (width != null) styleParts.push(`width: ${width}`);
651
+ else if (height != null) styleParts.push(`width: '100%'`);
650
652
  if (height != null) styleParts.push(`height: ${height}`);
653
+ else if (width != null) styleParts.push(`height: '100%'`);
651
654
  styleParts.push(`objectFit: 'cover'`);
655
+ styleParts.push(`objectPosition: '50% 50%'`);
652
656
  const replacement = `<img src={${identifier}} alt=${jsString$1(hint)} style={{ ${styleParts.join(", ")} }} />`;
653
657
  return {
654
658
  importSplice,
@@ -660,7 +664,7 @@ function applyEdit(source, line, column, ops) {
660
664
  ok: true,
661
665
  source
662
666
  };
663
- const ast = parseSource$1(source);
667
+ const ast = parseSource$2(source);
664
668
  if (!ast) return {
665
669
  ok: false,
666
670
  status: 422,
@@ -756,38 +760,38 @@ function commentsPlugin(opts) {
756
760
  if (method !== "POST") return next();
757
761
  try {
758
762
  if (url.pathname === "/") {
759
- const body = await readBody$2(req);
763
+ const body = await readBody$3(req);
760
764
  const slideId = body.slideId ?? "";
761
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
762
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
763
- if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
764
- 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" });
765
769
  let source;
766
770
  try {
767
771
  source = await fs.readFile(file, "utf8");
768
772
  } catch {
769
- return json$2(res, 404, { error: "slide not found" });
773
+ return json$3(res, 404, { error: "slide not found" });
770
774
  }
771
775
  const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
772
- 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 });
773
777
  const changed = result.source !== source;
774
778
  if (changed) await fs.writeFile(file, result.source, "utf8");
775
- return json$2(res, 200, {
779
+ return json$3(res, 200, {
776
780
  ok: true,
777
781
  changed
778
782
  });
779
783
  }
780
784
  if (url.pathname === "/batch") {
781
- const body = await readBody$2(req);
785
+ const body = await readBody$3(req);
782
786
  const slideId = body.slideId ?? "";
783
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
784
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
785
- 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" });
786
790
  let source;
787
791
  try {
788
792
  source = await fs.readFile(file, "utf8");
789
793
  } catch {
790
- return json$2(res, 404, { error: "slide not found" });
794
+ return json$3(res, 404, { error: "slide not found" });
791
795
  }
792
796
  const original = source;
793
797
  const results = [];
@@ -810,7 +814,7 @@ function commentsPlugin(opts) {
810
814
  }
811
815
  const changed = source !== original;
812
816
  if (changed) await fs.writeFile(file, source, "utf8");
813
- return json$2(res, 200, {
817
+ return json$3(res, 200, {
814
818
  ok: true,
815
819
  changed,
816
820
  results
@@ -818,7 +822,7 @@ function commentsPlugin(opts) {
818
822
  }
819
823
  return next();
820
824
  } catch (err) {
821
- json$2(res, 500, { error: String(err.message ?? err) });
825
+ json$3(res, 500, { error: String(err.message ?? err) });
822
826
  }
823
827
  });
824
828
  server.middlewares.use("/__comments", async (req, res, next) => {
@@ -827,31 +831,31 @@ function commentsPlugin(opts) {
827
831
  try {
828
832
  if (method === "GET" && url.pathname === "/") {
829
833
  const slideId = url.searchParams.get("slideId") ?? "";
830
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
831
- 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" });
832
836
  let source;
833
837
  try {
834
838
  source = await fs.readFile(file, "utf8");
835
839
  } catch {
836
- return json$2(res, 404, { error: "slide not found" });
840
+ return json$3(res, 404, { error: "slide not found" });
837
841
  }
838
- return json$2(res, 200, { comments: parseMarkers(source) });
842
+ return json$3(res, 200, { comments: parseMarkers(source) });
839
843
  }
840
844
  if (method === "POST" && url.pathname === "/add") {
841
- const body = await readBody$2(req);
845
+ const body = await readBody$3(req);
842
846
  const slideId = body.slideId ?? "";
843
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
844
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
845
- if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
846
- 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" });
847
851
  let source;
848
852
  try {
849
853
  source = await fs.readFile(file, "utf8");
850
854
  } catch {
851
- return json$2(res, 404, { error: "slide not found" });
855
+ return json$3(res, 404, { error: "slide not found" });
852
856
  }
853
857
  const plan = findInsertion(source, body.line, body.column);
854
- 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.` });
855
859
  const id = newId();
856
860
  const ts = new Date().toISOString();
857
861
  const payload = b64urlEncode(JSON.stringify({
@@ -862,35 +866,109 @@ function commentsPlugin(opts) {
862
866
  const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
863
867
  await fs.writeFile(file, next$1, "utf8");
864
868
  const markerLine = offsetToLine(next$1, plan.offset + 1);
865
- return json$2(res, 200, {
869
+ return json$3(res, 200, {
866
870
  id,
867
871
  line: markerLine
868
872
  });
869
873
  }
870
874
  if (method === "DELETE" && url.pathname.startsWith("/")) {
871
875
  const id = url.pathname.slice(1);
872
- 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" });
873
877
  const slideId = url.searchParams.get("slideId") ?? "";
874
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
875
- 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" });
876
880
  let source;
877
881
  try {
878
882
  source = await fs.readFile(file, "utf8");
879
883
  } catch {
880
- return json$2(res, 404, { error: "slide not found" });
884
+ return json$3(res, 404, { error: "slide not found" });
881
885
  }
882
886
  const lines = source.split("\n");
883
887
  const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
884
888
  const hit = lines.findIndex((l) => idRe.test(l));
885
- 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" });
886
890
  lines.splice(hit, 1);
887
891
  await fs.writeFile(file, lines.join("\n"), "utf8");
888
- return json$2(res, 200, { ok: true });
892
+ return json$3(res, 200, { ok: true });
889
893
  }
890
894
  next();
891
895
  } catch (err) {
892
- json$2(res, 500, { error: String(err.message ?? err) });
896
+ json$3(res, 500, { error: String(err.message ?? err) });
897
+ }
898
+ });
899
+ }
900
+ };
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;
893
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 {}
894
972
  });
895
973
  }
896
974
  };
@@ -898,8 +976,8 @@ function commentsPlugin(opts) {
898
976
 
899
977
  //#endregion
900
978
  //#region src/vite/design-plugin.ts
901
- const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
902
- async function readBody$1(req) {
979
+ const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
980
+ async function readBody$2(req) {
903
981
  return await new Promise((resolve, reject) => {
904
982
  const chunks = [];
905
983
  req.on("data", (c) => chunks.push(c));
@@ -915,19 +993,19 @@ async function readBody$1(req) {
915
993
  req.on("error", reject);
916
994
  });
917
995
  }
918
- function json$1(res, status, body) {
996
+ function json$2(res, status, body) {
919
997
  res.statusCode = status;
920
998
  res.setHeader("content-type", "application/json");
921
999
  res.end(JSON.stringify(body));
922
1000
  }
923
- function resolveSlidePath(userCwd, slidesDir, slideId) {
924
- 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;
925
1003
  const slidesRoot = path.resolve(userCwd, slidesDir);
926
1004
  const full = path.resolve(slidesRoot, slideId, "index.tsx");
927
1005
  if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
928
1006
  return full;
929
1007
  }
930
- function parseSource(source) {
1008
+ function parseSource$1(source) {
931
1009
  try {
932
1010
  return parse(source, {
933
1011
  sourceType: "module",
@@ -1065,7 +1143,7 @@ function serializeDesign(design) {
1065
1143
  return serializeValue(design, 0);
1066
1144
  }
1067
1145
  function parseSlideDesign(source) {
1068
- const ast = parseSource(source);
1146
+ const ast = parseSource$1(source);
1069
1147
  if (!ast) return {
1070
1148
  ok: false,
1071
1149
  exists: true,
@@ -1207,7 +1285,7 @@ function applyDesignWrite(source, next) {
1207
1285
  error: `serialize failed: ${err.message}`
1208
1286
  };
1209
1287
  }
1210
- const ast = parseSource(source);
1288
+ const ast = parseSource$1(source);
1211
1289
  if (!ast) return {
1212
1290
  ok: false,
1213
1291
  status: 422,
@@ -1223,7 +1301,7 @@ function applyDesignWrite(source, next) {
1223
1301
  };
1224
1302
  }
1225
1303
  const withImport = ensureDesignSystemImport(source, ast);
1226
- const ast2 = parseSource(withImport.source);
1304
+ const ast2 = parseSource$1(withImport.source);
1227
1305
  if (!ast2) return {
1228
1306
  ok: false,
1229
1307
  status: 422,
@@ -1249,51 +1327,51 @@ function designPlugin(opts) {
1249
1327
  const url = new URL(req.url ?? "/", "http://local");
1250
1328
  const method = req.method ?? "GET";
1251
1329
  const slideId = url.searchParams.get("slideId") ?? "";
1252
- const file = resolveSlidePath(userCwd, slidesDir, slideId);
1253
- 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" });
1254
1332
  try {
1255
1333
  if (method === "GET" && url.pathname === "/") {
1256
1334
  let source;
1257
1335
  try {
1258
1336
  source = await fs.readFile(file, "utf8");
1259
1337
  } catch {
1260
- return json$1(res, 404, { error: "slide not found" });
1338
+ return json$2(res, 404, { error: "slide not found" });
1261
1339
  }
1262
1340
  const parsed = parseSlideDesign(source);
1263
- if (parsed.ok) return json$1(res, 200, {
1341
+ if (parsed.ok) return json$2(res, 200, {
1264
1342
  design: parsed.design,
1265
1343
  exists: true,
1266
1344
  warning: null
1267
1345
  });
1268
- if (parsed.exists === false) return json$1(res, 200, {
1346
+ if (parsed.exists === false) return json$2(res, 200, {
1269
1347
  design: defaultDesign,
1270
1348
  exists: false,
1271
1349
  warning: null
1272
1350
  });
1273
- return json$1(res, 200, {
1351
+ return json$2(res, 200, {
1274
1352
  design: defaultDesign,
1275
1353
  exists: true,
1276
1354
  warning: parsed.error
1277
1355
  });
1278
1356
  }
1279
1357
  if (method === "PUT" && url.pathname === "/") {
1280
- const body = await readBody$1(req);
1358
+ const body = await readBody$2(req);
1281
1359
  const patch = body.patch;
1282
- 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" });
1283
1361
  let source;
1284
1362
  try {
1285
1363
  source = await fs.readFile(file, "utf8");
1286
1364
  } catch {
1287
- return json$1(res, 404, { error: "slide not found" });
1365
+ return json$2(res, 404, { error: "slide not found" });
1288
1366
  }
1289
1367
  const parsed = parseSlideDesign(source);
1290
1368
  const baseDesign = parsed.ok ? parsed.design : defaultDesign;
1291
- 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 });
1292
1370
  const merged = mergeDesign(baseDesign, patch);
1293
1371
  const written = applyDesignWrite(source, merged);
1294
- 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 });
1295
1373
  if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
1296
- return json$1(res, 200, {
1374
+ return json$2(res, 200, {
1297
1375
  ok: true,
1298
1376
  design: merged,
1299
1377
  created: written.created
@@ -1304,12 +1382,12 @@ function designPlugin(opts) {
1304
1382
  try {
1305
1383
  source = await fs.readFile(file, "utf8");
1306
1384
  } catch {
1307
- return json$1(res, 404, { error: "slide not found" });
1385
+ return json$2(res, 404, { error: "slide not found" });
1308
1386
  }
1309
1387
  const written = applyDesignWrite(source, defaultDesign);
1310
- 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 });
1311
1389
  if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
1312
- return json$1(res, 200, {
1390
+ return json$2(res, 200, {
1313
1391
  ok: true,
1314
1392
  design: defaultDesign,
1315
1393
  created: written.created
@@ -1317,7 +1395,7 @@ function designPlugin(opts) {
1317
1395
  }
1318
1396
  return next();
1319
1397
  } catch (err) {
1320
- json$1(res, 500, { error: String(err.message ?? err) });
1398
+ json$2(res, 500, { error: String(err.message ?? err) });
1321
1399
  }
1322
1400
  });
1323
1401
  }
@@ -1327,7 +1405,7 @@ function designPlugin(opts) {
1327
1405
  //#endregion
1328
1406
  //#region src/vite/files-plugin.ts
1329
1407
  const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
1330
- const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
1408
+ const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
1331
1409
  const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
1332
1410
  const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
1333
1411
  const ASSET_MAX_BYTES = 25 * 1024 * 1024;
@@ -1368,7 +1446,7 @@ function validateAssetName(v) {
1368
1446
  if (dot <= 0 || dot === trimmed.length - 1) return null;
1369
1447
  return trimmed;
1370
1448
  }
1371
- async function readBody(req) {
1449
+ async function readBody$1(req) {
1372
1450
  return await new Promise((resolve, reject) => {
1373
1451
  const chunks = [];
1374
1452
  req.on("data", (c) => chunks.push(c));
@@ -1384,7 +1462,7 @@ async function readBody(req) {
1384
1462
  req.on("error", reject);
1385
1463
  });
1386
1464
  }
1387
- function json(res, status, body) {
1465
+ function json$1(res, status, body) {
1388
1466
  res.statusCode = status;
1389
1467
  res.setHeader("content-type", "application/json");
1390
1468
  res.end(JSON.stringify(body));
@@ -1428,7 +1506,7 @@ function validateSlideName(v) {
1428
1506
  return trimmed;
1429
1507
  }
1430
1508
  async function rmSlideDir(slidesRoot, slideId) {
1431
- if (!SLIDE_ID_RE.test(slideId)) return false;
1509
+ if (!SLIDE_ID_RE$1.test(slideId)) return false;
1432
1510
  const dir = path.resolve(slidesRoot, slideId);
1433
1511
  if (!dir.startsWith(slidesRoot + path.sep)) return false;
1434
1512
  try {
@@ -1442,7 +1520,7 @@ async function rmSlideDir(slidesRoot, slideId) {
1442
1520
  }
1443
1521
  }
1444
1522
  function resolveAssetsDir(slidesRoot, slideId) {
1445
- if (!SLIDE_ID_RE.test(slideId)) return null;
1523
+ if (!SLIDE_ID_RE$1.test(slideId)) return null;
1446
1524
  const slideDir = path.resolve(slidesRoot, slideId);
1447
1525
  if (!slideDir.startsWith(slidesRoot + path.sep)) return null;
1448
1526
  const assetsDir = path.resolve(slideDir, "assets");
@@ -1458,7 +1536,7 @@ function resolveAssetFile(slidesRoot, slideId, filename) {
1458
1536
  return file;
1459
1537
  }
1460
1538
  function resolveSlideEntry(slidesRoot, slideId) {
1461
- if (!SLIDE_ID_RE.test(slideId)) return null;
1539
+ if (!SLIDE_ID_RE$1.test(slideId)) return null;
1462
1540
  const dir = path.resolve(slidesRoot, slideId);
1463
1541
  if (!dir.startsWith(slidesRoot + path.sep)) return null;
1464
1542
  return path.join(dir, "index.tsx");
@@ -1521,6 +1599,236 @@ function updateMetaTitleInSource(source, title) {
1521
1599
  const insertion = `export const meta: SlideMeta = { title: ${newLiteral} };\n\n`;
1522
1600
  return source.slice(0, exportDefaultIdx) + insertion + source.slice(exportDefaultIdx);
1523
1601
  }
1602
+ function findDefaultExportArray(source) {
1603
+ let ast;
1604
+ try {
1605
+ ast = parse(source, {
1606
+ sourceType: "module",
1607
+ plugins: ["typescript", "jsx"],
1608
+ errorRecovery: true
1609
+ });
1610
+ } catch {
1611
+ return null;
1612
+ }
1613
+ const body = ast.program?.body ?? [];
1614
+ for (const node of body) {
1615
+ if (node.type !== "ExportDefaultDeclaration") continue;
1616
+ let inner = node.declaration;
1617
+ while (inner && (inner.type === "TSAsExpression" || inner.type === "TSSatisfiesExpression")) inner = inner.expression;
1618
+ if (!inner || inner.type !== "ArrayExpression") return null;
1619
+ const arrayStart = inner.start;
1620
+ const arrayEnd = inner.end;
1621
+ const rawElements = inner.elements ?? [];
1622
+ const elements = [];
1623
+ for (const el of rawElements) {
1624
+ if (!el || typeof el.start !== "number" || typeof el.end !== "number") return null;
1625
+ elements.push({
1626
+ start: el.start,
1627
+ end: el.end
1628
+ });
1629
+ }
1630
+ return {
1631
+ elements,
1632
+ arrayStart,
1633
+ arrayEnd
1634
+ };
1635
+ }
1636
+ return null;
1637
+ }
1638
+ /**
1639
+ * Rewrite `export default [...]` so its elements appear in the requested order.
1640
+ *
1641
+ * `order[i]` is the original index that should land at new position `i`. The
1642
+ * function preserves each element's exact source slice (including any inline
1643
+ * comments that hug an identifier) and keeps the inter-element separator slots
1644
+ * in their original positions, so a 3-page array `[A, B, C]` reordered to
1645
+ * `[2, 0, 1]` becomes `[C, A, B]` with the same indentation and trailing
1646
+ * commas the author wrote.
1647
+ *
1648
+ * Returns `null` when the file's default export isn't an array literal, or the
1649
+ * order is not a valid permutation of `[0, n-1]`.
1650
+ */
1651
+ function reorderDefaultExportPagesInSource(source, order) {
1652
+ const found = findDefaultExportArray(source);
1653
+ if (!found) return null;
1654
+ const { elements, arrayStart, arrayEnd } = found;
1655
+ const n = elements.length;
1656
+ if (order.length !== n) return null;
1657
+ const seen = new Set();
1658
+ for (const idx of order) {
1659
+ if (!Number.isInteger(idx) || idx < 0 || idx >= n) return null;
1660
+ if (seen.has(idx)) return null;
1661
+ seen.add(idx);
1662
+ }
1663
+ if (n === 0) return source;
1664
+ let identity = true;
1665
+ for (let i = 0; i < n; i++) if (order[i] !== i) {
1666
+ identity = false;
1667
+ break;
1668
+ }
1669
+ if (identity) return source;
1670
+ const prefix = source.slice(arrayStart, elements[0].start);
1671
+ const suffix = source.slice(elements[n - 1].end, arrayEnd);
1672
+ const separators = [];
1673
+ for (let i = 0; i < n - 1; i++) separators.push(source.slice(elements[i].end, elements[i + 1].start));
1674
+ const elementText = elements.map((el) => source.slice(el.start, el.end));
1675
+ let rebuilt = prefix + elementText[order[0]];
1676
+ for (let i = 1; i < n; i++) rebuilt += separators[i - 1] + elementText[order[i]];
1677
+ rebuilt += suffix;
1678
+ return source.slice(0, arrayStart) + rebuilt + source.slice(arrayEnd);
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
+ }
1524
1832
  function validateIcon(v) {
1525
1833
  if (!v || typeof v !== "object") return null;
1526
1834
  const icon = v;
@@ -1563,7 +1871,7 @@ function filesPlugin(opts) {
1563
1871
  const parts = rel.split(path.sep);
1564
1872
  if (parts.length < 3 || parts[1] !== "assets") return;
1565
1873
  const slideId = parts[0];
1566
- if (!SLIDE_ID_RE.test(slideId)) return;
1874
+ if (!SLIDE_ID_RE$1.test(slideId)) return;
1567
1875
  server.ws.send({
1568
1876
  type: "custom",
1569
1877
  event: "open-slide:assets-changed",
@@ -1577,27 +1885,84 @@ function filesPlugin(opts) {
1577
1885
  const url = new URL(req.url ?? "/", "http://local");
1578
1886
  const method = req.method ?? "GET";
1579
1887
  try {
1888
+ const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
1889
+ if (reorderMatch && method === "PUT") {
1890
+ const slideId$1 = reorderMatch[1];
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" });
1894
+ const order = [];
1895
+ for (const v of body.order) {
1896
+ if (!Number.isInteger(v)) return json$1(res, 400, { error: "invalid order" });
1897
+ order.push(v);
1898
+ }
1899
+ const entry = resolveSlideEntry(slidesRoot, slideId$1);
1900
+ if (!entry) return json$1(res, 400, { error: "invalid slideId" });
1901
+ let source;
1902
+ try {
1903
+ source = await fs.readFile(entry, "utf8");
1904
+ } catch {
1905
+ return json$1(res, 404, { error: "slide not found" });
1906
+ }
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, {
1913
+ ok: true,
1914
+ slideId: slideId$1,
1915
+ order
1916
+ });
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
+ }
1580
1945
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
1581
1946
  if (!idMatch) return next();
1582
1947
  const slideId = idMatch[1];
1583
- 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" });
1584
1949
  if (method === "PATCH") {
1585
- const body = await readBody(req);
1950
+ const body = await readBody$1(req);
1586
1951
  const name = validateSlideName(body.name);
1587
- if (!name) return json(res, 400, { error: "invalid name" });
1952
+ if (!name) return json$1(res, 400, { error: "invalid name" });
1588
1953
  const entry = resolveSlideEntry(slidesRoot, slideId);
1589
- if (!entry) return json(res, 400, { error: "invalid slideId" });
1954
+ if (!entry) return json$1(res, 400, { error: "invalid slideId" });
1590
1955
  let source;
1591
1956
  try {
1592
1957
  source = await fs.readFile(entry, "utf8");
1593
1958
  } catch {
1594
- return json(res, 404, { error: "slide not found" });
1959
+ return json$1(res, 404, { error: "slide not found" });
1595
1960
  }
1596
1961
  const updated = updateMetaTitleInSource(source, name);
1597
- 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" });
1598
1963
  if (updated !== source) await fs.writeFile(entry, updated, "utf8");
1599
1964
  server.ws.send({ type: "full-reload" });
1600
- return json(res, 200, {
1965
+ return json$1(res, 200, {
1601
1966
  ok: true,
1602
1967
  slideId,
1603
1968
  name
@@ -1605,15 +1970,15 @@ function filesPlugin(opts) {
1605
1970
  }
1606
1971
  if (method === "DELETE") {
1607
1972
  const removed = await rmSlideDir(slidesRoot, slideId);
1608
- if (!removed) return json(res, 404, { error: "slide not found" });
1973
+ if (!removed) return json$1(res, 404, { error: "slide not found" });
1609
1974
  const manifest = await readManifest(manifestPath);
1610
1975
  delete manifest.assignments[slideId];
1611
1976
  await writeManifest(manifestPath, manifest);
1612
- return json(res, 200, { ok: true });
1977
+ return json$1(res, 200, { ok: true });
1613
1978
  }
1614
1979
  return next();
1615
1980
  } catch (err) {
1616
- json(res, 500, { error: String(err.message ?? err) });
1981
+ json$1(res, 500, { error: String(err.message ?? err) });
1617
1982
  }
1618
1983
  });
1619
1984
  server.middlewares.use("/__assets", async (req, res, next) => {
@@ -1625,12 +1990,12 @@ function filesPlugin(opts) {
1625
1990
  if (listMatch && method === "GET") {
1626
1991
  const slideId = listMatch[1];
1627
1992
  const assetsDir = resolveAssetsDir(slidesRoot, slideId);
1628
- if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
1993
+ if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
1629
1994
  let entries;
1630
1995
  try {
1631
1996
  entries = await fs.readdir(assetsDir);
1632
1997
  } catch (err) {
1633
- if (err.code === "ENOENT") return json(res, 200, { assets: [] });
1998
+ if (err.code === "ENOENT") return json$1(res, 200, { assets: [] });
1634
1999
  throw err;
1635
2000
  }
1636
2001
  const assets = [];
@@ -1647,13 +2012,13 @@ function filesPlugin(opts) {
1647
2012
  });
1648
2013
  }
1649
2014
  assets.sort((a, b) => a.name.localeCompare(b.name));
1650
- return json(res, 200, { assets });
2015
+ return json$1(res, 200, { assets });
1651
2016
  }
1652
2017
  if (fileMatch) {
1653
2018
  const slideId = fileMatch[1];
1654
2019
  const filename = decodeURIComponent(fileMatch[2]);
1655
2020
  const file = resolveAssetFile(slidesRoot, slideId, filename);
1656
- if (!file) return json(res, 400, { error: "invalid path" });
2021
+ if (!file) return json$1(res, 400, { error: "invalid path" });
1657
2022
  if (method === "GET") try {
1658
2023
  const buf = await fs.readFile(file);
1659
2024
  res.statusCode = 200;
@@ -1662,20 +2027,20 @@ function filesPlugin(opts) {
1662
2027
  res.end(buf);
1663
2028
  return;
1664
2029
  } catch (err) {
1665
- 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" });
1666
2031
  throw err;
1667
2032
  }
1668
2033
  if (method === "POST") {
1669
2034
  const overwrite = url.searchParams.get("overwrite") === "1";
1670
2035
  const lenHeader = req.headers["content-length"];
1671
2036
  const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
1672
- 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" });
1673
2038
  if (!overwrite) try {
1674
2039
  await fs.access(file);
1675
- return json(res, 409, { error: "asset exists" });
2040
+ return json$1(res, 409, { error: "asset exists" });
1676
2041
  } catch {}
1677
2042
  const assetsDir = resolveAssetsDir(slidesRoot, slideId);
1678
- if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
2043
+ if (!assetsDir) return json$1(res, 400, { error: "invalid slideId" });
1679
2044
  await fs.mkdir(assetsDir, { recursive: true });
1680
2045
  const chunks = [];
1681
2046
  let total = 0;
@@ -1693,9 +2058,9 @@ function filesPlugin(opts) {
1693
2058
  req.on("end", () => resolve());
1694
2059
  req.on("error", reject);
1695
2060
  });
1696
- if (oversized) return json(res, 413, { error: "file too large" });
2061
+ if (oversized) return json$1(res, 413, { error: "file too large" });
1697
2062
  await fs.writeFile(file, Buffer.concat(chunks));
1698
- return json(res, 200, {
2063
+ return json$1(res, 200, {
1699
2064
  ok: true,
1700
2065
  name: filename,
1701
2066
  size: total,
@@ -1704,26 +2069,26 @@ function filesPlugin(opts) {
1704
2069
  });
1705
2070
  }
1706
2071
  if (method === "PATCH") {
1707
- const body = await readBody(req);
2072
+ const body = await readBody$1(req);
1708
2073
  const target = validateAssetName(body.name);
1709
- if (!target) return json(res, 400, { error: "invalid name" });
1710
- 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, {
1711
2076
  ok: true,
1712
2077
  name: filename
1713
2078
  });
1714
2079
  const dest = resolveAssetFile(slidesRoot, slideId, target);
1715
- if (!dest) return json(res, 400, { error: "invalid name" });
2080
+ if (!dest) return json$1(res, 400, { error: "invalid name" });
1716
2081
  try {
1717
2082
  await fs.access(dest);
1718
- return json(res, 409, { error: "target exists" });
2083
+ return json$1(res, 409, { error: "target exists" });
1719
2084
  } catch {}
1720
2085
  try {
1721
2086
  await fs.rename(file, dest);
1722
2087
  } catch (err) {
1723
- 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" });
1724
2089
  throw err;
1725
2090
  }
1726
- return json(res, 200, {
2091
+ return json$1(res, 200, {
1727
2092
  ok: true,
1728
2093
  name: target
1729
2094
  });
@@ -1732,15 +2097,15 @@ function filesPlugin(opts) {
1732
2097
  try {
1733
2098
  await fs.unlink(file);
1734
2099
  } catch (err) {
1735
- 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" });
1736
2101
  throw err;
1737
2102
  }
1738
- return json(res, 200, { ok: true });
2103
+ return json$1(res, 200, { ok: true });
1739
2104
  }
1740
2105
  }
1741
2106
  return next();
1742
2107
  } catch (err) {
1743
- json(res, 500, { error: String(err.message ?? err) });
2108
+ json$1(res, 500, { error: String(err.message ?? err) });
1744
2109
  }
1745
2110
  });
1746
2111
  server.middlewares.use("/__svgl", async (req, res, next) => {
@@ -1759,16 +2124,16 @@ function filesPlugin(opts) {
1759
2124
  target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
1760
2125
  } else if (reqUrl.pathname === "/svg") {
1761
2126
  const u = reqUrl.searchParams.get("u");
1762
- if (!u) return json(res, 400, { error: "missing u" });
2127
+ if (!u) return json$1(res, 400, { error: "missing u" });
1763
2128
  let parsed;
1764
2129
  try {
1765
2130
  parsed = new URL(u);
1766
2131
  } catch {
1767
- return json(res, 400, { error: "invalid u" });
2132
+ return json$1(res, 400, { error: "invalid u" });
1768
2133
  }
1769
- if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
2134
+ if (parsed.protocol !== "https:") return json$1(res, 400, { error: "https only" });
1770
2135
  const host = parsed.hostname.toLowerCase();
1771
- 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" });
1772
2137
  target = parsed.toString();
1773
2138
  } else return next();
1774
2139
  const upstream = await fetch(target);
@@ -1779,7 +2144,7 @@ function filesPlugin(opts) {
1779
2144
  const buf = Buffer.from(await upstream.arrayBuffer());
1780
2145
  res.end(buf);
1781
2146
  } catch (err) {
1782
- json(res, 502, { error: String(err.message ?? err) });
2147
+ json$1(res, 502, { error: String(err.message ?? err) });
1783
2148
  }
1784
2149
  });
1785
2150
  server.middlewares.use("/__folders", async (req, res, next) => {
@@ -1788,14 +2153,14 @@ function filesPlugin(opts) {
1788
2153
  try {
1789
2154
  if (method === "GET" && url.pathname === "/") {
1790
2155
  const manifest = await readManifest(manifestPath);
1791
- return json(res, 200, manifest);
2156
+ return json$1(res, 200, manifest);
1792
2157
  }
1793
2158
  if (method === "POST" && url.pathname === "/") {
1794
- const body = await readBody(req);
2159
+ const body = await readBody$1(req);
1795
2160
  const name = validateName(body.name);
1796
- if (!name) return json(res, 400, { error: "invalid name" });
2161
+ if (!name) return json$1(res, 400, { error: "invalid name" });
1797
2162
  const icon = validateIcon(body.icon);
1798
- if (!icon) return json(res, 400, { error: "invalid icon" });
2163
+ if (!icon) return json$1(res, 400, { error: "invalid icon" });
1799
2164
  const manifest = await readManifest(manifestPath);
1800
2165
  const folder = {
1801
2166
  id: newFolderId(),
@@ -1804,58 +2169,58 @@ function filesPlugin(opts) {
1804
2169
  };
1805
2170
  manifest.folders.push(folder);
1806
2171
  await writeManifest(manifestPath, manifest);
1807
- return json(res, 200, folder);
2172
+ return json$1(res, 200, folder);
1808
2173
  }
1809
2174
  if (method === "PUT" && url.pathname === "/assign") {
1810
- const body = await readBody(req);
1811
- 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" });
1812
2177
  const slideId = body.slideId;
1813
2178
  let folderId;
1814
2179
  if (body.folderId === null) folderId = null;
1815
2180
  else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
1816
- else return json(res, 400, { error: "invalid folderId" });
2181
+ else return json$1(res, 400, { error: "invalid folderId" });
1817
2182
  const manifest = await readManifest(manifestPath);
1818
- 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" });
1819
2184
  if (folderId === null) delete manifest.assignments[slideId];
1820
2185
  else manifest.assignments[slideId] = folderId;
1821
2186
  await writeManifest(manifestPath, manifest);
1822
- return json(res, 200, { ok: true });
2187
+ return json$1(res, 200, { ok: true });
1823
2188
  }
1824
2189
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
1825
2190
  if (idMatch) {
1826
2191
  const id = idMatch[1];
1827
- 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" });
1828
2193
  if (method === "PATCH") {
1829
- const body = await readBody(req);
2194
+ const body = await readBody$1(req);
1830
2195
  const manifest = await readManifest(manifestPath);
1831
2196
  const folder = manifest.folders.find((f) => f.id === id);
1832
- if (!folder) return json(res, 404, { error: "folder not found" });
2197
+ if (!folder) return json$1(res, 404, { error: "folder not found" });
1833
2198
  if (body.name !== void 0) {
1834
2199
  const name = validateName(body.name);
1835
- if (!name) return json(res, 400, { error: "invalid name" });
2200
+ if (!name) return json$1(res, 400, { error: "invalid name" });
1836
2201
  folder.name = name;
1837
2202
  }
1838
2203
  if (body.icon !== void 0) {
1839
2204
  const icon = validateIcon(body.icon);
1840
- if (!icon) return json(res, 400, { error: "invalid icon" });
2205
+ if (!icon) return json$1(res, 400, { error: "invalid icon" });
1841
2206
  folder.icon = icon;
1842
2207
  }
1843
2208
  await writeManifest(manifestPath, manifest);
1844
- return json(res, 200, folder);
2209
+ return json$1(res, 200, folder);
1845
2210
  }
1846
2211
  if (method === "DELETE") {
1847
2212
  const manifest = await readManifest(manifestPath);
1848
2213
  const before = manifest.folders.length;
1849
2214
  manifest.folders = manifest.folders.filter((f) => f.id !== id);
1850
- 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" });
1851
2216
  for (const [slideId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[slideId];
1852
2217
  await writeManifest(manifestPath, manifest);
1853
- return json(res, 200, { ok: true });
2218
+ return json$1(res, 200, { ok: true });
1854
2219
  }
1855
2220
  }
1856
2221
  next();
1857
2222
  } catch (err) {
1858
- json(res, 500, { error: String(err.message ?? err) });
2223
+ json$1(res, 500, { error: String(err.message ?? err) });
1859
2224
  }
1860
2225
  });
1861
2226
  }
@@ -1866,11 +2231,11 @@ function filesPlugin(opts) {
1866
2231
  //#region src/vite/loc-tags-plugin.ts
1867
2232
  const FORWARDING_COMPONENTS = new Set(["ImagePlaceholder"]);
1868
2233
  function isTaggableJsxName(name) {
1869
- if (!t.isJSXIdentifier(name)) return false;
2234
+ if (!t$1.isJSXIdentifier(name)) return false;
1870
2235
  return /^[a-z]/.test(name.name) || FORWARDING_COMPONENTS.has(name.name);
1871
2236
  }
1872
2237
  function alreadyTagged(opening) {
1873
- 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");
1874
2239
  }
1875
2240
  function injectLocTags(code) {
1876
2241
  let ast;
@@ -1885,7 +2250,7 @@ function injectLocTags(code) {
1885
2250
  }
1886
2251
  const insertions = [];
1887
2252
  walkJsx(ast, (node) => {
1888
- if (!t.isJSXElement(node) || !node.loc) return;
2253
+ if (!t$1.isJSXElement(node) || !node.loc) return;
1889
2254
  const opening = node.openingElement;
1890
2255
  const name = opening.name;
1891
2256
  if (!isTaggableJsxName(name) || alreadyTagged(opening)) return;
@@ -1920,6 +2285,204 @@ function locTagsPlugin(opts) {
1920
2285
  };
1921
2286
  }
1922
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
+
1923
2486
  //#endregion
1924
2487
  //#region src/vite/open-slide-plugin.ts
1925
2488
  const CONFIG_FILE = "open-slide.config.ts";
@@ -2121,9 +2684,17 @@ async function createViteConfig(opts) {
2121
2684
  userCwd,
2122
2685
  slidesDir
2123
2686
  }),
2687
+ notesPlugin({
2688
+ userCwd,
2689
+ slidesDir
2690
+ }),
2124
2691
  filesPlugin({
2125
2692
  userCwd,
2126
2693
  slidesDir
2694
+ }),
2695
+ currentPlugin({
2696
+ userCwd,
2697
+ slidesDir
2127
2698
  })
2128
2699
  ],
2129
2700
  resolve: { alias: { "@": APP_ROOT } },