@open-slide/core 0.0.11 → 0.0.12

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 (88) hide show
  1. package/dist/{build-DHiRlpjn.js → build-aiY_8kwE.js} +2 -1
  2. package/dist/cli/bin.js +43 -4
  3. package/dist/{config-LZM903FE.js → config-CVqRAagl.js} +592 -63
  4. package/dist/design-CROQh0AA.js +35 -0
  5. package/dist/{dev-B3JzCYn7.js → dev-R2we2iaF.js} +2 -1
  6. package/dist/index.d.ts +55 -4
  7. package/dist/index.js +110 -1
  8. package/dist/{preview-UikovHEt.js → preview-CU4zSyGp.js} +2 -1
  9. package/dist/sync-3oqN1WyK.js +139 -0
  10. package/dist/sync-B4eLo2H6.js +3 -0
  11. package/dist/vite/index.d.ts +1 -1
  12. package/dist/vite/index.js +2 -1
  13. package/package.json +2 -1
  14. package/skills/apply-comments/SKILL.md +83 -0
  15. package/skills/create-slide/SKILL.md +81 -0
  16. package/skills/create-theme/SKILL.md +194 -0
  17. package/skills/slide-authoring/SKILL.md +288 -0
  18. package/src/app/{App.tsx → app.tsx} +8 -6
  19. package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
  20. package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
  21. package/src/app/components/history-provider.tsx +120 -0
  22. package/src/app/components/image-placeholder.tsx +121 -0
  23. package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
  24. package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
  25. package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
  26. package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
  27. package/src/app/components/inspector/save-bar.tsx +47 -0
  28. package/src/app/components/panel/panel-fields.tsx +60 -0
  29. package/src/app/components/panel/panel-shell.tsx +78 -0
  30. package/src/app/components/panel/save-card.tsx +139 -0
  31. package/src/app/components/pdf-progress-toast.tsx +25 -0
  32. package/src/app/components/player.tsx +341 -0
  33. package/src/app/components/present/blackout-overlay.tsx +18 -0
  34. package/src/app/components/present/control-bar.tsx +204 -0
  35. package/src/app/components/present/help-overlay.tsx +56 -0
  36. package/src/app/components/present/jump-input.tsx +74 -0
  37. package/src/app/components/present/laser-pointer.tsx +40 -0
  38. package/src/app/components/present/overview-grid.tsx +184 -0
  39. package/src/app/components/present/progress-bar.tsx +26 -0
  40. package/src/app/components/present/use-idle.ts +44 -0
  41. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  42. package/src/app/components/present/use-presenter-channel.ts +71 -0
  43. package/src/app/components/present/use-touch-swipe.ts +63 -0
  44. package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
  45. package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
  46. package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
  47. package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
  48. package/src/app/components/style-panel/design-provider.tsx +139 -0
  49. package/src/app/components/style-panel/style-panel.tsx +326 -0
  50. package/src/app/components/style-panel/use-design.ts +112 -0
  51. package/src/app/components/theme-toggle.tsx +57 -0
  52. package/src/app/components/thumbnail-rail.tsx +151 -0
  53. package/src/app/components/ui/button.tsx +51 -19
  54. package/src/app/components/ui/card.tsx +1 -1
  55. package/src/app/components/ui/dialog.tsx +25 -9
  56. package/src/app/components/ui/dropdown-menu.tsx +29 -12
  57. package/src/app/components/ui/input.tsx +13 -9
  58. package/src/app/components/ui/popover.tsx +5 -2
  59. package/src/app/components/ui/progress.tsx +2 -2
  60. package/src/app/components/ui/select.tsx +11 -5
  61. package/src/app/components/ui/separator.tsx +1 -1
  62. package/src/app/components/ui/slider.tsx +4 -4
  63. package/src/app/components/ui/sonner.tsx +11 -1
  64. package/src/app/components/ui/tabs.tsx +6 -6
  65. package/src/app/components/ui/textarea.tsx +11 -7
  66. package/src/app/components/ui/toggle-group.tsx +2 -2
  67. package/src/app/components/ui/toggle.tsx +6 -6
  68. package/src/app/components/ui/tooltip.tsx +5 -2
  69. package/src/app/lib/export-html.ts +10 -1
  70. package/src/app/lib/export-pdf.ts +7 -0
  71. package/src/app/lib/folders.ts +1 -1
  72. package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
  73. package/src/app/lib/sdk.ts +5 -0
  74. package/src/app/lib/slides.ts +1 -1
  75. package/src/app/lib/utils.ts +1 -1
  76. package/src/app/main.tsx +5 -2
  77. package/src/app/routes/{Home.tsx → home.tsx} +266 -97
  78. package/src/app/routes/presenter.tsx +400 -0
  79. package/src/app/routes/slide.tsx +519 -0
  80. package/src/app/styles.css +338 -67
  81. package/src/app/components/PdfProgressToast.tsx +0 -23
  82. package/src/app/components/Player.tsx +0 -100
  83. package/src/app/components/ThumbnailRail.tsx +0 -68
  84. package/src/app/components/inspector/SaveBar.tsx +0 -77
  85. package/src/app/routes/Slide.tsx +0 -478
  86. /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
  87. /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
  88. /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
@@ -1,10 +1,11 @@
1
+ import { defaultDesign } from "./design-CROQh0AA.js";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
5
+ import { randomUUID } from "node:crypto";
4
6
  import { existsSync } from "node:fs";
5
7
  import tailwindcss from "@tailwindcss/vite";
6
8
  import react from "@vitejs/plugin-react";
7
- import { randomUUID } from "node:crypto";
8
9
  import { parse } from "@babel/parser";
9
10
  import fg from "fast-glob";
10
11
  import { loadConfigFromFile } from "vite";
@@ -47,7 +48,7 @@ function walkJsx(ast, visit) {
47
48
  //#endregion
48
49
  //#region src/vite/comments-plugin.ts
49
50
  const MARKER_RE = /\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
50
- const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
51
+ const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
51
52
  function b64urlEncode(s) {
52
53
  return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
53
54
  }
@@ -55,7 +56,7 @@ function b64urlDecode(s) {
55
56
  const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
56
57
  return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
57
58
  }
58
- async function readBody$1(req) {
59
+ async function readBody$2(req) {
59
60
  return await new Promise((resolve, reject) => {
60
61
  const chunks = [];
61
62
  req.on("data", (c) => chunks.push(c));
@@ -71,13 +72,13 @@ async function readBody$1(req) {
71
72
  req.on("error", reject);
72
73
  });
73
74
  }
74
- function json$1(res, status, body) {
75
+ function json$2(res, status, body) {
75
76
  res.statusCode = status;
76
77
  res.setHeader("content-type", "application/json");
77
78
  res.end(JSON.stringify(body));
78
79
  }
79
- function resolveSlidePath(userCwd, slidesDir, slideId) {
80
- if (!SLIDE_ID_RE$1.test(slideId)) return null;
80
+ function resolveSlidePath$1(userCwd, slidesDir, slideId) {
81
+ if (!SLIDE_ID_RE$2.test(slideId)) return null;
81
82
  const slidesRoot = path.resolve(userCwd, slidesDir);
82
83
  const full = path.resolve(slidesRoot, slideId, "index.tsx");
83
84
  if (!full.startsWith(slidesRoot + path.sep)) return null;
@@ -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(source) {
188
+ function parseSource$1(source) {
188
189
  try {
189
190
  return parse(source, {
190
191
  sourceType: "module",
@@ -196,7 +197,7 @@ function parseSource(source) {
196
197
  }
197
198
  }
198
199
  function findInnermostJsxElement(source, line, column) {
199
- const ast = parseSource(source);
200
+ const ast = parseSource$1(source);
200
201
  if (!ast) return null;
201
202
  const exact = findJsxByStart(ast, line, column);
202
203
  if (exact) return exact;
@@ -216,7 +217,7 @@ function findJsxByStart(ast, line, column) {
216
217
  });
217
218
  return hit;
218
219
  }
219
- function jsString(s) {
220
+ function jsString$1(s) {
220
221
  return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
221
222
  }
222
223
  function findStyleAttr(opening) {
@@ -251,7 +252,7 @@ function buildStyleSplice(source, element, ops) {
251
252
  }
252
253
  }
253
254
  for (const op of ops) if (op.value === null) style.delete(op.key);
254
- else style.set(op.key, jsString(op.value));
255
+ else style.set(op.key, jsString$1(op.value));
255
256
  if (style.size === 0) {
256
257
  if (!existing) return null;
257
258
  let from = existing.start;
@@ -277,7 +278,7 @@ function buildStyleSplice(source, element, ops) {
277
278
  };
278
279
  }
279
280
  function formatJsxText(value) {
280
- if (/[{}<>]/.test(value) || /^\s|\s$/.test(value) || value === "") return `{${jsString(value)}}`;
281
+ if (/[{}<>]/.test(value) || /^\s|\s$/.test(value) || value === "") return `{${jsString$1(value)}}`;
281
282
  return value;
282
283
  }
283
284
  function buildTextSplice(element, value) {
@@ -306,13 +307,13 @@ function buildTextSplice(element, value) {
306
307
  if (expr.type === "StringLiteral" || expr.type === "NumericLiteral") return {
307
308
  from: child.start,
308
309
  to: child.end,
309
- text: `{${jsString(value)}}`
310
+ text: `{${jsString$1(value)}}`
310
311
  };
311
312
  return { error: "element has dynamic expression child" };
312
313
  }
313
314
  return { error: "element has complex children" };
314
315
  }
315
- function findImports(ast) {
316
+ function findImports$1(ast) {
316
317
  const body = ast.program?.body ?? [];
317
318
  const out = [];
318
319
  for (const node of body) {
@@ -338,7 +339,7 @@ function findImports(ast) {
338
339
  }
339
340
  function collectTopLevelIdentifiers(ast) {
340
341
  const names = new Set();
341
- for (const imp of findImports(ast)) {
342
+ for (const imp of findImports$1(ast)) {
342
343
  if (imp.defaultIdent) names.add(imp.defaultIdent);
343
344
  const specs = imp.node.specifiers ?? [];
344
345
  for (const spec of specs) if (spec.type !== "ImportDefaultSpecifier") {
@@ -381,7 +382,7 @@ function planAssetAttr(ast, element, attr, assetPath) {
381
382
  if (!opening) return { error: "no opening element" };
382
383
  if (!attr || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) return { error: "invalid attribute name" };
383
384
  if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
384
- const imports = findImports(ast);
385
+ const imports = findImports$1(ast);
385
386
  let identifier = null;
386
387
  for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) {
387
388
  identifier = imp.defaultIdent;
@@ -422,6 +423,75 @@ function planAssetAttr(ast, element, attr, assetPath) {
422
423
  attrSplice
423
424
  };
424
425
  }
426
+ function readJsxStringAttr(opening, name) {
427
+ const attr = findJsxAttr(opening, name);
428
+ if (!attr) return null;
429
+ const value = attr.value ?? null;
430
+ if (!value) return null;
431
+ if (value.type === "StringLiteral") return value.value;
432
+ if (value.type === "JSXExpressionContainer") {
433
+ const expr = value.expression;
434
+ if (expr.type === "StringLiteral") return expr.value;
435
+ }
436
+ return null;
437
+ }
438
+ function readJsxNumberAttr(opening, name) {
439
+ const attr = findJsxAttr(opening, name);
440
+ if (!attr) return null;
441
+ const value = attr.value ?? null;
442
+ if (!value || value.type !== "JSXExpressionContainer") return null;
443
+ const expr = value.expression;
444
+ if (expr.type === "NumericLiteral") {
445
+ const n = expr.value;
446
+ return Number.isFinite(n) ? n : null;
447
+ }
448
+ return null;
449
+ }
450
+ function planReplacePlaceholder(ast, element, assetPath) {
451
+ const opening = element.openingElement;
452
+ if (!opening) return { error: "no opening element" };
453
+ const elName = opening.name;
454
+ if (elName?.type !== "JSXIdentifier" || elName.name !== "ImagePlaceholder") return { error: "not a placeholder" };
455
+ if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
456
+ const hint = readJsxStringAttr(opening, "hint") ?? "";
457
+ const width = readJsxNumberAttr(opening, "width");
458
+ const height = readJsxNumberAttr(opening, "height");
459
+ const imports = findImports$1(ast);
460
+ let identifier = null;
461
+ for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) {
462
+ identifier = imp.defaultIdent;
463
+ break;
464
+ }
465
+ let importSplice = null;
466
+ if (!identifier) {
467
+ const filename = assetPath.slice(assetPath.lastIndexOf("/") + 1);
468
+ const taken = collectTopLevelIdentifiers(ast);
469
+ identifier = safeAssetIdentifier(filename, taken);
470
+ const importStmt = `import ${identifier} from '${assetPath.replace(/'/g, "\\'")}';\n`;
471
+ const insertAt = imports.length > 0 ? imports[imports.length - 1].node.end : 0;
472
+ const prefix = imports.length > 0 ? "\n" : "";
473
+ importSplice = {
474
+ from: insertAt,
475
+ to: insertAt,
476
+ text: prefix + importStmt
477
+ };
478
+ }
479
+ const styleParts = [];
480
+ if (width != null) styleParts.push(`width: ${width}`);
481
+ if (height != null) styleParts.push(`height: ${height}`);
482
+ styleParts.push(`objectFit: 'cover'`);
483
+ const styleAttr = ` style={{ ${styleParts.join(", ")} }}`;
484
+ const altAttr = ` alt=${jsString$1(hint)}`;
485
+ const replacement = `<img src={${identifier}}${altAttr}${styleAttr} />`;
486
+ return {
487
+ importSplice,
488
+ elementSplice: {
489
+ from: element.start,
490
+ to: element.end,
491
+ text: replacement
492
+ }
493
+ };
494
+ }
425
495
  function applyEdit(source, line, column, ops) {
426
496
  if (ops.length === 0) return {
427
497
  ok: true,
@@ -458,8 +528,9 @@ function applyEdit(source, line, column, ops) {
458
528
  splices.push(result);
459
529
  }
460
530
  const assetOps = ops.flatMap((op) => op.kind === "set-attr-asset" ? [op] : []);
461
- if (assetOps.length > 0) {
462
- const ast = parseSource(source);
531
+ const placeholderOps = ops.flatMap((op) => op.kind === "replace-placeholder-with-image" ? [op] : []);
532
+ if (assetOps.length > 0 || placeholderOps.length > 0) {
533
+ const ast = parseSource$1(source);
463
534
  if (!ast) return {
464
535
  ok: false,
465
536
  status: 422,
@@ -476,6 +547,16 @@ function applyEdit(source, line, column, ops) {
476
547
  splices.push(plan.attrSplice);
477
548
  if (plan.importSplice) importSplices.push(plan.importSplice);
478
549
  }
550
+ for (const op of placeholderOps) {
551
+ const plan = planReplacePlaceholder(ast, element, op.assetPath);
552
+ if ("error" in plan) return {
553
+ ok: false,
554
+ status: 422,
555
+ error: plan.error
556
+ };
557
+ splices.push(plan.elementSplice);
558
+ if (plan.importSplice) importSplices.push(plan.importSplice);
559
+ }
479
560
  if (importSplices.length > 0) {
480
561
  const from = importSplices[0].from;
481
562
  const to = importSplices[0].to;
@@ -512,38 +593,38 @@ function commentsPlugin(opts) {
512
593
  if (method !== "POST") return next();
513
594
  try {
514
595
  if (url.pathname === "/") {
515
- const body = await readBody$1(req);
596
+ const body = await readBody$2(req);
516
597
  const slideId = body.slideId ?? "";
517
- const file = resolveSlidePath(userCwd, slidesDir, slideId);
518
- if (!file) return json$1(res, 400, { error: "invalid slideId" });
519
- if (!body.line || body.line < 1) return json$1(res, 400, { error: "invalid line" });
520
- if (!Array.isArray(body.ops)) return json$1(res, 400, { error: "missing ops" });
598
+ const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
599
+ if (!file) return json$2(res, 400, { error: "invalid slideId" });
600
+ if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
601
+ if (!Array.isArray(body.ops)) return json$2(res, 400, { error: "missing ops" });
521
602
  let source;
522
603
  try {
523
604
  source = await fs.readFile(file, "utf8");
524
605
  } catch {
525
- return json$1(res, 404, { error: "slide not found" });
606
+ return json$2(res, 404, { error: "slide not found" });
526
607
  }
527
608
  const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
528
- if (!result.ok) return json$1(res, result.status, { error: result.error });
609
+ if (!result.ok) return json$2(res, result.status, { error: result.error });
529
610
  const changed = result.source !== source;
530
611
  if (changed) await fs.writeFile(file, result.source, "utf8");
531
- return json$1(res, 200, {
612
+ return json$2(res, 200, {
532
613
  ok: true,
533
614
  changed
534
615
  });
535
616
  }
536
617
  if (url.pathname === "/batch") {
537
- const body = await readBody$1(req);
618
+ const body = await readBody$2(req);
538
619
  const slideId = body.slideId ?? "";
539
- const file = resolveSlidePath(userCwd, slidesDir, slideId);
540
- if (!file) return json$1(res, 400, { error: "invalid slideId" });
541
- if (!Array.isArray(body.edits)) return json$1(res, 400, { error: "missing edits" });
620
+ const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
621
+ if (!file) return json$2(res, 400, { error: "invalid slideId" });
622
+ if (!Array.isArray(body.edits)) return json$2(res, 400, { error: "missing edits" });
542
623
  let source;
543
624
  try {
544
625
  source = await fs.readFile(file, "utf8");
545
626
  } catch {
546
- return json$1(res, 404, { error: "slide not found" });
627
+ return json$2(res, 404, { error: "slide not found" });
547
628
  }
548
629
  const original = source;
549
630
  const results = [];
@@ -566,7 +647,7 @@ function commentsPlugin(opts) {
566
647
  }
567
648
  const changed = source !== original;
568
649
  if (changed) await fs.writeFile(file, source, "utf8");
569
- return json$1(res, 200, {
650
+ return json$2(res, 200, {
570
651
  ok: true,
571
652
  changed,
572
653
  results
@@ -574,7 +655,7 @@ function commentsPlugin(opts) {
574
655
  }
575
656
  return next();
576
657
  } catch (err) {
577
- json$1(res, 500, { error: String(err.message ?? err) });
658
+ json$2(res, 500, { error: String(err.message ?? err) });
578
659
  }
579
660
  });
580
661
  server.middlewares.use("/__comments", async (req, res, next) => {
@@ -583,31 +664,31 @@ function commentsPlugin(opts) {
583
664
  try {
584
665
  if (method === "GET" && url.pathname === "/") {
585
666
  const slideId = url.searchParams.get("slideId") ?? "";
586
- const file = resolveSlidePath(userCwd, slidesDir, slideId);
587
- if (!file) return json$1(res, 400, { error: "invalid slideId" });
667
+ const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
668
+ if (!file) return json$2(res, 400, { error: "invalid slideId" });
588
669
  let source;
589
670
  try {
590
671
  source = await fs.readFile(file, "utf8");
591
672
  } catch {
592
- return json$1(res, 404, { error: "slide not found" });
673
+ return json$2(res, 404, { error: "slide not found" });
593
674
  }
594
- return json$1(res, 200, { comments: parseMarkers(source) });
675
+ return json$2(res, 200, { comments: parseMarkers(source) });
595
676
  }
596
677
  if (method === "POST" && url.pathname === "/add") {
597
- const body = await readBody$1(req);
678
+ const body = await readBody$2(req);
598
679
  const slideId = body.slideId ?? "";
599
- const file = resolveSlidePath(userCwd, slidesDir, slideId);
600
- if (!file) return json$1(res, 400, { error: "invalid slideId" });
601
- if (!body.line || body.line < 1) return json$1(res, 400, { error: "invalid line" });
602
- if (!body.text || typeof body.text !== "string") return json$1(res, 400, { error: "missing text" });
680
+ const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
681
+ if (!file) return json$2(res, 400, { error: "invalid slideId" });
682
+ if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
683
+ if (!body.text || typeof body.text !== "string") return json$2(res, 400, { error: "missing text" });
603
684
  let source;
604
685
  try {
605
686
  source = await fs.readFile(file, "utf8");
606
687
  } catch {
607
- return json$1(res, 404, { error: "slide not found" });
688
+ return json$2(res, 404, { error: "slide not found" });
608
689
  }
609
690
  const plan = findInsertion(source, body.line, body.column);
610
- if (!plan) return json$1(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
691
+ if (!plan) return json$2(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
611
692
  const id = newId();
612
693
  const ts = new Date().toISOString();
613
694
  const payload = b64urlEncode(JSON.stringify({
@@ -618,32 +699,460 @@ function commentsPlugin(opts) {
618
699
  const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
619
700
  await fs.writeFile(file, next$1, "utf8");
620
701
  const markerLine = offsetToLine(next$1, plan.offset + 1);
621
- return json$1(res, 200, {
702
+ return json$2(res, 200, {
622
703
  id,
623
704
  line: markerLine
624
705
  });
625
706
  }
626
707
  if (method === "DELETE" && url.pathname.startsWith("/")) {
627
708
  const id = url.pathname.slice(1);
628
- if (!/^c-[a-f0-9]+$/.test(id)) return json$1(res, 400, { error: "invalid id" });
709
+ if (!/^c-[a-f0-9]+$/.test(id)) return json$2(res, 400, { error: "invalid id" });
629
710
  const slideId = url.searchParams.get("slideId") ?? "";
630
- const file = resolveSlidePath(userCwd, slidesDir, slideId);
631
- if (!file) return json$1(res, 400, { error: "invalid slideId" });
711
+ const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
712
+ if (!file) return json$2(res, 400, { error: "invalid slideId" });
632
713
  let source;
633
714
  try {
634
715
  source = await fs.readFile(file, "utf8");
635
716
  } catch {
636
- return json$1(res, 404, { error: "slide not found" });
717
+ return json$2(res, 404, { error: "slide not found" });
637
718
  }
638
719
  const lines = source.split("\n");
639
720
  const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
640
721
  const hit = lines.findIndex((l) => idRe.test(l));
641
- if (hit === -1) return json$1(res, 404, { error: "marker not found" });
722
+ if (hit === -1) return json$2(res, 404, { error: "marker not found" });
642
723
  lines.splice(hit, 1);
643
724
  await fs.writeFile(file, lines.join("\n"), "utf8");
644
- return json$1(res, 200, { ok: true });
725
+ return json$2(res, 200, { ok: true });
645
726
  }
646
727
  next();
728
+ } catch (err) {
729
+ json$2(res, 500, { error: String(err.message ?? err) });
730
+ }
731
+ });
732
+ }
733
+ };
734
+ }
735
+
736
+ //#endregion
737
+ //#region src/vite/design-plugin.ts
738
+ const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
739
+ async function readBody$1(req) {
740
+ return await new Promise((resolve, reject) => {
741
+ const chunks = [];
742
+ req.on("data", (c) => chunks.push(c));
743
+ req.on("end", () => {
744
+ const raw = Buffer.concat(chunks).toString("utf8");
745
+ if (!raw) return resolve({});
746
+ try {
747
+ resolve(JSON.parse(raw));
748
+ } catch (e) {
749
+ reject(e);
750
+ }
751
+ });
752
+ req.on("error", reject);
753
+ });
754
+ }
755
+ function json$1(res, status, body) {
756
+ res.statusCode = status;
757
+ res.setHeader("content-type", "application/json");
758
+ res.end(JSON.stringify(body));
759
+ }
760
+ function resolveSlidePath(userCwd, slidesDir, slideId) {
761
+ if (!SLIDE_ID_RE$1.test(slideId)) return null;
762
+ const slidesRoot = path.resolve(userCwd, slidesDir);
763
+ const full = path.resolve(slidesRoot, slideId, "index.tsx");
764
+ if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
765
+ return full;
766
+ }
767
+ function parseSource(source) {
768
+ try {
769
+ return parse(source, {
770
+ sourceType: "module",
771
+ plugins: ["typescript", "jsx"],
772
+ errorRecovery: true
773
+ });
774
+ } catch {
775
+ return null;
776
+ }
777
+ }
778
+ function findDesignDecl(ast) {
779
+ const body = ast.program?.body ?? [];
780
+ for (const node of body) {
781
+ let varDecl = null;
782
+ if (node.type === "VariableDeclaration") varDecl = node;
783
+ else if (node.type === "ExportNamedDeclaration") {
784
+ const decl = node.declaration;
785
+ if (decl?.type === "VariableDeclaration") varDecl = decl;
786
+ }
787
+ if (!varDecl) continue;
788
+ const declarations = varDecl.declarations ?? [];
789
+ for (const d of declarations) {
790
+ const id = d.id;
791
+ if (!id || id.type !== "Identifier" || id.name !== "design") continue;
792
+ const init = d.init;
793
+ if (!init) return null;
794
+ let inner = init;
795
+ if (inner.type === "TSSatisfiesExpression" || inner.type === "TSAsExpression") {
796
+ const expr = inner.expression;
797
+ if (expr) inner = expr;
798
+ }
799
+ if (inner.type !== "ObjectExpression") return null;
800
+ return {
801
+ declStart: node.start,
802
+ declEnd: node.end,
803
+ objectStart: inner.start,
804
+ objectEnd: inner.end
805
+ };
806
+ }
807
+ }
808
+ return null;
809
+ }
810
+ function literalToValue(node) {
811
+ switch (node.type) {
812
+ case "StringLiteral": return node.value;
813
+ case "NumericLiteral": return node.value;
814
+ case "BooleanLiteral": return node.value;
815
+ case "NullLiteral": return null;
816
+ case "UnaryExpression": {
817
+ const op = node.operator;
818
+ const arg = node.argument;
819
+ const v = literalToValue(arg);
820
+ if (op === "-" && typeof v === "number") return -v;
821
+ if (op === "+" && typeof v === "number") return v;
822
+ throw new Error(`unsupported unary operator ${op}`);
823
+ }
824
+ case "TemplateLiteral": {
825
+ const quasis = node.quasis;
826
+ const expressions = node.expressions;
827
+ if (expressions.length > 0) throw new Error("template literal has expressions");
828
+ return quasis[0].value.cooked ?? quasis[0].value.raw;
829
+ }
830
+ case "ArrayExpression": {
831
+ const elements = node.elements;
832
+ return elements.map((el) => {
833
+ if (!el) throw new Error("array has hole");
834
+ return literalToValue(el);
835
+ });
836
+ }
837
+ case "ObjectExpression": {
838
+ const properties = node.properties;
839
+ const out = {};
840
+ for (const prop of properties) {
841
+ if (prop.type !== "ObjectProperty") throw new Error("object has spread or method");
842
+ const p = prop;
843
+ if (p.computed) throw new Error("object has computed key");
844
+ let key;
845
+ if (p.key.type === "Identifier" && typeof p.key.name === "string") key = p.key.name;
846
+ else if (p.key.type === "StringLiteral" && typeof p.key.value === "string") key = p.key.value;
847
+ else throw new Error("unsupported object key");
848
+ out[key] = literalToValue(p.value);
849
+ }
850
+ return out;
851
+ }
852
+ default: throw new Error(`unsupported node type ${node.type}`);
853
+ }
854
+ }
855
+ function isPlainObject(v) {
856
+ return typeof v === "object" && v !== null && !Array.isArray(v);
857
+ }
858
+ function mergeDesign(base, patch) {
859
+ const out = JSON.parse(JSON.stringify(base));
860
+ const apply = (target, src) => {
861
+ for (const [k, v] of Object.entries(src)) if (isPlainObject(v) && isPlainObject(target[k])) apply(target[k], v);
862
+ else target[k] = v;
863
+ };
864
+ if (isPlainObject(patch)) apply(out, patch);
865
+ return out;
866
+ }
867
+ function indent(level) {
868
+ return " ".repeat(level);
869
+ }
870
+ function jsString(s) {
871
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
872
+ }
873
+ function isValidIdentifier(name) {
874
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
875
+ }
876
+ function serializeValue(value, level) {
877
+ if (value === null) return "null";
878
+ if (typeof value === "string") return jsString(value);
879
+ if (typeof value === "number") {
880
+ if (!Number.isFinite(value)) throw new Error("non-finite number");
881
+ return String(value);
882
+ }
883
+ if (typeof value === "boolean") return value ? "true" : "false";
884
+ if (Array.isArray(value)) {
885
+ if (value.length === 0) return "[]";
886
+ const inner = value.map((el) => serializeValue(el, level + 1)).join(", ");
887
+ return `[${inner}]`;
888
+ }
889
+ if (isPlainObject(value)) {
890
+ const entries = Object.entries(value);
891
+ if (entries.length === 0) return "{}";
892
+ const childIndent = indent(level + 1);
893
+ const lines = entries.map(([k, v]) => {
894
+ const key = isValidIdentifier(k) ? k : jsString(k);
895
+ return `${childIndent}${key}: ${serializeValue(v, level + 1)},`;
896
+ });
897
+ return `{\n${lines.join("\n")}\n${indent(level)}}`;
898
+ }
899
+ throw new Error(`unsupported value type ${typeof value}`);
900
+ }
901
+ function serializeDesign(design) {
902
+ return serializeValue(design, 0);
903
+ }
904
+ function parseSlideDesign(source) {
905
+ const ast = parseSource(source);
906
+ if (!ast) return {
907
+ ok: false,
908
+ exists: true,
909
+ error: "could not parse slide source"
910
+ };
911
+ const loc = findDesignDecl(ast);
912
+ if (!loc) return {
913
+ ok: false,
914
+ exists: false
915
+ };
916
+ const objectNode = findDesignObjectNode(ast);
917
+ if (!objectNode) return {
918
+ ok: false,
919
+ exists: true,
920
+ error: "design has unsupported initializer"
921
+ };
922
+ let value;
923
+ try {
924
+ value = literalToValue(objectNode);
925
+ } catch (err) {
926
+ return {
927
+ ok: false,
928
+ exists: true,
929
+ error: err.message
930
+ };
931
+ }
932
+ const merged = mergeDesign(defaultDesign, value);
933
+ return {
934
+ ok: true,
935
+ design: merged,
936
+ loc
937
+ };
938
+ }
939
+ function findDesignObjectNode(ast) {
940
+ const body = ast.program?.body ?? [];
941
+ for (const node of body) {
942
+ let varDecl = null;
943
+ if (node.type === "VariableDeclaration") varDecl = node;
944
+ else if (node.type === "ExportNamedDeclaration") {
945
+ const decl = node.declaration;
946
+ if (decl?.type === "VariableDeclaration") varDecl = decl;
947
+ }
948
+ if (!varDecl) continue;
949
+ const declarations = varDecl.declarations ?? [];
950
+ for (const d of declarations) {
951
+ const id = d.id;
952
+ if (!id || id.type !== "Identifier" || id.name !== "design") continue;
953
+ const init = d.init;
954
+ if (!init) return null;
955
+ let inner = init;
956
+ if (inner.type === "TSSatisfiesExpression" || inner.type === "TSAsExpression") {
957
+ const expr = inner.expression;
958
+ if (expr) inner = expr;
959
+ }
960
+ if (inner.type !== "ObjectExpression") return null;
961
+ return inner;
962
+ }
963
+ }
964
+ return null;
965
+ }
966
+ function findImports(ast) {
967
+ const body = ast.program?.body ?? [];
968
+ const out = [];
969
+ for (const node of body) {
970
+ if (node.type !== "ImportDeclaration") continue;
971
+ const src = node.source?.value;
972
+ if (typeof src !== "string") continue;
973
+ const specs = node.specifiers ?? [];
974
+ out.push({
975
+ node,
976
+ source: src,
977
+ specifiers: specs
978
+ });
979
+ }
980
+ return out;
981
+ }
982
+ function ensureDesignSystemImport(source, ast) {
983
+ const imports = findImports(ast);
984
+ const coreImport = imports.find((imp) => imp.source === "@open-slide/core");
985
+ if (coreImport) {
986
+ const hasDesignSystem = coreImport.specifiers.some((spec) => {
987
+ if (spec.type !== "ImportSpecifier") return false;
988
+ const imported = spec.imported;
989
+ return imported?.name === "DesignSystem";
990
+ });
991
+ if (hasDesignSystem) return {
992
+ source,
993
+ offsetShift: 0
994
+ };
995
+ const node = coreImport.node;
996
+ const importText = source.slice(node.start, node.end);
997
+ const braceClose = importText.lastIndexOf("}");
998
+ if (braceClose === -1) return {
999
+ source,
1000
+ offsetShift: 0
1001
+ };
1002
+ const absoluteBrace = node.start + braceClose;
1003
+ const insertText = coreImport.specifiers.length > 0 ? ", type DesignSystem" : "type DesignSystem";
1004
+ const next$1 = `${source.slice(0, absoluteBrace)}${insertText}${source.slice(absoluteBrace)}`;
1005
+ return {
1006
+ source: next$1,
1007
+ offsetShift: insertText.length
1008
+ };
1009
+ }
1010
+ const stmt = `import type { DesignSystem } from '@open-slide/core';\n`;
1011
+ if (imports.length > 0) {
1012
+ const last = imports[imports.length - 1];
1013
+ const insertAt = last.node.end;
1014
+ const trail = source[insertAt] === "\n" ? "" : "\n";
1015
+ const next$1 = `${source.slice(0, insertAt)}\n${stmt.slice(0, -1)}${trail}${source.slice(insertAt)}`;
1016
+ return {
1017
+ source: next$1,
1018
+ offsetShift: 1 + stmt.length - (trail ? 0 : 1)
1019
+ };
1020
+ }
1021
+ const next = `${stmt}\n${source}`;
1022
+ return {
1023
+ source: next,
1024
+ offsetShift: stmt.length + 1
1025
+ };
1026
+ }
1027
+ function findInsertionPoint(source, ast) {
1028
+ const imports = findImports(ast);
1029
+ if (imports.length === 0) return 0;
1030
+ const last = imports[imports.length - 1];
1031
+ let off = last.node.end;
1032
+ while (off < source.length && source[off] !== "\n") off++;
1033
+ if (off < source.length) off++;
1034
+ return off;
1035
+ }
1036
+ function applyDesignWrite(source, next) {
1037
+ let body;
1038
+ try {
1039
+ body = serializeDesign(next);
1040
+ } catch (err) {
1041
+ return {
1042
+ ok: false,
1043
+ status: 422,
1044
+ error: `serialize failed: ${err.message}`
1045
+ };
1046
+ }
1047
+ const ast = parseSource(source);
1048
+ if (!ast) return {
1049
+ ok: false,
1050
+ status: 422,
1051
+ error: "could not parse slide source"
1052
+ };
1053
+ const loc = findDesignDecl(ast);
1054
+ if (loc) {
1055
+ const out$1 = source.slice(0, loc.objectStart) + body + source.slice(loc.objectEnd);
1056
+ return {
1057
+ ok: true,
1058
+ source: out$1,
1059
+ created: false
1060
+ };
1061
+ }
1062
+ const withImport = ensureDesignSystemImport(source, ast);
1063
+ const ast2 = parseSource(withImport.source);
1064
+ if (!ast2) return {
1065
+ ok: false,
1066
+ status: 422,
1067
+ error: "failed to re-parse after adding import"
1068
+ };
1069
+ const insertAt = findInsertionPoint(withImport.source, ast2);
1070
+ const block = `\nconst design: DesignSystem = ${body};\n`;
1071
+ const out = withImport.source.slice(0, insertAt) + block + withImport.source.slice(insertAt);
1072
+ return {
1073
+ ok: true,
1074
+ source: out,
1075
+ created: true
1076
+ };
1077
+ }
1078
+ function designPlugin(opts) {
1079
+ const userCwd = opts.userCwd;
1080
+ const slidesDir = opts.slidesDir ?? "slides";
1081
+ return {
1082
+ name: "open-slide:design",
1083
+ apply: "serve",
1084
+ configureServer(server) {
1085
+ server.middlewares.use("/__design", async (req, res, next) => {
1086
+ const url = new URL(req.url ?? "/", "http://local");
1087
+ const method = req.method ?? "GET";
1088
+ const slideId = url.searchParams.get("slideId") ?? "";
1089
+ const file = resolveSlidePath(userCwd, slidesDir, slideId);
1090
+ if (!file) return json$1(res, 400, { error: "invalid slideId" });
1091
+ try {
1092
+ if (method === "GET" && url.pathname === "/") {
1093
+ let source;
1094
+ try {
1095
+ source = await fs.readFile(file, "utf8");
1096
+ } catch {
1097
+ return json$1(res, 404, { error: "slide not found" });
1098
+ }
1099
+ const parsed = parseSlideDesign(source);
1100
+ if (parsed.ok) return json$1(res, 200, {
1101
+ design: parsed.design,
1102
+ exists: true,
1103
+ warning: null
1104
+ });
1105
+ if (parsed.exists === false) return json$1(res, 200, {
1106
+ design: defaultDesign,
1107
+ exists: false,
1108
+ warning: null
1109
+ });
1110
+ return json$1(res, 200, {
1111
+ design: defaultDesign,
1112
+ exists: true,
1113
+ warning: parsed.error
1114
+ });
1115
+ }
1116
+ if (method === "PUT" && url.pathname === "/") {
1117
+ const body = await readBody$1(req);
1118
+ const patch = body.patch;
1119
+ if (!patch || typeof patch !== "object") return json$1(res, 400, { error: "missing patch object" });
1120
+ let source;
1121
+ try {
1122
+ source = await fs.readFile(file, "utf8");
1123
+ } catch {
1124
+ return json$1(res, 404, { error: "slide not found" });
1125
+ }
1126
+ const parsed = parseSlideDesign(source);
1127
+ const baseDesign = parsed.ok ? parsed.design : defaultDesign;
1128
+ if (!parsed.ok && parsed.exists) return json$1(res, 422, { error: parsed.error });
1129
+ const merged = mergeDesign(baseDesign, patch);
1130
+ const written = applyDesignWrite(source, merged);
1131
+ if (!written.ok) return json$1(res, written.status, { error: written.error });
1132
+ if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
1133
+ return json$1(res, 200, {
1134
+ ok: true,
1135
+ design: merged,
1136
+ created: written.created
1137
+ });
1138
+ }
1139
+ if (method === "POST" && url.pathname === "/reset") {
1140
+ let source;
1141
+ try {
1142
+ source = await fs.readFile(file, "utf8");
1143
+ } catch {
1144
+ return json$1(res, 404, { error: "slide not found" });
1145
+ }
1146
+ const written = applyDesignWrite(source, defaultDesign);
1147
+ if (!written.ok) return json$1(res, written.status, { error: written.error });
1148
+ if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
1149
+ return json$1(res, 200, {
1150
+ ok: true,
1151
+ design: defaultDesign,
1152
+ created: written.created
1153
+ });
1154
+ }
1155
+ return next();
647
1156
  } catch (err) {
648
1157
  json$1(res, 500, { error: String(err.message ?? err) });
649
1158
  }
@@ -838,10 +1347,10 @@ function updateMetaTitleInSource(source, title) {
838
1347
  return source.slice(0, openBrace + 1) + newBody + source.slice(closeBrace);
839
1348
  }
840
1349
  const firstIndentMatch = body.match(/\n([ \t]+)\S/);
841
- const indent = firstIndentMatch ? firstIndentMatch[1] : " ";
1350
+ const indent$1 = firstIndentMatch ? firstIndentMatch[1] : " ";
842
1351
  const trimmedBody = body.replace(/^\s*\n?/, "");
843
1352
  const needsSeparator = trimmedBody.trim().length > 0;
844
- const insertion$1 = `\n${indent}title: ${newLiteral}${needsSeparator ? "," : ""}`;
1353
+ const insertion$1 = `\n${indent$1}title: ${newLiteral}${needsSeparator ? "," : ""}`;
845
1354
  return source.slice(0, openBrace + 1) + insertion$1 + body + source.slice(closeBrace);
846
1355
  }
847
1356
  const exportDefaultIdx = source.search(/export\s+default\b/);
@@ -1192,10 +1701,12 @@ function filesPlugin(opts) {
1192
1701
 
1193
1702
  //#endregion
1194
1703
  //#region src/vite/loc-tags-plugin.ts
1195
- function isHostJsxName(name) {
1704
+ const FORWARDING_COMPONENTS = new Set(["ImagePlaceholder"]);
1705
+ function isTaggableJsxName(name) {
1196
1706
  if (!name || typeof name !== "object") return false;
1197
1707
  const n = name;
1198
- return n.type === "JSXIdentifier" && typeof n.name === "string" && /^[a-z]/.test(n.name);
1708
+ if (n.type !== "JSXIdentifier" || typeof n.name !== "string") return false;
1709
+ return /^[a-z]/.test(n.name) || FORWARDING_COMPONENTS.has(n.name);
1199
1710
  }
1200
1711
  function alreadyTagged(opening) {
1201
1712
  const attrs = opening.attributes ?? [];
@@ -1223,7 +1734,7 @@ function injectLocTags(code) {
1223
1734
  const opening = node.openingElement;
1224
1735
  if (!opening) return;
1225
1736
  const name = opening.name;
1226
- if (!isHostJsxName(name)) return;
1737
+ if (!isTaggableJsxName(name)) return;
1227
1738
  if (alreadyTagged(opening)) return;
1228
1739
  const loc = node.loc;
1229
1740
  if (!loc) return;
@@ -1366,21 +1877,38 @@ function openSlidePlugin(opts) {
1366
1877
  return null;
1367
1878
  },
1368
1879
  configureServer(server) {
1880
+ const isSlideEntry = (p) => {
1881
+ const rel = path.relative(slidesRoot, p);
1882
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
1883
+ const parts = rel.split(path.sep);
1884
+ if (parts.length !== 2) return false;
1885
+ return /^index\.(tsx|jsx|ts|js)$/.test(parts[1]);
1886
+ };
1887
+ let reloadTimer = null;
1369
1888
  const reload = () => {
1370
- const mod = server.moduleGraph.getModuleById(resolved(SLIDES_VMOD));
1371
- if (mod) server.moduleGraph.invalidateModule(mod);
1372
- server.ws.send({ type: "full-reload" });
1889
+ if (reloadTimer) clearTimeout(reloadTimer);
1890
+ reloadTimer = setTimeout(() => {
1891
+ reloadTimer = null;
1892
+ const mod = server.moduleGraph.getModuleById(resolved(SLIDES_VMOD));
1893
+ if (mod) server.moduleGraph.invalidateModule(mod);
1894
+ server.ws.send({ type: "full-reload" });
1895
+ }, 150);
1373
1896
  };
1374
- server.watcher.add(path.join(slidesRoot, "*"));
1897
+ server.watcher.add(path.join(slidesRoot, "*/index.{tsx,jsx,ts,js}"));
1375
1898
  server.watcher.on("add", (p) => {
1376
- if (p.startsWith(slidesRoot)) reload();
1899
+ if (isSlideEntry(p)) reload();
1377
1900
  });
1378
1901
  server.watcher.on("unlink", (p) => {
1379
- if (p.startsWith(slidesRoot)) reload();
1902
+ if (isSlideEntry(p)) reload();
1380
1903
  });
1904
+ let foldersTimer = null;
1381
1905
  const invalidateFolders = () => {
1382
- const mod = server.moduleGraph.getModuleById(resolved(FOLDERS_VMOD));
1383
- if (mod) server.moduleGraph.invalidateModule(mod);
1906
+ if (foldersTimer) clearTimeout(foldersTimer);
1907
+ foldersTimer = setTimeout(() => {
1908
+ foldersTimer = null;
1909
+ const mod = server.moduleGraph.getModuleById(resolved(FOLDERS_VMOD));
1910
+ if (mod) server.moduleGraph.invalidateModule(mod);
1911
+ }, 100);
1384
1912
  };
1385
1913
  server.watcher.add(foldersManifestPath);
1386
1914
  server.watcher.on("change", (p) => {
@@ -1436,6 +1964,7 @@ async function createViteConfig(opts) {
1436
1964
  userCwd,
1437
1965
  config
1438
1966
  }),
1967
+ designPlugin({ userCwd }),
1439
1968
  commentsPlugin({
1440
1969
  userCwd,
1441
1970
  slidesDir