@bendyline/squisq-editor-react 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.d.ts +75 -33
  2. package/dist/index.js +1717 -935
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles/index.css +145 -5
  5. package/package.json +4 -4
  6. package/src/DocumentSettingsDialog.tsx +32 -18
  7. package/src/EditorShell.tsx +1 -1
  8. package/src/PreviewControls.tsx +57 -24
  9. package/src/RecorderEntry.tsx +2 -0
  10. package/src/Toolbar.tsx +164 -19
  11. package/src/__tests__/codeContextSectionView.test.tsx +8 -6
  12. package/src/__tests__/documentSettingsDialog.test.tsx +22 -0
  13. package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
  14. package/src/__tests__/previewControls.test.tsx +164 -0
  15. package/src/__tests__/recorderTheme.test.tsx +42 -0
  16. package/src/__tests__/selectionConversions.test.ts +80 -0
  17. package/src/__tests__/tiptapBridge.test.ts +48 -7
  18. package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
  19. package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
  20. package/src/asciiDiagram/AsciiDiagramWidget.tsx +34 -4
  21. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +58 -1
  22. package/src/asciiDiagram/asciiDiagramCommands.ts +36 -10
  23. package/src/asciiDiagram/asciiDiagramData.ts +33 -0
  24. package/src/asciiDiagram/asciiDiagramOps.ts +19 -0
  25. package/src/codeContext/types.ts +1 -1
  26. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
  27. package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
  28. package/src/diagram/DiagramCanvas.tsx +16 -2
  29. package/src/frontmatterSettings.ts +23 -0
  30. package/src/index.ts +7 -1
  31. package/src/recorder/RecorderButton.tsx +9 -1
  32. package/src/recorder/RecorderModal.tsx +84 -41
  33. package/src/recorder/RecorderPanel.tsx +9 -1
  34. package/src/scene/__tests__/sceneIsolation.test.tsx +27 -1
  35. package/src/scene/adapters/DrawingAdapter.ts +6 -1
  36. package/src/scene/adapters/LayoutAdapter.ts +3 -0
  37. package/src/scene/commands/SceneCommand.ts +13 -2
  38. package/src/scene/tools/SelectTool.ts +5 -5
  39. package/src/selectionConversions.ts +155 -0
  40. package/src/styles/ascii-timeline.css +101 -4
  41. package/src/styles/editor.css +30 -0
  42. package/src/styles/tree-view.css +51 -1
  43. package/src/timeline/TimelineEditorWidget.tsx +200 -41
  44. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +61 -2
  45. package/src/timeline/__tests__/timelineCommands.test.ts +32 -0
  46. package/src/timeline/__tests__/timelineOps.test.ts +55 -0
  47. package/src/timeline/timelineCommands.ts +54 -0
  48. package/src/timeline/timelineOps.ts +107 -3
  49. package/src/tiptapBridge.ts +23 -5
  50. package/src/treeview/TreeOutlineWidget.tsx +153 -3
  51. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
  52. package/src/treeview/__tests__/treeOps.test.ts +52 -0
  53. package/src/treeview/__tests__/treeViewCommands.test.ts +16 -0
  54. package/src/treeview/treeOps.ts +59 -0
  55. package/src/treeview/treeViewCommands.ts +5 -0
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/EditorShell.tsx
2
- import { useEffect as useEffect61, useRef as useRef60, useState as useState70, useCallback as useCallback56, useMemo as useMemo47 } from "react";
2
+ import { useEffect as useEffect61, useRef as useRef61, useState as useState70, useCallback as useCallback56, useMemo as useMemo47 } from "react";
3
3
 
4
4
  // src/EditorContext.tsx
5
5
  import {
@@ -204,7 +204,14 @@ function markdownToTiptap(markdown) {
204
204
  }
205
205
  if (line.startsWith("> ")) {
206
206
  flushList();
207
- pushBlock(`<blockquote><p>${inlineToHtml(line.slice(2))}</p></blockquote>`);
207
+ const quoteLines = [line.slice(2)];
208
+ while (i + 1 < lines.length && lines[i + 1].startsWith("> ")) {
209
+ i++;
210
+ quoteLines.push(lines[i].slice(2));
211
+ }
212
+ pushBlock(
213
+ `<blockquote>${quoteLines.map((quoteLine) => `<p>${inlineToHtml(quoteLine)}</p>`).join("")}</blockquote>`
214
+ );
208
215
  continue;
209
216
  }
210
217
  const taskMatch = line.match(/^[-*+]\s+\[([xX ])\]\s*(.*)$/);
@@ -329,10 +336,15 @@ function tiptapToMarkdown(html) {
329
336
  }
330
337
  const bqMatch = remaining.match(/^<blockquote>(.*?)<\/blockquote>/s);
331
338
  if (bqMatch) {
332
- const inner = htmlToInline(bqMatch[1].replace(/<\/?p>/g, ""));
333
- lines.push("> " + inner);
334
- lines.push("");
335
- remaining = remaining.slice(bqMatch[0].length);
339
+ const paragraphs = bqMatch[1].split(/<\/p>\s*<p[^>]*>/i).map((paragraph) => paragraph.replace(/^<p[^>]*>/i, "").replace(/<\/p>\s*$/i, ""));
340
+ for (const paragraph of paragraphs) {
341
+ for (const quoteLine of htmlToInline(paragraph).split("\n")) {
342
+ lines.push("> " + quoteLine);
343
+ }
344
+ }
345
+ const next = remaining.slice(bqMatch[0].length);
346
+ if (!/^\s*<blockquote>/.test(next)) lines.push("");
347
+ remaining = next;
336
348
  continue;
337
349
  }
338
350
  if (remaining.startsWith("<hr>") || remaining.startsWith("<hr/>") || remaining.startsWith("<hr />")) {
@@ -346,11 +358,11 @@ function tiptapToMarkdown(html) {
346
358
  /^<div[^>]*class="[^"]*tableWrapper[^"]*"[^>]*><table[^>]*>(.*?)<\/table>\s*<\/div>/s
347
359
  ) || remaining.match(/^<table[^>]*>(.*?)<\/table>/s);
348
360
  if (tableMatch) {
349
- const tableContent = tableMatch[1];
361
+ const tableContent2 = tableMatch[1];
350
362
  const rows = [];
351
363
  const rowRegex = /<tr[^>]*>(.*?)<\/tr>/gs;
352
364
  let rowExec;
353
- while ((rowExec = rowRegex.exec(tableContent)) !== null) {
365
+ while ((rowExec = rowRegex.exec(tableContent2)) !== null) {
354
366
  const rowHtml = rowExec[1];
355
367
  const cells = [];
356
368
  const cellRegex = /<(th|td)([^>]*)>(.*?)<\/\1>/gs;
@@ -2357,9 +2369,27 @@ var overlayStyle = {
2357
2369
  justifyContent: "center",
2358
2370
  zIndex: 1e4
2359
2371
  };
2372
+ function recorderThemeStyle(colorScheme) {
2373
+ const dark = colorScheme === "dark";
2374
+ return {
2375
+ colorScheme,
2376
+ "--squisq-recorder-surface": `var(--squisq-bg, ${dark ? "#1f2937" : "#fffdf7"})`,
2377
+ "--squisq-recorder-input": `var(--squisq-input-bg, ${dark ? "#374151" : "#fff"})`,
2378
+ "--squisq-recorder-border": `var(--squisq-border, ${dark ? "#4b5563" : "#c9b98a"})`,
2379
+ "--squisq-recorder-text": `var(--squisq-text, ${dark ? "#e5e7eb" : "#4a3c1f"})`,
2380
+ "--squisq-recorder-muted": `var(--squisq-text-muted, ${dark ? "#9ca3af" : "#5a4a2a"})`,
2381
+ "--squisq-recorder-accent": "var(--squisq-accent, #8b6914)",
2382
+ "--squisq-recorder-accent-text": "#fff",
2383
+ "--squisq-recorder-danger": dark ? "#dc4c4c" : "#b33a3a",
2384
+ "--squisq-recorder-danger-border": dark ? "#ef6a6a" : "#902929",
2385
+ "--squisq-recorder-error-bg": dark ? "#3f151b" : "#fceeee",
2386
+ "--squisq-recorder-error-border": dark ? "#7f1d1d" : "#d88a8a",
2387
+ "--squisq-recorder-error-text": dark ? "#fecdd3" : "#8c2a2a"
2388
+ };
2389
+ }
2360
2390
  var modalStyle = {
2361
- background: "#FFFDF7",
2362
- border: "1px solid #c9b98a",
2391
+ background: "var(--squisq-recorder-surface)",
2392
+ border: "1px solid var(--squisq-recorder-border)",
2363
2393
  borderRadius: 0,
2364
2394
  padding: "24px 28px",
2365
2395
  width: "min(560px, calc(100vw - 48px))",
@@ -2367,30 +2397,30 @@ var modalStyle = {
2367
2397
  overflowY: "auto",
2368
2398
  boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
2369
2399
  fontFamily: "system-ui, -apple-system, sans-serif",
2370
- color: "#4a3c1f"
2400
+ color: "var(--squisq-recorder-text)"
2371
2401
  };
2372
2402
  var titleStyle = {
2373
2403
  margin: "0 0 16px 0",
2374
2404
  fontSize: 18,
2375
2405
  fontWeight: 600,
2376
- color: "#2d2310"
2406
+ color: "var(--squisq-recorder-text)"
2377
2407
  };
2378
2408
  var labelStyle = {
2379
2409
  display: "block",
2380
2410
  fontSize: 13,
2381
2411
  fontWeight: 500,
2382
2412
  marginBottom: 4,
2383
- color: "#5a4a2a"
2413
+ color: "var(--squisq-recorder-text)"
2384
2414
  };
2385
2415
  var inputStyle = {
2386
2416
  width: "100%",
2387
2417
  padding: "6px 8px",
2388
2418
  fontSize: 13,
2389
2419
  fontFamily: "inherit",
2390
- border: "1px solid #c9b98a",
2420
+ border: "1px solid var(--squisq-recorder-border)",
2391
2421
  borderRadius: 0,
2392
- background: "#fff",
2393
- color: "#4a3c1f",
2422
+ background: "var(--squisq-recorder-input)",
2423
+ color: "var(--squisq-recorder-text)",
2394
2424
  marginBottom: 12,
2395
2425
  boxSizing: "border-box"
2396
2426
  };
@@ -2405,9 +2435,9 @@ var btnPrimary = {
2405
2435
  fontFamily: "inherit",
2406
2436
  fontWeight: 500,
2407
2437
  cursor: "pointer",
2408
- background: "#8B6914",
2409
- color: "#fff",
2410
- border: "1px solid #7a5c10",
2438
+ background: "var(--squisq-recorder-accent)",
2439
+ color: "var(--squisq-recorder-accent-text)",
2440
+ border: "1px solid var(--squisq-recorder-accent)",
2411
2441
  borderRadius: 0
2412
2442
  };
2413
2443
  var btnSecondary = {
@@ -2416,15 +2446,15 @@ var btnSecondary = {
2416
2446
  fontFamily: "inherit",
2417
2447
  fontWeight: 500,
2418
2448
  cursor: "pointer",
2419
- background: "#E8DFC6",
2420
- color: "#4a3c1f",
2421
- border: "1px solid #c9b98a",
2449
+ background: "var(--squisq-recorder-input)",
2450
+ color: "var(--squisq-recorder-text)",
2451
+ border: "1px solid var(--squisq-recorder-border)",
2422
2452
  borderRadius: 0
2423
2453
  };
2424
2454
  var btnDanger = {
2425
2455
  ...btnPrimary,
2426
- background: "#B33A3A",
2427
- borderColor: "#902929"
2456
+ background: "var(--squisq-recorder-danger)",
2457
+ borderColor: "var(--squisq-recorder-danger-border)"
2428
2458
  };
2429
2459
  var toggleRowStyle = {
2430
2460
  display: "flex",
@@ -2437,16 +2467,16 @@ var toggleBase = {
2437
2467
  fontFamily: "inherit",
2438
2468
  cursor: "pointer",
2439
2469
  background: "transparent",
2440
- color: "#5a4a2a",
2441
- border: "1px solid #c9b98a",
2470
+ color: "var(--squisq-recorder-text)",
2471
+ border: "1px solid var(--squisq-recorder-border)",
2442
2472
  borderRadius: 999
2443
2473
  };
2444
2474
  var toggleActive = {
2445
2475
  ...toggleBase,
2446
- color: "#fffdf5",
2476
+ color: "var(--squisq-recorder-accent-text)",
2447
2477
  fontWeight: 600,
2448
- background: "#8B6914",
2449
- borderColor: "#8B6914"
2478
+ background: "var(--squisq-recorder-accent)",
2479
+ borderColor: "var(--squisq-recorder-accent)"
2450
2480
  };
2451
2481
  var previewBoxStyle = {
2452
2482
  width: "100%",
@@ -2464,20 +2494,20 @@ var previewBoxStyle = {
2464
2494
  var audioMeterStyle = {
2465
2495
  width: "100%",
2466
2496
  height: 56,
2467
- background: "#F2EBD9",
2468
- border: "1px solid #c9b98a",
2497
+ background: "var(--squisq-recorder-input)",
2498
+ border: "1px solid var(--squisq-recorder-border)",
2469
2499
  marginBottom: 12,
2470
2500
  display: "flex",
2471
2501
  alignItems: "center",
2472
2502
  justifyContent: "center",
2473
- color: "#5a4a2a",
2503
+ color: "var(--squisq-recorder-muted)",
2474
2504
  fontSize: 13,
2475
2505
  fontVariantNumeric: "tabular-nums"
2476
2506
  };
2477
2507
  var errorStyle = {
2478
- background: "#FCEEEE",
2479
- border: "1px solid #D88A8A",
2480
- color: "#8C2A2A",
2508
+ background: "var(--squisq-recorder-error-bg)",
2509
+ border: "1px solid var(--squisq-recorder-error-border)",
2510
+ color: "var(--squisq-recorder-error-text)",
2481
2511
  padding: "8px 10px",
2482
2512
  fontSize: 13,
2483
2513
  marginBottom: 12
@@ -2488,6 +2518,18 @@ var buttonRowStyle = {
2488
2518
  justifyContent: "flex-end",
2489
2519
  marginTop: 8
2490
2520
  };
2521
+ var summaryStyle = {
2522
+ margin: "0 0 12px 0",
2523
+ fontSize: 12,
2524
+ color: "var(--squisq-recorder-muted)"
2525
+ };
2526
+ var recordingStatusStyle = {
2527
+ fontSize: 13,
2528
+ fontVariantNumeric: "tabular-nums",
2529
+ marginBottom: 12,
2530
+ color: "var(--squisq-recorder-accent)",
2531
+ fontWeight: 600
2532
+ };
2491
2533
  function formatDurationMs(ms) {
2492
2534
  const totalSec = Math.floor(ms / 1e3);
2493
2535
  const m = Math.floor(totalSec / 60);
@@ -2529,6 +2571,7 @@ function RecorderModal({
2529
2571
  mediaProvider,
2530
2572
  container = null,
2531
2573
  initialMode = "mic",
2574
+ colorScheme = "light",
2532
2575
  onClose,
2533
2576
  onSave
2534
2577
  }) {
@@ -2663,144 +2706,144 @@ function RecorderModal({
2663
2706
  setVideo((v) => v === key ? "none" : key);
2664
2707
  }
2665
2708
  };
2666
- return /* @__PURE__ */ jsx4("div", { style: overlayStyle, role: "dialog", "aria-modal": "true", "aria-label": "Record media", children: /* @__PURE__ */ jsxs2("div", { style: modalStyle, onClick: (e2) => e2.stopPropagation(), children: [
2667
- /* @__PURE__ */ jsx4("h2", { style: titleStyle, children: "Record media" }),
2668
- /* @__PURE__ */ jsx4("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLES.map((t) => {
2669
- const active = toggleActiveFor(t.key);
2670
- return /* @__PURE__ */ jsx4(
2671
- "button",
2672
- {
2673
- type: "button",
2674
- "aria-pressed": active,
2675
- style: active ? toggleActive : toggleBase,
2676
- onClick: () => onToggle(t.key),
2677
- disabled: togglesLocked,
2678
- children: t.label
2679
- },
2680
- t.key
2681
- );
2682
- }) }),
2683
- /* @__PURE__ */ jsx4("p", { style: { margin: "0 0 12px 0", fontSize: 12, color: "#5a4a2a" }, children: captureSummary(micOn, video) }),
2684
- recorder.error && /* @__PURE__ */ jsx4("div", { style: errorStyle, children: recorder.error.message }),
2685
- saveError && /* @__PURE__ */ jsx4("div", { style: errorStyle, children: saveError }),
2686
- !showPreview && /* @__PURE__ */ jsx4("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx4("span", { children: "Click Start Preview to start a recording." }) }),
2687
- showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsx4("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx4(
2688
- "video",
2689
- {
2690
- ref: previewRef,
2691
- autoPlay: true,
2692
- muted: true,
2693
- playsInline: true,
2694
- style: { width: "100%", height: "100%", objectFit: "contain" }
2695
- }
2696
- ) }),
2697
- showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx4("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs2(Fragment, { children: [
2698
- "\u25CF Recording ",
2699
- formatDurationMs(recorder.durationMs)
2700
- ] }) : /* @__PURE__ */ jsx4(Fragment, { children: "Microphone ready" }) }),
2701
- recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsx4("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx4(
2702
- "video",
2703
- {
2704
- src: playbackUrl,
2705
- controls: true,
2706
- playsInline: true,
2707
- style: { width: "100%", height: "100%", objectFit: "contain" }
2708
- }
2709
- ) }),
2710
- recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 12 }, children: [
2711
- /* @__PURE__ */ jsxs2("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
2712
- "\u2713 Recorded ",
2713
- formatDurationMs(recorder.durationMs)
2714
- ] }),
2715
- /* @__PURE__ */ jsx4("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
2716
- ] }),
2717
- source === "mic" && /* @__PURE__ */ jsxs2(Fragment, { children: [
2718
- /* @__PURE__ */ jsx4("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
2719
- /* @__PURE__ */ jsx4(
2720
- "textarea",
2721
- {
2722
- id: "recorder-source-text",
2723
- style: textareaStyle,
2724
- placeholder: "Type the text you're going to read aloud.",
2725
- value: sourceText,
2726
- onChange: (e2) => setSourceText(e2.target.value),
2727
- disabled: recorder.state === "recording"
2728
- }
2729
- )
2730
- ] }),
2731
- video === "screen" && /* @__PURE__ */ jsxs2(
2732
- "label",
2733
- {
2734
- style: {
2735
- display: "flex",
2736
- alignItems: "center",
2737
- gap: 6,
2738
- marginBottom: 12,
2739
- fontSize: 13
2740
- },
2741
- children: [
2709
+ return /* @__PURE__ */ jsx4(
2710
+ "div",
2711
+ {
2712
+ className: "squisq-editor-shell squisq-recorder-overlay",
2713
+ "data-theme": colorScheme,
2714
+ style: { ...overlayStyle, ...recorderThemeStyle(colorScheme) },
2715
+ role: "dialog",
2716
+ "aria-modal": "true",
2717
+ "aria-label": "Record media",
2718
+ children: /* @__PURE__ */ jsxs2("div", { style: modalStyle, onClick: (e2) => e2.stopPropagation(), children: [
2719
+ /* @__PURE__ */ jsx4("h2", { style: titleStyle, children: "Record media" }),
2720
+ /* @__PURE__ */ jsx4("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLES.map((t) => {
2721
+ const active = toggleActiveFor(t.key);
2722
+ return /* @__PURE__ */ jsx4(
2723
+ "button",
2724
+ {
2725
+ type: "button",
2726
+ "aria-pressed": active,
2727
+ style: active ? toggleActive : toggleBase,
2728
+ onClick: () => onToggle(t.key),
2729
+ disabled: togglesLocked,
2730
+ children: t.label
2731
+ },
2732
+ t.key
2733
+ );
2734
+ }) }),
2735
+ /* @__PURE__ */ jsx4("p", { style: summaryStyle, children: captureSummary(micOn, video) }),
2736
+ recorder.error && /* @__PURE__ */ jsx4("div", { style: errorStyle, children: recorder.error.message }),
2737
+ saveError && /* @__PURE__ */ jsx4("div", { style: errorStyle, children: saveError }),
2738
+ !showPreview && /* @__PURE__ */ jsx4("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx4("span", { children: "Click Start Preview to start a recording." }) }),
2739
+ showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsx4("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx4(
2740
+ "video",
2741
+ {
2742
+ ref: previewRef,
2743
+ autoPlay: true,
2744
+ muted: true,
2745
+ playsInline: true,
2746
+ style: { width: "100%", height: "100%", objectFit: "contain" }
2747
+ }
2748
+ ) }),
2749
+ showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx4("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs2(Fragment, { children: [
2750
+ "\u25CF Recording ",
2751
+ formatDurationMs(recorder.durationMs)
2752
+ ] }) : /* @__PURE__ */ jsx4(Fragment, { children: "Microphone ready" }) }),
2753
+ recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsx4("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx4(
2754
+ "video",
2755
+ {
2756
+ src: playbackUrl,
2757
+ controls: true,
2758
+ playsInline: true,
2759
+ style: { width: "100%", height: "100%", objectFit: "contain" }
2760
+ }
2761
+ ) }),
2762
+ recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 12 }, children: [
2763
+ /* @__PURE__ */ jsxs2("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
2764
+ "\u2713 Recorded ",
2765
+ formatDurationMs(recorder.durationMs)
2766
+ ] }),
2767
+ /* @__PURE__ */ jsx4("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
2768
+ ] }),
2769
+ source === "mic" && /* @__PURE__ */ jsxs2(Fragment, { children: [
2770
+ /* @__PURE__ */ jsx4("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
2742
2771
  /* @__PURE__ */ jsx4(
2743
- "input",
2772
+ "textarea",
2744
2773
  {
2745
- type: "checkbox",
2746
- checked: includeSystemAudio,
2747
- onChange: (e2) => setIncludeSystemAudio(e2.target.checked),
2748
- disabled: recorder.state === "recording" || recorder.state === "requesting"
2774
+ id: "recorder-source-text",
2775
+ style: textareaStyle,
2776
+ placeholder: "Type the text you're going to read aloud.",
2777
+ value: sourceText,
2778
+ onChange: (e2) => setSourceText(e2.target.value),
2779
+ disabled: recorder.state === "recording"
2749
2780
  }
2750
- ),
2751
- "Include system audio (Chrome only)"
2752
- ]
2753
- }
2754
- ),
2755
- /* @__PURE__ */ jsx4("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
2756
- /* @__PURE__ */ jsx4(
2757
- "input",
2758
- {
2759
- id: "recorder-basename",
2760
- type: "text",
2761
- style: inputStyle,
2762
- placeholder: source === "mic" ? "narration" : "recording",
2763
- value: basename,
2764
- onChange: (e2) => setBasename(e2.target.value),
2765
- disabled: recorder.state === "recording"
2766
- }
2767
- ),
2768
- recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs2(
2769
- "div",
2770
- {
2771
- style: {
2772
- fontSize: 13,
2773
- fontVariantNumeric: "tabular-nums",
2774
- marginBottom: 12,
2775
- color: "#8B6914",
2776
- fontWeight: 600
2777
- },
2778
- children: [
2781
+ )
2782
+ ] }),
2783
+ video === "screen" && /* @__PURE__ */ jsxs2(
2784
+ "label",
2785
+ {
2786
+ style: {
2787
+ display: "flex",
2788
+ alignItems: "center",
2789
+ gap: 6,
2790
+ marginBottom: 12,
2791
+ fontSize: 13
2792
+ },
2793
+ children: [
2794
+ /* @__PURE__ */ jsx4(
2795
+ "input",
2796
+ {
2797
+ type: "checkbox",
2798
+ style: { accentColor: "var(--squisq-recorder-accent)" },
2799
+ checked: includeSystemAudio,
2800
+ onChange: (e2) => setIncludeSystemAudio(e2.target.checked),
2801
+ disabled: recorder.state === "recording" || recorder.state === "requesting"
2802
+ }
2803
+ ),
2804
+ "Include system audio (Chrome only)"
2805
+ ]
2806
+ }
2807
+ ),
2808
+ /* @__PURE__ */ jsx4("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
2809
+ /* @__PURE__ */ jsx4(
2810
+ "input",
2811
+ {
2812
+ id: "recorder-basename",
2813
+ type: "text",
2814
+ style: inputStyle,
2815
+ placeholder: source === "mic" ? "narration" : "recording",
2816
+ value: basename,
2817
+ onChange: (e2) => setBasename(e2.target.value),
2818
+ disabled: recorder.state === "recording"
2819
+ }
2820
+ ),
2821
+ recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs2("div", { style: recordingStatusStyle, children: [
2779
2822
  "\u25CF Recording ",
2780
2823
  formatDurationMs(recorder.durationMs)
2781
- ]
2782
- }
2783
- ),
2784
- /* @__PURE__ */ jsxs2("div", { style: buttonRowStyle, children: [
2785
- /* @__PURE__ */ jsx4("button", { type: "button", style: btnSecondary, onClick: handleClose, disabled: isBusy, children: "Close" }),
2786
- (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx4(
2787
- "button",
2788
- {
2789
- type: "button",
2790
- style: btnPrimary,
2791
- onClick: handleRequest,
2792
- disabled: isBusy || !canCapture,
2793
- children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
2794
- }
2795
- ),
2796
- canRecord && /* @__PURE__ */ jsx4("button", { type: "button", style: btnPrimary, onClick: handleStart, disabled: isBusy, children: "Record" }),
2797
- canStop && /* @__PURE__ */ jsx4("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
2798
- canSave && /* @__PURE__ */ jsxs2(Fragment, { children: [
2799
- /* @__PURE__ */ jsx4("button", { type: "button", style: btnSecondary, onClick: handleDiscard, disabled: isBusy, children: "Discard & re-record" }),
2800
- /* @__PURE__ */ jsx4("button", { type: "button", style: btnPrimary, onClick: handleSave, disabled: isBusy, children: isSaving ? "Saving\u2026" : "Save to document" })
2824
+ ] }),
2825
+ /* @__PURE__ */ jsxs2("div", { style: buttonRowStyle, children: [
2826
+ /* @__PURE__ */ jsx4("button", { type: "button", style: btnSecondary, onClick: handleClose, disabled: isBusy, children: "Close" }),
2827
+ (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx4(
2828
+ "button",
2829
+ {
2830
+ type: "button",
2831
+ style: btnPrimary,
2832
+ onClick: handleRequest,
2833
+ disabled: isBusy || !canCapture,
2834
+ children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
2835
+ }
2836
+ ),
2837
+ canRecord && /* @__PURE__ */ jsx4("button", { type: "button", style: btnPrimary, onClick: handleStart, disabled: isBusy, children: "Record" }),
2838
+ canStop && /* @__PURE__ */ jsx4("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
2839
+ canSave && /* @__PURE__ */ jsxs2(Fragment, { children: [
2840
+ /* @__PURE__ */ jsx4("button", { type: "button", style: btnSecondary, onClick: handleDiscard, disabled: isBusy, children: "Discard & re-record" }),
2841
+ /* @__PURE__ */ jsx4("button", { type: "button", style: btnPrimary, onClick: handleSave, disabled: isBusy, children: isSaving ? "Saving\u2026" : "Save to document" })
2842
+ ] })
2843
+ ] })
2801
2844
  ] })
2802
- ] })
2803
- ] }) });
2845
+ }
2846
+ );
2804
2847
  }
2805
2848
 
2806
2849
  // src/recorder/RecorderPanel.tsx
@@ -2809,6 +2852,7 @@ function RecorderPanel({
2809
2852
  mediaProvider,
2810
2853
  container = null,
2811
2854
  initialMode = "mic",
2855
+ colorScheme = "light",
2812
2856
  onSave,
2813
2857
  tooltip = "Record media",
2814
2858
  className
@@ -2835,6 +2879,7 @@ function RecorderPanel({
2835
2879
  mediaProvider,
2836
2880
  container,
2837
2881
  initialMode,
2882
+ colorScheme,
2838
2883
  onClose: handleClose,
2839
2884
  onSave: (result) => {
2840
2885
  onSave?.(result);
@@ -2892,7 +2937,8 @@ function RecorderEntry() {
2892
2937
  insertAtCursor,
2893
2938
  bumpMediaRevision,
2894
2939
  markdownSource,
2895
- setMarkdownSource
2940
+ setMarkdownSource,
2941
+ colorScheme
2896
2942
  } = useEditorContext();
2897
2943
  const handleSave = useCallback7(
2898
2944
  (result) => {
@@ -2953,6 +2999,7 @@ ${videoTag}` : videoTag);
2953
2999
  {
2954
3000
  mediaProvider,
2955
3001
  container: workspaceContainer,
3002
+ colorScheme,
2956
3003
  onSave: handleSave,
2957
3004
  className: "squisq-toolbar-button"
2958
3005
  }
@@ -5812,13 +5859,25 @@ function resolvePersistedTransformStyleId(value) {
5812
5859
  return VALID_TRANSFORM_IDS.has(normalized) ? normalized : null;
5813
5860
  }
5814
5861
 
5815
- // src/DocumentSettingsDialog.tsx
5816
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
5817
- var FM = {
5862
+ // src/frontmatterSettings.ts
5863
+ var FRONTMATTER_SETTING_KEYS = {
5818
5864
  theme: { canonical: "squisq-theme", legacy: ["themeId", "theme"] },
5819
5865
  transform: { canonical: "squisq-transform", legacy: "transform-style" },
5820
- captions: { canonical: "squisq-captions", legacy: "caption-style" }
5866
+ captions: { canonical: "squisq-captions", legacy: "caption-style" },
5867
+ coverSlide: { canonical: "squisq-cover-slide", legacy: "cover-slide" }
5821
5868
  };
5869
+ var FRONTMATTER_SETTING_DEFAULTS = {
5870
+ theme: "standard",
5871
+ transform: "",
5872
+ captions: "standard",
5873
+ coverSlide: true
5874
+ };
5875
+ function omitFrontmatterDefault(value, defaultValue) {
5876
+ return value === defaultValue ? null : value;
5877
+ }
5878
+
5879
+ // src/DocumentSettingsDialog.tsx
5880
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
5822
5881
  function readFm(fm, canonical, legacy) {
5823
5882
  if (!fm) return "";
5824
5883
  const v = Object.prototype.hasOwnProperty.call(fm, canonical) ? fm[canonical] : fm[legacy];
@@ -5840,9 +5899,17 @@ function DocumentSettingsDialog({
5840
5899
  const currentTitle = typeof frontmatter?.title === "string" ? frontmatter.title : "";
5841
5900
  const currentTheme = readFrontmatterThemeId(frontmatter) ?? "";
5842
5901
  const currentTransform = resolvePersistedTransformStyleId(
5843
- readFm(frontmatter, FM.transform.canonical, FM.transform.legacy)
5902
+ readFm(
5903
+ frontmatter,
5904
+ FRONTMATTER_SETTING_KEYS.transform.canonical,
5905
+ FRONTMATTER_SETTING_KEYS.transform.legacy
5906
+ )
5844
5907
  ) ?? "";
5845
- const currentCaptions = readFm(frontmatter, FM.captions.canonical, FM.captions.legacy);
5908
+ const currentCaptions = readFm(
5909
+ frontmatter,
5910
+ FRONTMATTER_SETTING_KEYS.captions.canonical,
5911
+ FRONTMATTER_SETTING_KEYS.captions.legacy
5912
+ );
5846
5913
  const [title, setTitle] = useState14(currentTitle);
5847
5914
  const [theme, setTheme] = useState14(currentTheme);
5848
5915
  const [transform, setTransform] = useState14(currentTransform);
@@ -5880,19 +5947,28 @@ function DocumentSettingsDialog({
5880
5947
  const nextTitle = !trimmedTitle || titleMatchesInferred ? null : trimmedTitle;
5881
5948
  const updates = {
5882
5949
  title: nextTitle,
5883
- [FM.theme.canonical]: theme || null,
5950
+ [FRONTMATTER_SETTING_KEYS.theme.canonical]: omitFrontmatterDefault(
5951
+ theme || FRONTMATTER_SETTING_DEFAULTS.theme,
5952
+ FRONTMATTER_SETTING_DEFAULTS.theme
5953
+ ),
5884
5954
  // Saving settings canonicalizes all older theme spellings.
5885
- [FM.theme.legacy[0]]: null,
5886
- [FM.theme.legacy[1]]: null,
5887
- [FM.transform.canonical]: transform || null,
5888
- [FM.transform.legacy]: null,
5889
- [FM.captions.canonical]: captions || null,
5890
- ...currentCaptions && captions !== currentCaptions ? { [FM.captions.legacy]: null } : {}
5955
+ [FRONTMATTER_SETTING_KEYS.theme.legacy[0]]: null,
5956
+ [FRONTMATTER_SETTING_KEYS.theme.legacy[1]]: null,
5957
+ [FRONTMATTER_SETTING_KEYS.transform.canonical]: omitFrontmatterDefault(
5958
+ transform,
5959
+ FRONTMATTER_SETTING_DEFAULTS.transform
5960
+ ),
5961
+ [FRONTMATTER_SETTING_KEYS.transform.legacy]: null,
5962
+ [FRONTMATTER_SETTING_KEYS.captions.canonical]: omitFrontmatterDefault(
5963
+ captions || FRONTMATTER_SETTING_DEFAULTS.captions,
5964
+ FRONTMATTER_SETTING_DEFAULTS.captions
5965
+ ),
5966
+ [FRONTMATTER_SETTING_KEYS.captions.legacy]: null
5891
5967
  };
5892
5968
  const nextSource = setFrontmatterValues(markdownSource, updates);
5893
5969
  onSave(nextSource);
5894
5970
  },
5895
- [title, theme, transform, captions, inferredTitle, markdownSource, currentCaptions, onSave]
5971
+ [title, theme, transform, captions, inferredTitle, markdownSource, onSave]
5896
5972
  );
5897
5973
  return /* @__PURE__ */ jsx16("div", { className: "squisq-doc-settings-overlay", onMouseDown: handleBackdrop, children: /* @__PURE__ */ jsxs11("form", { className: "squisq-doc-settings-dialog", onSubmit: handleSave, children: [
5898
5974
  /* @__PURE__ */ jsxs11("div", { className: "squisq-doc-settings-header", children: [
@@ -7906,8 +7982,7 @@ var SelectTool = {
7906
7982
  }
7907
7983
  } else if (dragState.kind === "resize") {
7908
7984
  const { x, y, width, height } = dragState.currentBounds;
7909
- ctx.dispatch({ kind: "moveLayer", id: dragState.layerId, x, y });
7910
- ctx.dispatch({ kind: "resizeLayer", id: dragState.layerId, width, height });
7985
+ ctx.dispatch({ kind: "resizeLayer", id: dragState.layerId, x, y, width, height });
7911
7986
  }
7912
7987
  e2.currentTarget.releasePointerCapture?.(e2.pointerId);
7913
7988
  setDragState(ctx.interaction, null);
@@ -11078,6 +11153,9 @@ function useLayoutAdapter(editor, headingPos, options = {}) {
11078
11153
  moveLayoutLayer(editor, headingPos, cmd.id, cmd.x, cmd.y);
11079
11154
  return;
11080
11155
  case "resizeLayer":
11156
+ if (cmd.x !== void 0 && cmd.y !== void 0) {
11157
+ moveLayoutLayer(editor, headingPos, cmd.id, cmd.x, cmd.y);
11158
+ }
11081
11159
  resizeLayoutLayer(editor, headingPos, cmd.id, cmd.width, cmd.height);
11082
11160
  return;
11083
11161
  case "addLayer": {
@@ -12271,7 +12349,12 @@ function useDrawingAdapter(editor, headingPos, options = {}) {
12271
12349
  case "resizeLayer": {
12272
12350
  if (!isPrimaryShapeLayer(cmd.id)) return;
12273
12351
  const id = shapeIdFromLayerId(cmd.id);
12274
- if (id) resizeShape(editor, headingPos, id, cmd.width, cmd.height);
12352
+ if (id) {
12353
+ if (cmd.x !== void 0 && cmd.y !== void 0) {
12354
+ moveShape(editor, headingPos, id, cmd.x, cmd.y);
12355
+ }
12356
+ resizeShape(editor, headingPos, id, cmd.width, cmd.height);
12357
+ }
12275
12358
  return;
12276
12359
  }
12277
12360
  case "addLayer": {
@@ -13493,7 +13576,13 @@ function applyCommand(layers, cmd) {
13493
13576
  return layers.map(
13494
13577
  (l) => l.id === cmd.id ? {
13495
13578
  ...l,
13496
- position: { ...l.position, width: cmd.width, height: cmd.height }
13579
+ position: {
13580
+ ...l.position,
13581
+ ...cmd.x !== void 0 ? { x: cmd.x } : {},
13582
+ ...cmd.y !== void 0 ? { y: cmd.y } : {},
13583
+ width: cmd.width,
13584
+ height: cmd.height
13585
+ }
13497
13586
  } : l
13498
13587
  );
13499
13588
  case "addLayer":
@@ -15957,12 +16046,6 @@ function resolveFrontmatterBoolean(value) {
15957
16046
  if (v === "false" || v === "no" || v === "off" || v === "hide" || v === "hidden") return false;
15958
16047
  return null;
15959
16048
  }
15960
- var FM_KEYS = {
15961
- theme: { canonical: "squisq-theme", legacy: ["themeId", "theme"] },
15962
- transform: { canonical: "squisq-transform", legacy: "transform-style" },
15963
- captions: { canonical: "squisq-captions", legacy: "caption-style" },
15964
- coverSlide: { canonical: "squisq-cover-slide", legacy: "cover-slide" }
15965
- };
15966
16049
  function readFrontmatterKey(fm, canonical, legacy) {
15967
16050
  if (!fm) return void 0;
15968
16051
  return Object.prototype.hasOwnProperty.call(fm, canonical) ? fm[canonical] : fm[legacy];
@@ -16009,7 +16092,7 @@ function PreviewSettingsProvider({
16009
16092
  );
16010
16093
  const [selectedThemeId, setSelectedThemeId] = useState32(null);
16011
16094
  useEffect26(() => setSelectedThemeId(null), [fmTheme]);
16012
- const resolvedThemeId = selectedThemeId ?? fmTheme ?? "standard";
16095
+ const resolvedThemeId = selectedThemeId ?? fmTheme ?? FRONTMATTER_SETTING_DEFAULTS.theme;
16013
16096
  const resolvedTheme = useMemo21(
16014
16097
  () => customThemes.find((theme) => theme.id === resolvedThemeId) ?? resolveThemeForDoc(doc, resolvedThemeId),
16015
16098
  [customThemes, doc, resolvedThemeId]
@@ -16028,9 +16111,12 @@ function PreviewSettingsProvider({
16028
16111
  const selectedCustom = customThemes.find((theme) => theme.id === id);
16029
16112
  const alreadyDocScoped = docThemes.some((theme) => theme.id === id);
16030
16113
  const updates = {
16031
- [FM_KEYS.theme.canonical]: id,
16032
- [FM_KEYS.theme.legacy[0]]: null,
16033
- [FM_KEYS.theme.legacy[1]]: null
16114
+ [FRONTMATTER_SETTING_KEYS.theme.canonical]: omitFrontmatterDefault(
16115
+ id,
16116
+ FRONTMATTER_SETTING_DEFAULTS.theme
16117
+ ),
16118
+ [FRONTMATTER_SETTING_KEYS.theme.legacy[0]]: null,
16119
+ [FRONTMATTER_SETTING_KEYS.theme.legacy[1]]: null
16034
16120
  };
16035
16121
  if (selectedCustom && !alreadyDocScoped) {
16036
16122
  updates[FRONTMATTER_CUSTOM_THEMES_KEY2] = writeCustomThemesToFrontmatter2([...docThemes, selectedCustom]) ?? null;
@@ -16057,7 +16143,7 @@ function PreviewSettingsProvider({
16057
16143
  const nextThemes = idx >= 0 ? docThemes2.map((t, i) => i === idx ? theme : t) : [...docThemes2, theme];
16058
16144
  const updates = {
16059
16145
  [FRONTMATTER_CUSTOM_THEMES_KEY2]: writeCustomThemesToFrontmatter2(nextThemes) ?? null,
16060
- [FM_KEYS.theme.canonical]: theme.id
16146
+ [FRONTMATTER_SETTING_KEYS.theme.canonical]: theme.id
16061
16147
  };
16062
16148
  if (extras?.templates && extras.templates.length > 0) {
16063
16149
  const existing = doc?.customTemplates ?? [];
@@ -16083,7 +16169,11 @@ function PreviewSettingsProvider({
16083
16169
  );
16084
16170
  const fmTransform = useMemo21(
16085
16171
  () => resolvePersistedTransformStyleId(
16086
- readFrontmatterKey(frontmatter, FM_KEYS.transform.canonical, FM_KEYS.transform.legacy)
16172
+ readFrontmatterKey(
16173
+ frontmatter,
16174
+ FRONTMATTER_SETTING_KEYS.transform.canonical,
16175
+ FRONTMATTER_SETTING_KEYS.transform.legacy
16176
+ )
16087
16177
  ),
16088
16178
  [frontmatter]
16089
16179
  );
@@ -16094,42 +16184,71 @@ function PreviewSettingsProvider({
16094
16184
  (id) => {
16095
16185
  setSelectedTransformStyle(id);
16096
16186
  if (id !== null) {
16097
- persistFrontmatter({ [FM_KEYS.transform.canonical]: id === "" ? null : id });
16187
+ persistFrontmatter({
16188
+ [FRONTMATTER_SETTING_KEYS.transform.canonical]: omitFrontmatterDefault(
16189
+ id,
16190
+ FRONTMATTER_SETTING_DEFAULTS.transform
16191
+ ),
16192
+ [FRONTMATTER_SETTING_KEYS.transform.legacy]: null
16193
+ });
16098
16194
  }
16099
16195
  },
16100
16196
  [persistFrontmatter]
16101
16197
  );
16102
16198
  const fmCaptionMode = useMemo21(
16103
16199
  () => resolveFrontmatterCaptionMode(
16104
- readFrontmatterKey(frontmatter, FM_KEYS.captions.canonical, FM_KEYS.captions.legacy)
16200
+ readFrontmatterKey(
16201
+ frontmatter,
16202
+ FRONTMATTER_SETTING_KEYS.captions.canonical,
16203
+ FRONTMATTER_SETTING_KEYS.captions.legacy
16204
+ )
16105
16205
  ),
16106
16206
  [frontmatter]
16107
16207
  );
16108
16208
  const [selectedCaptionMode, setSelectedCaptionMode] = useState32(null);
16109
16209
  useEffect26(() => setSelectedCaptionMode(null), [fmCaptionMode]);
16110
- const activeCaptionMode = selectedCaptionMode ?? fmCaptionMode ?? "standard";
16210
+ const activeCaptionMode = selectedCaptionMode ?? fmCaptionMode ?? FRONTMATTER_SETTING_DEFAULTS.captions;
16111
16211
  const activeCaptionsEnabled = activeCaptionMode !== "off";
16112
16212
  const activeCaptionStyle = activeCaptionMode === "social" ? "social" : "standard";
16113
16213
  const handleSetCaptionMode = useCallback29(
16114
16214
  (mode) => {
16115
16215
  setSelectedCaptionMode(mode);
16116
- persistFrontmatter({ [FM_KEYS.captions.canonical]: mode });
16216
+ persistFrontmatter({
16217
+ [FRONTMATTER_SETTING_KEYS.captions.canonical]: omitFrontmatterDefault(
16218
+ mode,
16219
+ FRONTMATTER_SETTING_DEFAULTS.captions
16220
+ ),
16221
+ [FRONTMATTER_SETTING_KEYS.captions.legacy]: null
16222
+ });
16117
16223
  },
16118
16224
  [persistFrontmatter]
16119
16225
  );
16120
16226
  const fmCoverSlide = useMemo21(
16121
16227
  () => resolveFrontmatterBoolean(
16122
- readFrontmatterKey(frontmatter, FM_KEYS.coverSlide.canonical, FM_KEYS.coverSlide.legacy)
16228
+ readFrontmatterKey(
16229
+ frontmatter,
16230
+ FRONTMATTER_SETTING_KEYS.coverSlide.canonical,
16231
+ FRONTMATTER_SETTING_KEYS.coverSlide.legacy
16232
+ )
16123
16233
  ),
16124
16234
  [frontmatter]
16125
16235
  );
16126
16236
  const [selectedCoverSlide, setSelectedCoverSlide] = useState32(null);
16127
16237
  useEffect26(() => setSelectedCoverSlide(null), [fmCoverSlide]);
16128
- const activeCoverSlide = selectedCoverSlide ?? fmCoverSlide ?? true;
16238
+ const activeCoverSlide = selectedCoverSlide ?? fmCoverSlide ?? FRONTMATTER_SETTING_DEFAULTS.coverSlide;
16129
16239
  const handleSetCoverSlideEnabled = useCallback29(
16130
16240
  (enabled) => {
16131
16241
  setSelectedCoverSlide(enabled);
16132
- persistFrontmatter({ [FM_KEYS.coverSlide.canonical]: enabled ? "true" : "false" });
16242
+ persistFrontmatter({
16243
+ // The cover is enabled by default, so only persist the non-default
16244
+ // state. Pass the boolean through so YAML writes `false`, not
16245
+ // the string `"false"`.
16246
+ [FRONTMATTER_SETTING_KEYS.coverSlide.canonical]: omitFrontmatterDefault(
16247
+ enabled,
16248
+ FRONTMATTER_SETTING_DEFAULTS.coverSlide
16249
+ ),
16250
+ [FRONTMATTER_SETTING_KEYS.coverSlide.legacy]: null
16251
+ });
16133
16252
  },
16134
16253
  [persistFrontmatter]
16135
16254
  );
@@ -16770,6 +16889,106 @@ function filterVisibleMediaEntries(entries) {
16770
16889
  return entries.filter(isVisibleMediaEntry);
16771
16890
  }
16772
16891
 
16892
+ // src/selectionConversions.ts
16893
+ function selectionLines(text) {
16894
+ return text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
16895
+ }
16896
+ function splitPipeLine(line) {
16897
+ let value = line.trim();
16898
+ if (value.startsWith("|")) value = value.slice(1);
16899
+ if (value.endsWith("|") && !value.endsWith("\\|")) value = value.slice(0, -1);
16900
+ const cells = [];
16901
+ let cell = "";
16902
+ for (let i = 0; i < value.length; i++) {
16903
+ const char = value[i];
16904
+ if (char === "\\" && value[i + 1] === "|") {
16905
+ cell += "|";
16906
+ i++;
16907
+ } else if (char === "|") {
16908
+ cells.push(cell.trim());
16909
+ cell = "";
16910
+ } else {
16911
+ cell += char;
16912
+ }
16913
+ }
16914
+ cells.push(cell.trim());
16915
+ return cells;
16916
+ }
16917
+ function splitCommaLine(line) {
16918
+ const cells = [];
16919
+ let cell = "";
16920
+ let quoted = false;
16921
+ for (let i = 0; i < line.length; i++) {
16922
+ const char = line[i];
16923
+ if (char === '"' && !quoted && cell.trim().length === 0) {
16924
+ quoted = true;
16925
+ cell = "";
16926
+ } else if (char === '"' && quoted) {
16927
+ if (line[i + 1] === '"') {
16928
+ cell += '"';
16929
+ i++;
16930
+ } else {
16931
+ quoted = false;
16932
+ }
16933
+ } else if (char === "," && !quoted) {
16934
+ cells.push(cell.trim());
16935
+ cell = "";
16936
+ } else {
16937
+ cell += char;
16938
+ }
16939
+ }
16940
+ if (quoted) return line.split(",").map((value) => value.trim());
16941
+ cells.push(cell.trim());
16942
+ return cells;
16943
+ }
16944
+ function splitLine(line, delimiter) {
16945
+ switch (delimiter) {
16946
+ case "pipe":
16947
+ return splitPipeLine(line);
16948
+ case "comma":
16949
+ return splitCommaLine(line);
16950
+ case "tab":
16951
+ return line.trim().split(" ").map((cell) => cell.trim());
16952
+ case "multispace":
16953
+ return line.trim().split(/[ \u00a0]{2,}/).map((cell) => cell.trim());
16954
+ }
16955
+ }
16956
+ function selectionToTable(text) {
16957
+ const lines = selectionLines(text);
16958
+ const candidates = ["pipe", "comma", "tab", "multispace"];
16959
+ if (lines.length >= 2) {
16960
+ for (const delimiter of candidates) {
16961
+ const rows = lines.map((line) => splitLine(line, delimiter));
16962
+ const columnCount = rows[0]?.length ?? 0;
16963
+ if (columnCount >= 2 && rows.every((row) => row.length === columnCount)) {
16964
+ return { delimiter, rows };
16965
+ }
16966
+ }
16967
+ }
16968
+ return { delimiter: null, rows: lines.map((line) => [line]) };
16969
+ }
16970
+ function escapeTableCell(cell) {
16971
+ return cell.replace(/\|/g, "\\|");
16972
+ }
16973
+ function selectionToTableMarkdown(text) {
16974
+ const { rows } = selectionToTable(text);
16975
+ if (rows.length === 0) return "";
16976
+ const formatRow = (row) => `| ${row.map(escapeTableCell).join(" | ")} |`;
16977
+ const separator = rows[0].map(() => "---");
16978
+ return [formatRow(rows[0]), formatRow(separator), ...rows.slice(1).map(formatRow)].join("\n");
16979
+ }
16980
+ function selectionToTaskItems(text) {
16981
+ return selectionLines(text).map((line) => {
16982
+ const task = /^[-*+]\s+\[([ xX])\]\s*(.*)$/.exec(line);
16983
+ if (task) return { checked: task[1].toLowerCase() === "x", text: task[2].trim() };
16984
+ const withoutListMarker = line.replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, "");
16985
+ return { checked: false, text: withoutListMarker.trim() };
16986
+ });
16987
+ }
16988
+ function selectionToTaskListMarkdown(text) {
16989
+ return selectionToTaskItems(text).map((item) => `- [${item.checked ? "x" : " "}] ${item.text}`).join("\n");
16990
+ }
16991
+
16773
16992
  // src/Toolbar.tsx
16774
16993
  import { Fragment as Fragment14, jsx as jsx39, jsxs as jsxs29 } from "react/jsx-runtime";
16775
16994
  var VIEWS = [
@@ -16947,6 +17166,7 @@ var BUTTONS = [
16947
17166
  ];
16948
17167
  var FIRST_MEDIA_INDEX = BUTTONS.findIndex((b) => b.group === "media");
16949
17168
  var MEDIA_BUTTONS = BUTTONS.filter((b) => b.group === "media");
17169
+ var CONVERT_BUTTONS = MEDIA_BUTTONS.filter((b) => b.id === "table" || b.id === "tasklist");
16950
17170
  var INSERT_MENU_WIDTH = 200;
16951
17171
  var TASK_LIST_ITEMS = ["Task 1", "Task 2", "Task 3"];
16952
17172
  var TASK_LIST_MARKDOWN = TASK_LIST_ITEMS.map((item) => `- [ ] ${item}`).join("\n");
@@ -16960,18 +17180,31 @@ function fileCountLabel(count) {
16960
17180
  function fileCountBadge(count) {
16961
17181
  return count > 99 ? "99+" : String(count);
16962
17182
  }
16963
- function taskListContent() {
17183
+ function paragraphContent(text) {
17184
+ return text ? { type: "paragraph", content: [{ type: "text", text }] } : { type: "paragraph" };
17185
+ }
17186
+ function tableContent(rows) {
17187
+ return {
17188
+ type: "table",
17189
+ content: rows.map((row, rowIndex) => ({
17190
+ type: "tableRow",
17191
+ content: row.map((cell) => ({
17192
+ type: rowIndex === 0 ? "tableHeader" : "tableCell",
17193
+ content: [paragraphContent(cell)]
17194
+ }))
17195
+ }))
17196
+ };
17197
+ }
17198
+ function taskListContent(items = TASK_LIST_ITEMS.map((text) => ({
17199
+ checked: false,
17200
+ text
17201
+ }))) {
16964
17202
  return {
16965
17203
  type: "taskList",
16966
- content: TASK_LIST_ITEMS.map((item) => ({
17204
+ content: items.map((item) => ({
16967
17205
  type: "taskItem",
16968
- attrs: { checked: false },
16969
- content: [
16970
- {
16971
- type: "paragraph",
16972
- content: [{ type: "text", text: item }]
16973
- }
16974
- ]
17206
+ attrs: { checked: item.checked },
17207
+ content: [paragraphContent(item.text)]
16975
17208
  }))
16976
17209
  };
16977
17210
  }
@@ -16980,6 +17213,13 @@ function insertTaskList(editor) {
16980
17213
  const content = supportsTaskList ? taskListContent() : TASK_LIST_MARKDOWN;
16981
17214
  editor.chain().focus().insertContent(content).run();
16982
17215
  }
17216
+ function blockConversionRange(editor) {
17217
+ const { from, to, $from, $to } = editor.state.selection;
17218
+ return {
17219
+ from: $from.depth === 1 && $from.parent.isTextblock && $from.parentOffset === 0 ? $from.before(1) : from,
17220
+ to: $to.depth === 1 && $to.parent.isTextblock && $to.parentOffset === $to.parent.content.size ? $to.after(1) : to
17221
+ };
17222
+ }
16983
17223
  function isTiptapActive(editor, id) {
16984
17224
  if (!editor) return false;
16985
17225
  switch (id) {
@@ -17024,14 +17264,16 @@ var LAYOUT_STARTER_MARKDOWN = `
17024
17264
  Layout
17025
17265
  `;
17026
17266
  var DIAGRAM_STARTER_ART = [
17027
- "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
17028
- "\u2502 Start \u2502",
17029
- "\u2514\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2518",
17030
- " \u2502",
17031
- " \u25BC",
17032
- "\u250C\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510",
17033
- "\u2502 Next \u2502",
17034
- "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
17267
+ "",
17268
+ "",
17269
+ " \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
17270
+ " \u2502 Start \u2502",
17271
+ " \u2514\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2518",
17272
+ " \u2502",
17273
+ " \u25BC",
17274
+ " \u250C\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510",
17275
+ " \u2502 Next \u2502",
17276
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
17035
17277
  ].join("\n");
17036
17278
  var DIAGRAM_STARTER_MARKDOWN = "\n```diagram\n" + DIAGRAM_STARTER_ART + "\n```\n";
17037
17279
  function insertAsciiDiagramBlock(editor) {
@@ -17677,6 +17919,43 @@ ${TASK_LIST_MARKDOWN}
17677
17919
  },
17678
17920
  [monacoEditor, markdownSource, setMarkdownSource]
17679
17921
  );
17922
+ const readSelectedText = useCallback30(() => {
17923
+ if (activeSceneText) return "";
17924
+ if (activeView === "wysiwyg" && tiptapEditor) {
17925
+ const { from, to, empty } = tiptapEditor.state.selection;
17926
+ return empty ? "" : tiptapEditor.state.doc.textBetween(from, to, "\n");
17927
+ }
17928
+ if (activeView === "raw" && monacoEditor) {
17929
+ const selection = monacoEditor.getSelection();
17930
+ const model = monacoEditor.getModel();
17931
+ return selection && model ? model.getValueInRange(selection) : "";
17932
+ }
17933
+ return "";
17934
+ }, [activeSceneText, activeView, monacoEditor, tiptapEditor]);
17935
+ const handleConvertSelection = useCallback30(
17936
+ (target) => {
17937
+ const selectedText = readSelectedText();
17938
+ if (!selectedText.trim()) return;
17939
+ if (activeView === "wysiwyg" && tiptapEditor) {
17940
+ const content = target === "table" ? tableContent(selectionToTable(selectedText).rows) : taskListContent(selectionToTaskItems(selectedText));
17941
+ tiptapEditor.chain().focus().insertContentAt(blockConversionRange(tiptapEditor), content).run();
17942
+ return;
17943
+ }
17944
+ if (activeView === "raw" && monacoEditor) {
17945
+ const selection = monacoEditor.getSelection();
17946
+ if (!selection) return;
17947
+ const converted = target === "table" ? selectionToTableMarkdown(selectedText) : selectionToTaskListMarkdown(selectedText);
17948
+ if (!converted) return;
17949
+ const leadingNewline = /^(?:\r?\n)/.test(selectedText) ? "\n" : "";
17950
+ const trailingNewline = /(?:\r?\n)$/.test(selectedText) ? "\n" : "";
17951
+ monacoEditor.executeEdits("toolbar-convert-selection", [
17952
+ { range: selection, text: leadingNewline + converted + trailingNewline }
17953
+ ]);
17954
+ monacoEditor.focus();
17955
+ }
17956
+ },
17957
+ [activeView, monacoEditor, readSelectedText, tiptapEditor]
17958
+ );
17680
17959
  const handleImageFile = useCallback30(
17681
17960
  async (file) => {
17682
17961
  if (!mediaProvider) return;
@@ -17866,6 +18145,7 @@ ${TASK_LIST_MARKDOWN}
17866
18145
  if (sceneTextLevel === "block") return BLOCK_SCENE_BUTTONS.has(id);
17867
18146
  return INLINE_SCENE_BUTTONS.has(id);
17868
18147
  };
18148
+ const showConvertActions = insertMenuAnchor !== null && readSelectedText().trim().length > 0 && CONVERT_BUTTONS.some((button) => buttonAllowed(button.id));
17869
18149
  const maxHeadingLevelInDoc = useMemo22(() => {
17870
18150
  if (!markdownSource) return 0;
17871
18151
  let max = 0;
@@ -18682,7 +18962,37 @@ ${TASK_LIST_MARKDOWN}
18682
18962
  style: { position: "fixed", top: insertMenuAnchor.top, left: insertMenuAnchor.left },
18683
18963
  role: "menu",
18684
18964
  children: [
18685
- /* @__PURE__ */ jsx39("div", { className: "squisq-insert-menu-header", children: "Insert" }),
18965
+ showConvertActions && /* @__PURE__ */ jsxs29(Fragment14, { children: [
18966
+ /* @__PURE__ */ jsx39("div", { className: "squisq-insert-menu-header", children: "Convert" }),
18967
+ CONVERT_BUTTONS.map((btn) => {
18968
+ const label = btn.id === "table" ? "Table" : "Task List";
18969
+ return /* @__PURE__ */ jsxs29(
18970
+ "button",
18971
+ {
18972
+ className: "squisq-toolbar-overflow-item",
18973
+ disabled: !buttonAllowed(btn.id),
18974
+ onClick: () => {
18975
+ handleConvertSelection(btn.id);
18976
+ closeInsertMenu();
18977
+ },
18978
+ role: "menuitem",
18979
+ "aria-label": `Convert selection to ${label}`,
18980
+ children: [
18981
+ /* @__PURE__ */ jsx39("span", { className: "squisq-toolbar-overflow-icon", children: buttonIcon(btn) }),
18982
+ /* @__PURE__ */ jsx39("span", { children: label })
18983
+ ]
18984
+ },
18985
+ `convert-${btn.id}`
18986
+ );
18987
+ })
18988
+ ] }),
18989
+ /* @__PURE__ */ jsx39(
18990
+ "div",
18991
+ {
18992
+ className: `squisq-insert-menu-header${showConvertActions ? " squisq-insert-menu-header--separated" : ""}`,
18993
+ children: "Insert"
18994
+ }
18995
+ ),
18686
18996
  MEDIA_BUTTONS.filter((b) => isButtonVisible(b.id)).map((btn) => {
18687
18997
  const disabled = btn.id === "image" && !mediaProvider || !buttonAllowed(btn.id);
18688
18998
  const stripped = btn.title.replace(/^Insert\s+/i, "");
@@ -19310,7 +19620,7 @@ function RawEditor({
19310
19620
  }
19311
19621
 
19312
19622
  // src/WysiwygEditor.tsx
19313
- import { useCallback as useCallback36, useEffect as useEffect36, useMemo as useMemo30, useRef as useRef34, useState as useState44 } from "react";
19623
+ import { useCallback as useCallback36, useEffect as useEffect36, useMemo as useMemo30, useRef as useRef35, useState as useState44 } from "react";
19314
19624
  import { useEditor as useEditor2, EditorContent as EditorContent2 } from "@tiptap/react";
19315
19625
  import { Selection as Selection2 } from "@tiptap/pm/state";
19316
19626
  import StarterKit2 from "@tiptap/starter-kit";
@@ -19648,7 +19958,14 @@ function DiagramCanvas({
19648
19958
  if (!cmd.id.startsWith("node-card-")) return;
19649
19959
  const nodeId = nodeIdFromCardLayerId(cmd.id);
19650
19960
  if (!nodeId) return;
19651
- onCommand({ kind: "resizeNode", nodeId, width: cmd.width, height: cmd.height });
19961
+ onCommand({
19962
+ kind: "resizeNode",
19963
+ nodeId,
19964
+ width: cmd.width,
19965
+ height: cmd.height,
19966
+ ...cmd.x !== void 0 ? { x: cmd.x } : {},
19967
+ ...cmd.y !== void 0 ? { y: cmd.y } : {}
19968
+ });
19652
19969
  return;
19653
19970
  }
19654
19971
  case "setLayerAttr":
@@ -19734,6 +20051,168 @@ import {
19734
20051
  ASCII_CHAR_H,
19735
20052
  ASCII_CHAR_W
19736
20053
  } from "@bendyline/squisq/doc";
20054
+
20055
+ // src/asciiDiagram/asciiDiagramOps.ts
20056
+ function sanitizeAsciiLabel(label) {
20057
+ return label.replace(/[─-╿▲▼◀-◄▶-►←-↓`]/gu, " ").replace(/\s+/g, " ").trim();
20058
+ }
20059
+ function descendantsOf(diagram, nodeId) {
20060
+ const out = /* @__PURE__ */ new Set();
20061
+ let grew = true;
20062
+ while (grew) {
20063
+ grew = false;
20064
+ for (const n of diagram.nodes) {
20065
+ if (out.has(n.id)) continue;
20066
+ if (n.containerId === nodeId || n.containerId && out.has(n.containerId)) {
20067
+ out.add(n.id);
20068
+ grew = true;
20069
+ }
20070
+ }
20071
+ }
20072
+ return out;
20073
+ }
20074
+ function translateDiagramOp(diagram, dCol, dRow) {
20075
+ const colDelta = Math.round(dCol);
20076
+ const rowDelta = Math.round(dRow);
20077
+ if (colDelta === 0 && rowDelta === 0) return diagram;
20078
+ return {
20079
+ ...diagram,
20080
+ nodes: diagram.nodes.map((node) => ({
20081
+ ...node,
20082
+ col: Math.max(0, node.col + colDelta),
20083
+ row: Math.max(0, node.row + rowDelta)
20084
+ }))
20085
+ };
20086
+ }
20087
+ function moveNodeOp(diagram, nodeId, col, row) {
20088
+ const node = diagram.nodes.find((n) => n.id === nodeId);
20089
+ if (!node) return diagram;
20090
+ const dCol = Math.max(0, col) - node.col;
20091
+ const dRow = Math.max(0, row) - node.row;
20092
+ if (dCol === 0 && dRow === 0) return diagram;
20093
+ const moving = descendantsOf(diagram, nodeId);
20094
+ moving.add(nodeId);
20095
+ return {
20096
+ ...diagram,
20097
+ nodes: diagram.nodes.map(
20098
+ (n) => moving.has(n.id) ? { ...n, col: Math.max(0, n.col + dCol), row: Math.max(0, n.row + dRow) } : n
20099
+ )
20100
+ };
20101
+ }
20102
+ function resizeNodeOp(diagram, nodeId, wCols, hRows) {
20103
+ const node = diagram.nodes.find((n) => n.id === nodeId);
20104
+ if (!node) return diagram;
20105
+ const labelLines = node.label.length > 0 ? node.label.split("\n") : [];
20106
+ const minW = Math.max(3, labelLines.reduce((w, l) => Math.max(w, l.length), 0) + 4);
20107
+ const minH = Math.max(3, labelLines.length + 2);
20108
+ return {
20109
+ ...diagram,
20110
+ nodes: diagram.nodes.map(
20111
+ (n) => n.id === nodeId ? {
20112
+ ...n,
20113
+ wCols: Math.max(minW, Math.round(wCols)),
20114
+ hRows: Math.max(minH, Math.round(hRows))
20115
+ } : n
20116
+ )
20117
+ };
20118
+ }
20119
+ function addEdgeOp(diagram, source, target, label) {
20120
+ const ids = new Set(diagram.nodes.map((n) => n.id));
20121
+ if (!ids.has(source) || !ids.has(target) || source === target) return diagram;
20122
+ const exists = diagram.edges.some(
20123
+ (e2) => e2.source === source && e2.target === target && (e2.label ?? "") === (label ?? "")
20124
+ );
20125
+ if (exists) return diagram;
20126
+ const edge = {
20127
+ source,
20128
+ target,
20129
+ ...label ? { label } : {},
20130
+ directed: true
20131
+ };
20132
+ return { ...diagram, edges: [...diagram.edges, edge] };
20133
+ }
20134
+ function removeEdgeOp(diagram, source, target, label) {
20135
+ let removed = false;
20136
+ const edges = diagram.edges.filter((e2) => {
20137
+ if (removed) return true;
20138
+ const match = e2.source === source && e2.target === target && (label !== void 0 ? (e2.label ?? "") === label : true);
20139
+ if (match) {
20140
+ removed = true;
20141
+ return false;
20142
+ }
20143
+ return true;
20144
+ });
20145
+ return removed ? { ...diagram, edges } : diagram;
20146
+ }
20147
+ function renameNodeOp(diagram, nodeId, label) {
20148
+ const node = diagram.nodes.find((n) => n.id === nodeId);
20149
+ if (!node) return diagram;
20150
+ const clean = sanitizeAsciiLabel(label);
20151
+ if (clean.length === 0 || clean === node.label) return diagram;
20152
+ return {
20153
+ ...diagram,
20154
+ nodes: diagram.nodes.map((n) => n.id === nodeId ? { ...n, label: clean } : n)
20155
+ };
20156
+ }
20157
+ function addNodeOp(diagram, opts) {
20158
+ let label = opts.label;
20159
+ if (!label) {
20160
+ let i = 1;
20161
+ const labels = new Set(diagram.nodes.map((n) => n.label.split("\n")[0]));
20162
+ while (labels.has(`Node ${i}`)) i++;
20163
+ label = `Node ${i}`;
20164
+ }
20165
+ const clean = sanitizeAsciiLabel(label) || "Node";
20166
+ const col = Math.max(0, Math.round(opts.col));
20167
+ const row = Math.max(0, Math.round(opts.row));
20168
+ const wCols = clean.length + 4;
20169
+ const hRows = 3;
20170
+ const centerCol = col + wCols / 2;
20171
+ const centerRow = row + hRows / 2;
20172
+ const containers = diagram.nodes.filter((n) => diagram.nodes.some((m) => m.containerId === n.id)).filter(
20173
+ (n) => centerCol > n.col && centerCol < n.col + n.wCols && centerRow > n.row && centerRow < n.row + n.hRows
20174
+ ).sort((a, b) => a.wCols * a.hRows - b.wCols * b.hRows);
20175
+ const node = {
20176
+ id: `__new-${diagram.nodes.length}`,
20177
+ // provisional; the re-parse derives the real slug id
20178
+ label: clean,
20179
+ col,
20180
+ row,
20181
+ wCols,
20182
+ hRows,
20183
+ ...containers[0] ? { containerId: containers[0].id } : {}
20184
+ };
20185
+ return { diagram: { ...diagram, nodes: [...diagram.nodes, node] }, label: clean };
20186
+ }
20187
+ function removeNodeOp(diagram, nodeId) {
20188
+ const node = diagram.nodes.find((n) => n.id === nodeId);
20189
+ if (!node) return diagram;
20190
+ const nodes = diagram.nodes.filter((n) => n.id !== nodeId).map((n) => {
20191
+ if (n.containerId !== nodeId) return n;
20192
+ const promoted = { ...n };
20193
+ if (node.containerId) promoted.containerId = node.containerId;
20194
+ else delete promoted.containerId;
20195
+ return promoted;
20196
+ });
20197
+ const edges = diagram.edges.filter((e2) => e2.source !== nodeId && e2.target !== nodeId);
20198
+ return { ...diagram, nodes, edges };
20199
+ }
20200
+
20201
+ // src/asciiDiagram/asciiDiagramData.ts
20202
+ var ASCII_DIAGRAM_GUTTER_COLS = 8;
20203
+ var ASCII_DIAGRAM_GUTTER_ROWS = 2;
20204
+ function initialAsciiDiagramCanvasOffset(diagram) {
20205
+ if (diagram.nodes.length === 0) return { col: 0, row: 0 };
20206
+ const minCol = Math.min(...diagram.nodes.map((node) => node.col));
20207
+ const minRow = Math.min(...diagram.nodes.map((node) => node.row));
20208
+ return {
20209
+ col: Math.max(0, ASCII_DIAGRAM_GUTTER_COLS - minCol),
20210
+ row: Math.max(0, ASCII_DIAGRAM_GUTTER_ROWS - minRow)
20211
+ };
20212
+ }
20213
+ function offsetAsciiDiagram(diagram, offset) {
20214
+ return translateDiagramOp(diagram, offset.col, offset.row);
20215
+ }
19737
20216
  function asciiDiagramToCanvas(diagram) {
19738
20217
  const containerIds = new Set(
19739
20218
  diagram.nodes.map((n) => n.containerId).filter((id) => id !== void 0)
@@ -19941,139 +20420,6 @@ var RepairableDiagramExtension = Extension.create({
19941
20420
  }
19942
20421
  });
19943
20422
 
19944
- // src/asciiDiagram/asciiDiagramOps.ts
19945
- function sanitizeAsciiLabel(label) {
19946
- return label.replace(/[─-╿▲▼◀-◄▶-►←-↓`]/gu, " ").replace(/\s+/g, " ").trim();
19947
- }
19948
- function descendantsOf(diagram, nodeId) {
19949
- const out = /* @__PURE__ */ new Set();
19950
- let grew = true;
19951
- while (grew) {
19952
- grew = false;
19953
- for (const n of diagram.nodes) {
19954
- if (out.has(n.id)) continue;
19955
- if (n.containerId === nodeId || n.containerId && out.has(n.containerId)) {
19956
- out.add(n.id);
19957
- grew = true;
19958
- }
19959
- }
19960
- }
19961
- return out;
19962
- }
19963
- function moveNodeOp(diagram, nodeId, col, row) {
19964
- const node = diagram.nodes.find((n) => n.id === nodeId);
19965
- if (!node) return diagram;
19966
- const dCol = Math.max(0, col) - node.col;
19967
- const dRow = Math.max(0, row) - node.row;
19968
- if (dCol === 0 && dRow === 0) return diagram;
19969
- const moving = descendantsOf(diagram, nodeId);
19970
- moving.add(nodeId);
19971
- return {
19972
- ...diagram,
19973
- nodes: diagram.nodes.map(
19974
- (n) => moving.has(n.id) ? { ...n, col: Math.max(0, n.col + dCol), row: Math.max(0, n.row + dRow) } : n
19975
- )
19976
- };
19977
- }
19978
- function resizeNodeOp(diagram, nodeId, wCols, hRows) {
19979
- const node = diagram.nodes.find((n) => n.id === nodeId);
19980
- if (!node) return diagram;
19981
- const labelLines = node.label.length > 0 ? node.label.split("\n") : [];
19982
- const minW = Math.max(3, labelLines.reduce((w, l) => Math.max(w, l.length), 0) + 4);
19983
- const minH = Math.max(3, labelLines.length + 2);
19984
- return {
19985
- ...diagram,
19986
- nodes: diagram.nodes.map(
19987
- (n) => n.id === nodeId ? {
19988
- ...n,
19989
- wCols: Math.max(minW, Math.round(wCols)),
19990
- hRows: Math.max(minH, Math.round(hRows))
19991
- } : n
19992
- )
19993
- };
19994
- }
19995
- function addEdgeOp(diagram, source, target, label) {
19996
- const ids = new Set(diagram.nodes.map((n) => n.id));
19997
- if (!ids.has(source) || !ids.has(target) || source === target) return diagram;
19998
- const exists = diagram.edges.some(
19999
- (e2) => e2.source === source && e2.target === target && (e2.label ?? "") === (label ?? "")
20000
- );
20001
- if (exists) return diagram;
20002
- const edge = {
20003
- source,
20004
- target,
20005
- ...label ? { label } : {},
20006
- directed: true
20007
- };
20008
- return { ...diagram, edges: [...diagram.edges, edge] };
20009
- }
20010
- function removeEdgeOp(diagram, source, target, label) {
20011
- let removed = false;
20012
- const edges = diagram.edges.filter((e2) => {
20013
- if (removed) return true;
20014
- const match = e2.source === source && e2.target === target && (label !== void 0 ? (e2.label ?? "") === label : true);
20015
- if (match) {
20016
- removed = true;
20017
- return false;
20018
- }
20019
- return true;
20020
- });
20021
- return removed ? { ...diagram, edges } : diagram;
20022
- }
20023
- function renameNodeOp(diagram, nodeId, label) {
20024
- const node = diagram.nodes.find((n) => n.id === nodeId);
20025
- if (!node) return diagram;
20026
- const clean = sanitizeAsciiLabel(label);
20027
- if (clean.length === 0 || clean === node.label) return diagram;
20028
- return {
20029
- ...diagram,
20030
- nodes: diagram.nodes.map((n) => n.id === nodeId ? { ...n, label: clean } : n)
20031
- };
20032
- }
20033
- function addNodeOp(diagram, opts) {
20034
- let label = opts.label;
20035
- if (!label) {
20036
- let i = 1;
20037
- const labels = new Set(diagram.nodes.map((n) => n.label.split("\n")[0]));
20038
- while (labels.has(`Node ${i}`)) i++;
20039
- label = `Node ${i}`;
20040
- }
20041
- const clean = sanitizeAsciiLabel(label) || "Node";
20042
- const col = Math.max(0, Math.round(opts.col));
20043
- const row = Math.max(0, Math.round(opts.row));
20044
- const wCols = clean.length + 4;
20045
- const hRows = 3;
20046
- const centerCol = col + wCols / 2;
20047
- const centerRow = row + hRows / 2;
20048
- const containers = diagram.nodes.filter((n) => diagram.nodes.some((m) => m.containerId === n.id)).filter(
20049
- (n) => centerCol > n.col && centerCol < n.col + n.wCols && centerRow > n.row && centerRow < n.row + n.hRows
20050
- ).sort((a, b) => a.wCols * a.hRows - b.wCols * b.hRows);
20051
- const node = {
20052
- id: `__new-${diagram.nodes.length}`,
20053
- // provisional; the re-parse derives the real slug id
20054
- label: clean,
20055
- col,
20056
- row,
20057
- wCols,
20058
- hRows,
20059
- ...containers[0] ? { containerId: containers[0].id } : {}
20060
- };
20061
- return { diagram: { ...diagram, nodes: [...diagram.nodes, node] }, label: clean };
20062
- }
20063
- function removeNodeOp(diagram, nodeId) {
20064
- const node = diagram.nodes.find((n) => n.id === nodeId);
20065
- if (!node) return diagram;
20066
- const nodes = diagram.nodes.filter((n) => n.id !== nodeId).map((n) => {
20067
- if (n.containerId !== nodeId) return n;
20068
- const promoted = { ...n };
20069
- if (node.containerId) promoted.containerId = node.containerId;
20070
- else delete promoted.containerId;
20071
- return promoted;
20072
- });
20073
- const edges = diagram.edges.filter((e2) => e2.source !== nodeId && e2.target !== nodeId);
20074
- return { ...diagram, nodes, edges };
20075
- }
20076
-
20077
20423
  // src/asciiDiagram/asciiDiagramCommands.ts
20078
20424
  function replaceAsciiFenceText(editor, pos, nextText, ensureLanguage) {
20079
20425
  return editor.chain().command(({ tr, state }) => {
@@ -20101,43 +20447,52 @@ function applyRepairCommand(editor, blockId) {
20101
20447
  if (!repaired) return false;
20102
20448
  return replaceAsciiFenceText(editor, pos, repaired.art, "diagram");
20103
20449
  }
20104
- function applyOp(editor, blockId, op) {
20450
+ function applyOp(editor, blockId, op, diagramOffset) {
20105
20451
  const pos = findAsciiDiagramBlockPos(editor, blockId);
20106
20452
  if (pos === null) return false;
20107
20453
  const node = editor.state.doc.nodeAt(pos);
20108
20454
  if (!node || node.type.name !== "codeBlock") return false;
20109
- const diagram = parseAsciiDiagramForNode(node);
20110
- if (!diagram) return false;
20455
+ const sourceDiagram = parseAsciiDiagramForNode(node);
20456
+ if (!sourceDiagram) return false;
20457
+ const diagram = diagramOffset ? translateDiagramOp(sourceDiagram, diagramOffset.col, diagramOffset.row) : sourceDiagram;
20111
20458
  const next = op(diagram);
20112
- if (next === diagram) return false;
20459
+ if (next === diagram && diagram === sourceDiagram) return false;
20113
20460
  const rendered = renderAsciiDiagram(next);
20114
20461
  const verification = parseAsciiDiagram(rendered);
20115
20462
  if (verification.nodes.length !== next.nodes.length) return false;
20116
20463
  return replaceAsciiFenceText(editor, pos, rendered, "diagram");
20117
20464
  }
20118
- function applyAsciiDiagramCommand(editor, blockId, cmd) {
20465
+ function applyAsciiDiagramCommand(editor, blockId, cmd, options = {}) {
20466
+ const apply = (op) => applyOp(editor, blockId, op, options.diagramOffset);
20119
20467
  switch (cmd.kind) {
20120
20468
  case "moveNode": {
20121
20469
  const { col, row } = canvasToAsciiCell(cmd.x, cmd.y);
20122
- return applyOp(editor, blockId, (d) => moveNodeOp(d, cmd.nodeId, col, row));
20470
+ return apply((d) => moveNodeOp(d, cmd.nodeId, col, row));
20123
20471
  }
20124
20472
  case "resizeNode": {
20125
20473
  const wCols = Math.max(3, Math.round(cmd.width / ASCII_CHAR_W2));
20126
20474
  const hRows = Math.max(3, Math.round(cmd.height / ASCII_CHAR_H2));
20127
- return applyOp(editor, blockId, (d) => resizeNodeOp(d, cmd.nodeId, wCols, hRows));
20475
+ return apply((d) => {
20476
+ let next = d;
20477
+ if (cmd.x !== void 0 && cmd.y !== void 0) {
20478
+ const { col, row } = canvasToAsciiCell(cmd.x, cmd.y);
20479
+ next = moveNodeOp(next, cmd.nodeId, col, row);
20480
+ }
20481
+ return resizeNodeOp(next, cmd.nodeId, wCols, hRows);
20482
+ });
20128
20483
  }
20129
20484
  case "addConnection":
20130
- return applyOp(editor, blockId, (d) => addEdgeOp(d, cmd.source, cmd.target, cmd.type));
20485
+ return apply((d) => addEdgeOp(d, cmd.source, cmd.target, cmd.type));
20131
20486
  case "removeConnection":
20132
- return applyOp(editor, blockId, (d) => removeEdgeOp(d, cmd.source, cmd.target, cmd.type));
20487
+ return apply((d) => removeEdgeOp(d, cmd.source, cmd.target, cmd.type));
20133
20488
  case "renameNode":
20134
- return applyOp(editor, blockId, (d) => renameNodeOp(d, cmd.nodeId, cmd.newLabel));
20489
+ return apply((d) => renameNodeOp(d, cmd.nodeId, cmd.newLabel));
20135
20490
  case "addNode": {
20136
20491
  const { col, row } = canvasToAsciiCell(cmd.x, cmd.y);
20137
- return applyOp(editor, blockId, (d) => addNodeOp(d, { col, row }).diagram);
20492
+ return apply((d) => addNodeOp(d, { col, row }).diagram);
20138
20493
  }
20139
20494
  case "removeNode":
20140
- return applyOp(editor, blockId, (d) => removeNodeOp(d, cmd.nodeId));
20495
+ return apply((d) => removeNodeOp(d, cmd.nodeId));
20141
20496
  }
20142
20497
  const _exhaustive = cmd;
20143
20498
  void _exhaustive;
@@ -20158,11 +20513,28 @@ function AsciiDiagramWidget({
20158
20513
  const [maximized, setMaximized] = useState36(false);
20159
20514
  const [height, setHeight] = useState36(null);
20160
20515
  const [dragHeight, setDragHeight] = useState36(null);
20516
+ const canvasOffsetRef = useRef29(null);
20517
+ const [canvasOffsetVersion, setCanvasOffsetVersion] = useState36(0);
20161
20518
  const inlineRef = useRef29(null);
20162
20519
  const effectiveHeight = dragHeight ?? height;
20520
+ if (view && canvasOffsetRef.current === null) {
20521
+ canvasOffsetRef.current = initialAsciiDiagramCanvasOffset(view.diagram);
20522
+ }
20523
+ const canvasView = useMemo26(() => {
20524
+ if (!view) return null;
20525
+ const offset = canvasOffsetRef.current ?? { col: 0, row: 0 };
20526
+ return asciiDiagramToCanvas(offsetAsciiDiagram(view.diagram, offset));
20527
+ }, [view, canvasOffsetVersion]);
20163
20528
  const dispatch = useCallback33(
20164
20529
  (cmd) => {
20165
- applyAsciiDiagramCommand(editor, blockId, cmd);
20530
+ const offset = canvasOffsetRef.current ?? { col: 0, row: 0 };
20531
+ const applied = applyAsciiDiagramCommand(editor, blockId, cmd, {
20532
+ diagramOffset: offset
20533
+ });
20534
+ if (applied && (offset.col !== 0 || offset.row !== 0)) {
20535
+ canvasOffsetRef.current = { col: 0, row: 0 };
20536
+ setCanvasOffsetVersion((version) => version + 1);
20537
+ }
20166
20538
  },
20167
20539
  [editor, blockId]
20168
20540
  );
@@ -20253,8 +20625,8 @@ function AsciiDiagramWidget({
20253
20625
  DiagramCanvas,
20254
20626
  {
20255
20627
  textChannel,
20256
- nodes: view.nodes,
20257
- edges: view.edges,
20628
+ nodes: canvasView?.nodes ?? view.nodes,
20629
+ edges: canvasView?.edges ?? view.edges,
20258
20630
  onCommand: dispatch,
20259
20631
  showMaximize: true,
20260
20632
  maximized,
@@ -20530,7 +20902,7 @@ import {
20530
20902
  } from "@bendyline/squisq/doc";
20531
20903
 
20532
20904
  // src/treeview/TreeOutlineWidget.tsx
20533
- import { useCallback as useCallback34, useState as useState38 } from "react";
20905
+ import { useCallback as useCallback34, useRef as useRef30, useState as useState38 } from "react";
20534
20906
 
20535
20907
  // src/treeview/treeViewData.ts
20536
20908
  import { useEffect as useEffect30, useMemo as useMemo27, useState as useState37 } from "react";
@@ -20584,6 +20956,9 @@ function locate(roots, id, parent = null) {
20584
20956
  function markDir(n) {
20585
20957
  n.isDir = n.label.endsWith("/") || n.children.length > 0;
20586
20958
  }
20959
+ function containsNode(node, id) {
20960
+ return node.id === id || node.children.some((child) => containsNode(child, id));
20961
+ }
20587
20962
  function renameItemOp(tree, id, label) {
20588
20963
  const clean = sanitizeTreeLabel(label);
20589
20964
  const next = cloneTree(tree);
@@ -20642,6 +21017,34 @@ function outdentItemOp(tree, id) {
20642
21017
  markDir(loc.parent);
20643
21018
  return next;
20644
21019
  }
21020
+ function moveItemOp(tree, id, targetId, position) {
21021
+ const currentSource = locate(tree.roots, id);
21022
+ const currentTarget = locate(tree.roots, targetId);
21023
+ if (!currentSource || !currentTarget || id === targetId || containsNode(currentSource.node, targetId)) {
21024
+ return tree;
21025
+ }
21026
+ if (currentSource.siblings === currentTarget.siblings) {
21027
+ if (position === "before" && currentSource.index === currentTarget.index - 1) return tree;
21028
+ if (position === "after" && currentSource.index === currentTarget.index + 1) return tree;
21029
+ }
21030
+ if (position === "child" && currentSource.parent?.id === currentTarget.node.id && currentSource.index === currentSource.siblings.length - 1) {
21031
+ return tree;
21032
+ }
21033
+ const next = cloneTree(tree);
21034
+ const source = locate(next.roots, id);
21035
+ if (!source) return tree;
21036
+ const [moved] = source.siblings.splice(source.index, 1);
21037
+ if (source.parent) markDir(source.parent);
21038
+ const target = locate(next.roots, targetId);
21039
+ if (!target) return tree;
21040
+ if (position === "child") {
21041
+ target.node.children.push(moved);
21042
+ markDir(target.node);
21043
+ } else {
21044
+ target.siblings.splice(target.index + (position === "after" ? 1 : 0), 0, moved);
21045
+ }
21046
+ return next;
21047
+ }
20645
21048
  function moveItemUpOp(tree, id) {
20646
21049
  const next = cloneTree(tree);
20647
21050
  const loc = locate(next.roots, id);
@@ -20713,6 +21116,8 @@ function applyTreeCommand(editor, blockId, cmd) {
20713
21116
  return applyOp2(editor, blockId, (t) => indentItemOp(t, cmd.id));
20714
21117
  case "outdentItem":
20715
21118
  return applyOp2(editor, blockId, (t) => outdentItemOp(t, cmd.id));
21119
+ case "moveItem":
21120
+ return applyOp2(editor, blockId, (t) => moveItemOp(t, cmd.id, cmd.targetId, cmd.position));
20716
21121
  case "moveItemUp":
20717
21122
  return applyOp2(editor, blockId, (t) => moveItemUpOp(t, cmd.id));
20718
21123
  case "moveItemDown":
@@ -20729,9 +21134,36 @@ function applyTreeCommand(editor, blockId, cmd) {
20729
21134
 
20730
21135
  // src/treeview/TreeOutlineWidget.tsx
20731
21136
  import { jsx as jsx44, jsxs as jsxs33 } from "react/jsx-runtime";
21137
+ var TREE_DRAG_MIME = "application/x-squisq-tree-node";
21138
+ function findNode(nodes, id) {
21139
+ for (const node of nodes) {
21140
+ if (node.id === id) return node;
21141
+ const child = findNode(node.children, id);
21142
+ if (child) return child;
21143
+ }
21144
+ return null;
21145
+ }
21146
+ function nodeContains(node, id) {
21147
+ return node.id === id || node.children.some((child) => nodeContains(child, id));
21148
+ }
21149
+ function canDropNode(nodes, sourceId, targetId) {
21150
+ const source = findNode(nodes, sourceId);
21151
+ return source != null && !nodeContains(source, targetId);
21152
+ }
21153
+ function dropPositionForPointer(event) {
21154
+ const rect = event.currentTarget.getBoundingClientRect();
21155
+ if (rect.height <= 0) return "child";
21156
+ const ratio = (event.clientY - rect.top) / rect.height;
21157
+ if (ratio < 0.3) return "before";
21158
+ if (ratio > 0.7) return "after";
21159
+ return "child";
21160
+ }
20732
21161
  function TreeOutlineWidget({ editor, blockId }) {
20733
21162
  const view = useTreeViewData(editor, blockId);
20734
21163
  const [collapsed, setCollapsed] = useState38(() => /* @__PURE__ */ new Set());
21164
+ const activeDragRef = useRef30(null);
21165
+ const [draggedId, setDraggedId] = useState38(null);
21166
+ const [dropTarget, setDropTarget] = useState38(null);
20735
21167
  const dispatch = useCallback34(
20736
21168
  (cmd) => applyTreeCommand(editor, blockId, cmd),
20737
21169
  [editor, blockId]
@@ -20744,6 +21176,61 @@ function TreeOutlineWidget({ editor, blockId }) {
20744
21176
  return next;
20745
21177
  });
20746
21178
  }, []);
21179
+ const clearDragState = useCallback34(() => {
21180
+ activeDragRef.current = null;
21181
+ setDraggedId(null);
21182
+ setDropTarget(null);
21183
+ }, []);
21184
+ const handleDragStart = useCallback34((event, id) => {
21185
+ event.stopPropagation();
21186
+ activeDragRef.current = id;
21187
+ setDraggedId(id);
21188
+ setDropTarget(null);
21189
+ event.dataTransfer.effectAllowed = "move";
21190
+ event.dataTransfer.setData(TREE_DRAG_MIME, id);
21191
+ event.dataTransfer.setData("text/plain", id);
21192
+ }, []);
21193
+ const handleDragOver = useCallback34(
21194
+ (event, targetId) => {
21195
+ const sourceId = activeDragRef.current;
21196
+ if (!sourceId) return;
21197
+ event.preventDefault();
21198
+ event.stopPropagation();
21199
+ if (!view || !canDropNode(view.tree.roots, sourceId, targetId)) {
21200
+ event.dataTransfer.dropEffect = "none";
21201
+ setDropTarget(null);
21202
+ return;
21203
+ }
21204
+ const position = dropPositionForPointer(event);
21205
+ event.dataTransfer.dropEffect = "move";
21206
+ setDropTarget(
21207
+ (current) => current?.id === targetId && current.position === position ? current : { id: targetId, position }
21208
+ );
21209
+ },
21210
+ [view]
21211
+ );
21212
+ const handleDrop = useCallback34(
21213
+ (event, targetId) => {
21214
+ const sourceId = activeDragRef.current;
21215
+ if (!sourceId) return;
21216
+ event.preventDefault();
21217
+ event.stopPropagation();
21218
+ const position = dropPositionForPointer(event);
21219
+ const canMove = view && canDropNode(view.tree.roots, sourceId, targetId);
21220
+ clearDragState();
21221
+ if (!canMove) return;
21222
+ const moved = dispatch({ kind: "moveItem", id: sourceId, targetId, position });
21223
+ if (moved && position === "child") {
21224
+ setCollapsed((current) => {
21225
+ if (!current.has(targetId)) return current;
21226
+ const next = new Set(current);
21227
+ next.delete(targetId);
21228
+ return next;
21229
+ });
21230
+ }
21231
+ },
21232
+ [clearDragState, dispatch, view]
21233
+ );
20747
21234
  if (!view) return null;
20748
21235
  const roots = view.tree.roots;
20749
21236
  const firstRootId = roots[0]?.id;
@@ -20789,8 +21276,14 @@ function TreeOutlineWidget({ editor, blockId }) {
20789
21276
  node,
20790
21277
  depth: 0,
20791
21278
  collapsed,
21279
+ draggedId,
21280
+ dropTarget,
20792
21281
  toggleCollapse,
20793
- dispatch
21282
+ dispatch,
21283
+ onDragStart: handleDragStart,
21284
+ onDragOver: handleDragOver,
21285
+ onDrop: handleDrop,
21286
+ onDragEnd: clearDragState
20794
21287
  },
20795
21288
  node.id
20796
21289
  )) }),
@@ -20805,129 +21298,167 @@ function TreeRowView({
20805
21298
  node,
20806
21299
  depth,
20807
21300
  collapsed,
21301
+ draggedId,
21302
+ dropTarget,
20808
21303
  toggleCollapse,
20809
- dispatch
21304
+ dispatch,
21305
+ onDragStart,
21306
+ onDragOver,
21307
+ onDrop,
21308
+ onDragEnd
20810
21309
  }) {
20811
21310
  const hasChildren = node.children.length > 0;
20812
21311
  const isCollapsed = collapsed.has(node.id);
20813
21312
  const isDir = node.isDir || hasChildren;
20814
21313
  const [draft, setDraft] = useState38(node.label);
21314
+ const dropPosition = dropTarget?.id === node.id ? dropTarget.position : null;
21315
+ const itemClassName = [
21316
+ "squisq-tree-item",
21317
+ draggedId === node.id ? "squisq-tree-item--dragging" : "",
21318
+ dropPosition ? `squisq-tree-item--drop-${dropPosition}` : ""
21319
+ ].filter(Boolean).join(" ");
20815
21320
  const commit = () => {
20816
21321
  if (draft !== node.label && draft.trim().length > 0) {
20817
21322
  dispatch({ kind: "renameItem", id: node.id, label: draft });
20818
21323
  }
20819
21324
  };
20820
- return /* @__PURE__ */ jsxs33("li", { role: "treeitem", style: { paddingLeft: `${depth * 18}px` }, children: [
20821
- /* @__PURE__ */ jsxs33("div", { className: "squisq-tree-row", children: [
20822
- hasChildren ? /* @__PURE__ */ jsx44(
20823
- "button",
20824
- {
20825
- type: "button",
20826
- className: "squisq-tree-chevron",
20827
- "aria-label": isCollapsed ? "Expand" : "Collapse",
20828
- onClick: () => toggleCollapse(node.id),
20829
- children: /* @__PURE__ */ jsx44(Icon, { icon: `fa-solid ${isCollapsed ? "fa-chevron-right" : "fa-chevron-down"}` })
20830
- }
20831
- ) : /* @__PURE__ */ jsx44("span", { className: "squisq-tree-chevron squisq-tree-chevron--empty" }),
20832
- /* @__PURE__ */ jsx44(
20833
- "button",
20834
- {
20835
- type: "button",
20836
- className: "squisq-tree-icon",
20837
- title: isDir ? "Make a file" : "Make a folder",
20838
- onClick: () => dispatch({ kind: "toggleDir", id: node.id }),
20839
- children: /* @__PURE__ */ jsx44(Icon, { icon: `fa-solid ${isDir ? "fa-folder" : "fa-file"}` })
20840
- }
20841
- ),
20842
- /* @__PURE__ */ jsx44(
20843
- "input",
20844
- {
20845
- className: "squisq-tree-label",
20846
- value: draft,
20847
- onChange: (e2) => setDraft(e2.target.value),
20848
- onBlur: commit,
20849
- onKeyDown: (e2) => {
20850
- if (e2.key === "Enter") {
20851
- e2.preventDefault();
20852
- commit();
20853
- dispatch({ kind: "addItem", targetId: node.id, position: "siblingAfter" });
20854
- } else if (e2.key === "Backspace" && draft.length === 0) {
20855
- e2.preventDefault();
20856
- dispatch({ kind: "removeItem", id: node.id });
20857
- } else if (e2.key === "Tab") {
20858
- e2.preventDefault();
20859
- commit();
20860
- dispatch({ kind: e2.shiftKey ? "outdentItem" : "indentItem", id: node.id });
21325
+ return /* @__PURE__ */ jsxs33("li", { className: itemClassName, role: "treeitem", style: { paddingLeft: `${depth * 18}px` }, children: [
21326
+ /* @__PURE__ */ jsxs33(
21327
+ "div",
21328
+ {
21329
+ className: "squisq-tree-row",
21330
+ onDragOver: (event) => onDragOver(event, node.id),
21331
+ onDrop: (event) => onDrop(event, node.id),
21332
+ children: [
21333
+ hasChildren ? /* @__PURE__ */ jsx44(
21334
+ "button",
21335
+ {
21336
+ type: "button",
21337
+ className: "squisq-tree-chevron",
21338
+ "aria-label": isCollapsed ? "Expand" : "Collapse",
21339
+ onClick: () => toggleCollapse(node.id),
21340
+ children: /* @__PURE__ */ jsx44(Icon, { icon: `fa-solid ${isCollapsed ? "fa-chevron-right" : "fa-chevron-down"}` })
20861
21341
  }
20862
- }
20863
- }
20864
- ),
20865
- /* @__PURE__ */ jsxs33("span", { className: "squisq-tree-controls", children: [
20866
- /* @__PURE__ */ jsx44(
20867
- "button",
20868
- {
20869
- type: "button",
20870
- title: "Add child",
20871
- onClick: () => dispatch({ kind: "addItem", targetId: node.id, position: "child" }),
20872
- children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-plus" })
20873
- }
20874
- ),
20875
- /* @__PURE__ */ jsx44(
20876
- "button",
20877
- {
20878
- type: "button",
20879
- title: "Outdent",
20880
- onClick: () => dispatch({ kind: "outdentItem", id: node.id }),
20881
- children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-outdent" })
20882
- }
20883
- ),
20884
- /* @__PURE__ */ jsx44(
20885
- "button",
20886
- {
20887
- type: "button",
20888
- title: "Indent",
20889
- onClick: () => dispatch({ kind: "indentItem", id: node.id }),
20890
- children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-indent" })
20891
- }
20892
- ),
20893
- /* @__PURE__ */ jsx44(
20894
- "button",
20895
- {
20896
- type: "button",
20897
- title: "Move up",
20898
- onClick: () => dispatch({ kind: "moveItemUp", id: node.id }),
20899
- children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-arrow-up" })
20900
- }
20901
- ),
20902
- /* @__PURE__ */ jsx44(
20903
- "button",
20904
- {
20905
- type: "button",
20906
- title: "Move down",
20907
- onClick: () => dispatch({ kind: "moveItemDown", id: node.id }),
20908
- children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-arrow-down" })
20909
- }
20910
- ),
20911
- /* @__PURE__ */ jsx44(
20912
- "button",
20913
- {
20914
- type: "button",
20915
- title: "Delete",
20916
- className: "squisq-tree-delete",
20917
- onClick: () => dispatch({ kind: "removeItem", id: node.id }),
20918
- children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-trash" })
20919
- }
20920
- )
20921
- ] })
20922
- ] }),
21342
+ ) : /* @__PURE__ */ jsx44("span", { className: "squisq-tree-chevron squisq-tree-chevron--empty" }),
21343
+ /* @__PURE__ */ jsx44(
21344
+ "button",
21345
+ {
21346
+ type: "button",
21347
+ className: "squisq-tree-icon",
21348
+ title: isDir ? "Make a file" : "Make a folder",
21349
+ onClick: () => dispatch({ kind: "toggleDir", id: node.id }),
21350
+ children: /* @__PURE__ */ jsx44(Icon, { icon: `fa-solid ${isDir ? "fa-folder" : "fa-file"}` })
21351
+ }
21352
+ ),
21353
+ /* @__PURE__ */ jsx44(
21354
+ "input",
21355
+ {
21356
+ className: "squisq-tree-label",
21357
+ value: draft,
21358
+ onChange: (e2) => setDraft(e2.target.value),
21359
+ onBlur: commit,
21360
+ onKeyDown: (e2) => {
21361
+ if (e2.key === "Enter") {
21362
+ e2.preventDefault();
21363
+ commit();
21364
+ dispatch({ kind: "addItem", targetId: node.id, position: "siblingAfter" });
21365
+ } else if (e2.key === "Backspace" && draft.length === 0) {
21366
+ e2.preventDefault();
21367
+ dispatch({ kind: "removeItem", id: node.id });
21368
+ } else if (e2.key === "Tab") {
21369
+ e2.preventDefault();
21370
+ commit();
21371
+ dispatch({ kind: e2.shiftKey ? "outdentItem" : "indentItem", id: node.id });
21372
+ }
21373
+ }
21374
+ }
21375
+ ),
21376
+ /* @__PURE__ */ jsxs33("span", { className: "squisq-tree-controls", children: [
21377
+ /* @__PURE__ */ jsx44(
21378
+ "span",
21379
+ {
21380
+ className: "squisq-tree-drag-handle",
21381
+ draggable: true,
21382
+ "aria-hidden": "true",
21383
+ title: `Drag ${node.label} to move`,
21384
+ onDragStart: (event) => onDragStart(event, node.id),
21385
+ onDragEnd,
21386
+ children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-grip-vertical" })
21387
+ }
21388
+ ),
21389
+ /* @__PURE__ */ jsx44(
21390
+ "button",
21391
+ {
21392
+ type: "button",
21393
+ title: "Add child",
21394
+ onClick: () => dispatch({ kind: "addItem", targetId: node.id, position: "child" }),
21395
+ children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-plus" })
21396
+ }
21397
+ ),
21398
+ /* @__PURE__ */ jsx44(
21399
+ "button",
21400
+ {
21401
+ type: "button",
21402
+ title: "Outdent",
21403
+ onClick: () => dispatch({ kind: "outdentItem", id: node.id }),
21404
+ children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-outdent" })
21405
+ }
21406
+ ),
21407
+ /* @__PURE__ */ jsx44(
21408
+ "button",
21409
+ {
21410
+ type: "button",
21411
+ title: "Indent",
21412
+ onClick: () => dispatch({ kind: "indentItem", id: node.id }),
21413
+ children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-indent" })
21414
+ }
21415
+ ),
21416
+ /* @__PURE__ */ jsx44(
21417
+ "button",
21418
+ {
21419
+ type: "button",
21420
+ title: "Move up",
21421
+ onClick: () => dispatch({ kind: "moveItemUp", id: node.id }),
21422
+ children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-arrow-up" })
21423
+ }
21424
+ ),
21425
+ /* @__PURE__ */ jsx44(
21426
+ "button",
21427
+ {
21428
+ type: "button",
21429
+ title: "Move down",
21430
+ onClick: () => dispatch({ kind: "moveItemDown", id: node.id }),
21431
+ children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-arrow-down" })
21432
+ }
21433
+ ),
21434
+ /* @__PURE__ */ jsx44(
21435
+ "button",
21436
+ {
21437
+ type: "button",
21438
+ title: "Delete",
21439
+ className: "squisq-tree-delete",
21440
+ onClick: () => dispatch({ kind: "removeItem", id: node.id }),
21441
+ children: /* @__PURE__ */ jsx44(Icon, { icon: "fa-solid fa-trash" })
21442
+ }
21443
+ )
21444
+ ] })
21445
+ ]
21446
+ }
21447
+ ),
20923
21448
  hasChildren && !isCollapsed ? /* @__PURE__ */ jsx44("ul", { className: "squisq-tree-rows", role: "group", children: node.children.map((child) => /* @__PURE__ */ jsx44(
20924
21449
  TreeRowView,
20925
21450
  {
20926
21451
  node: child,
20927
21452
  depth: depth + 1,
20928
21453
  collapsed,
21454
+ draggedId,
21455
+ dropTarget,
20929
21456
  toggleCollapse,
20930
- dispatch
21457
+ dispatch,
21458
+ onDragStart,
21459
+ onDragOver,
21460
+ onDrop,
21461
+ onDragEnd
20931
21462
  },
20932
21463
  child.id
20933
21464
  )) }) : null
@@ -21118,16 +21649,16 @@ import {
21118
21649
  } from "@bendyline/squisq/doc";
21119
21650
 
21120
21651
  // src/timeline/TimelineEditorWidget.tsx
21121
- import { useCallback as useCallback35, useEffect as useEffect32, useMemo as useMemo29, useRef as useRef31, useState as useState40 } from "react";
21652
+ import { useCallback as useCallback35, useEffect as useEffect32, useMemo as useMemo29, useRef as useRef32, useState as useState40 } from "react";
21122
21653
  import {
21123
21654
  asciiTimelineToTemplateData
21124
21655
  } from "@bendyline/squisq/doc";
21125
21656
 
21126
21657
  // src/timeline/timelineData.ts
21127
- import { useEffect as useEffect31, useMemo as useMemo28, useRef as useRef30, useState as useState39 } from "react";
21658
+ import { useEffect as useEffect31, useMemo as useMemo28, useRef as useRef31, useState as useState39 } from "react";
21128
21659
  function useTimelineData(editor, blockId) {
21129
21660
  const [version, setVersion] = useState39(0);
21130
- const dataCache = useRef30(null);
21661
+ const dataCache = useRef31(null);
21131
21662
  useEffect31(() => {
21132
21663
  const onEditorChange = () => setVersion((value) => value + 1);
21133
21664
  editor.on("transaction", onEditorChange);
@@ -21176,12 +21707,79 @@ function sanitizeTimelineText(value) {
21176
21707
  }
21177
21708
  function nextTimelineEventId(timeline, base = "event") {
21178
21709
  const used = new Set(timeline.tracks.flatMap((track) => track.events.map((event) => event.id)));
21179
- const safeBase = safeId(base) || "event";
21710
+ const safeBase = safeId(base, "event");
21711
+ if (!used.has(safeBase)) return safeBase;
21712
+ let suffix = 2;
21713
+ while (used.has(`${safeBase}-${suffix}`)) suffix++;
21714
+ return `${safeBase}-${suffix}`;
21715
+ }
21716
+ function nextTimelineTrackId(timeline, base = "track") {
21717
+ const used = new Set(timeline.tracks.map((track) => track.id));
21718
+ const safeBase = safeId(base, "track");
21180
21719
  if (!used.has(safeBase)) return safeBase;
21181
21720
  let suffix = 2;
21182
21721
  while (used.has(`${safeBase}-${suffix}`)) suffix++;
21183
21722
  return `${safeBase}-${suffix}`;
21184
21723
  }
21724
+ function addTimelineTrackOp(timeline, options = {}) {
21725
+ const label = sanitizeTimelineText(options.label ?? "") || "New line";
21726
+ const eventLabel = sanitizeTimelineText(options.eventLabel ?? "") || "New event";
21727
+ const trackId = nextTimelineTrackId(timeline, options.id ?? label);
21728
+ const eventId = nextTimelineEventId(timeline, options.eventId ?? eventLabel);
21729
+ const { start, span } = globalBounds(timeline);
21730
+ const column = Math.round((start + span / 2) * COORDINATE_PRECISION) / COORDINATE_PRECISION;
21731
+ const row = timeline.tracks.reduce((maximum, track2) => Math.max(maximum, track2.row), -1) + 1;
21732
+ const track = {
21733
+ id: trackId,
21734
+ label,
21735
+ row,
21736
+ startColumn: start,
21737
+ endColumn: start + span,
21738
+ events: [
21739
+ {
21740
+ id: eventId,
21741
+ label: eventLabel,
21742
+ column,
21743
+ side: "above",
21744
+ marker: "filled"
21745
+ }
21746
+ ]
21747
+ };
21748
+ return {
21749
+ timeline: {
21750
+ ...timeline,
21751
+ tracks: [...timeline.tracks, track],
21752
+ width: Math.max(timeline.width, Math.ceil(track.endColumn) + 1),
21753
+ height: Math.max(timeline.height, row + 1)
21754
+ },
21755
+ trackId,
21756
+ eventId
21757
+ };
21758
+ }
21759
+ function updateTimelineTrackOp(timeline, trackId, label) {
21760
+ const track = timeline.tracks.find((candidate) => candidate.id === trackId);
21761
+ const nextLabel = sanitizeTimelineText(label);
21762
+ if (!track || !nextLabel || track.label === nextLabel) return timeline;
21763
+ return {
21764
+ ...timeline,
21765
+ tracks: timeline.tracks.map(
21766
+ (candidate) => candidate.id === trackId ? { ...candidate, label: nextLabel } : candidate
21767
+ )
21768
+ };
21769
+ }
21770
+ function removeTimelineTrackOp(timeline, trackId) {
21771
+ if (timeline.tracks.length <= 1) return timeline;
21772
+ const track = timeline.tracks.find((candidate) => candidate.id === trackId);
21773
+ if (!track) return timeline;
21774
+ const removedEventIds = new Set(track.events.map((event) => event.id));
21775
+ return {
21776
+ ...timeline,
21777
+ tracks: timeline.tracks.filter((candidate) => candidate.id !== trackId),
21778
+ links: timeline.links.filter(
21779
+ (link) => !removedEventIds.has(link.source) && !removedEventIds.has(link.target)
21780
+ )
21781
+ };
21782
+ }
21185
21783
  function addTimelineEventOp(timeline, trackId, position, options = {}) {
21186
21784
  if (!Number.isFinite(position)) return null;
21187
21785
  const track = timeline.tracks.find((candidate) => candidate.id === trackId);
@@ -21284,8 +21882,8 @@ function columnAtPosition(timeline, position) {
21284
21882
  const clamped = Math.max(0, Math.min(1, position));
21285
21883
  return Math.round((start + clamped * span) * COORDINATE_PRECISION) / COORDINATE_PRECISION;
21286
21884
  }
21287
- function safeId(value) {
21288
- return sanitizeTimelineText(value).toLowerCase().replace(/[^a-z0-9_.~-]+/g, "-").replace(/^-+|-+$/g, "") || "event";
21885
+ function safeId(value, fallback) {
21886
+ return sanitizeTimelineText(value).toLowerCase().replace(/[^a-z0-9_.~-]+/g, "-").replace(/^-+|-+$/g, "") || fallback;
21289
21887
  }
21290
21888
  function findEvent(timeline, eventId) {
21291
21889
  for (const track of timeline.tracks) {
@@ -21308,6 +21906,9 @@ function countEvents(timeline) {
21308
21906
  function hasEvent(timeline, eventId) {
21309
21907
  return timeline.tracks.some((track) => track.events.some((event) => event.id === eventId));
21310
21908
  }
21909
+ function hasTrack(timeline, trackId) {
21910
+ return timeline.tracks.some((track) => track.id === trackId);
21911
+ }
21311
21912
  function semanticSignature(timeline) {
21312
21913
  return JSON.stringify({
21313
21914
  style: timeline.style,
@@ -21460,6 +22061,8 @@ function verifyRenderedTimeline(next, rendered) {
21460
22061
  const ids = new Set(
21461
22062
  verification.tracks.flatMap((track) => track.events.map((event) => event.id))
21462
22063
  );
22064
+ const trackIds = new Set(verification.tracks.map((track) => track.id));
22065
+ if (trackIds.size !== verification.tracks.length) return null;
21463
22066
  if (ids.size !== countEvents(verification)) return null;
21464
22067
  if (verification.links.some((link) => !ids.has(link.source) || !ids.has(link.target))) {
21465
22068
  return null;
@@ -21478,18 +22081,50 @@ function applyOp3(editor, blockId, op, verifyResult) {
21478
22081
  if (!result || result.timeline === timeline) return NOT_APPLIED;
21479
22082
  const rendered = renderAsciiTimeline(result.timeline);
21480
22083
  const verification = verifyRenderedTimeline(result.timeline, rendered);
21481
- if (!verification || result.eventId && !hasEvent(verification, result.eventId) || verifyResult && !verifyResult(verification)) {
22084
+ if (!verification || result.eventId && !hasEvent(verification, result.eventId) || result.trackId && !hasTrack(verification, result.trackId) || verifyResult && !verifyResult(verification)) {
21482
22085
  return NOT_APPLIED;
21483
22086
  }
21484
22087
  if (!replaceAsciiFenceText(editor, pos, rendered, "timeline")) return NOT_APPLIED;
21485
22088
  return {
21486
22089
  applied: true,
21487
- ...result.eventId ? { eventId: result.eventId } : {}
22090
+ ...result.eventId ? { eventId: result.eventId } : {},
22091
+ ...result.trackId ? { trackId: result.trackId } : {}
21488
22092
  };
21489
22093
  }
21490
22094
  function applyTimelineCommand(editor, blockId, command2) {
21491
22095
  if (!editor.isEditable) return READ_ONLY;
21492
22096
  switch (command2.kind) {
22097
+ case "addTrack":
22098
+ return applyOp3(
22099
+ editor,
22100
+ blockId,
22101
+ (timeline) => addTimelineTrackOp(timeline, {
22102
+ id: command2.id,
22103
+ label: command2.label,
22104
+ eventId: command2.eventId,
22105
+ eventLabel: command2.eventLabel
22106
+ })
22107
+ );
22108
+ case "updateTrack": {
22109
+ const expectedLabel = sanitizeTimelineText(command2.label);
22110
+ return applyOp3(
22111
+ editor,
22112
+ blockId,
22113
+ (timeline) => ({
22114
+ timeline: updateTimelineTrackOp(timeline, command2.trackId, command2.label)
22115
+ }),
22116
+ (timeline) => timeline.tracks.some(
22117
+ (track) => track.id === command2.trackId && track.label === expectedLabel
22118
+ )
22119
+ );
22120
+ }
22121
+ case "removeTrack":
22122
+ return applyOp3(
22123
+ editor,
22124
+ blockId,
22125
+ (timeline) => ({ timeline: removeTimelineTrackOp(timeline, command2.trackId) }),
22126
+ (timeline) => !hasTrack(timeline, command2.trackId)
22127
+ );
21493
22128
  case "addEvent":
21494
22129
  return applyOp3(editor, blockId, (timeline) => {
21495
22130
  const result = addTimelineEventOp(timeline, command2.trackId, command2.position, {
@@ -21526,14 +22161,16 @@ function applyTimelineCommand(editor, blockId, command2) {
21526
22161
  }
21527
22162
 
21528
22163
  // src/timeline/TimelineEditorWidget.tsx
21529
- import { jsx as jsx45, jsxs as jsxs34 } from "react/jsx-runtime";
22164
+ import { Fragment as Fragment15, jsx as jsx45, jsxs as jsxs34 } from "react/jsx-runtime";
21530
22165
  var clamp01 = (value) => Math.max(0, Math.min(1, value));
21531
22166
  function TimelineEditorWidget({ editor, blockId }) {
21532
22167
  const view = useTimelineData(editor, blockId);
21533
22168
  const [selectedEventId, setSelectedEventId] = useState40(null);
22169
+ const [addingTrackId, setAddingTrackId] = useState40(null);
22170
+ const [editingTrackId, setEditingTrackId] = useState40(null);
21534
22171
  const [hover, setHover] = useState40(null);
21535
22172
  const [dragged, setDragged] = useState40(null);
21536
- const suppressClickEventId = useRef31(null);
22173
+ const suppressClickEventId = useRef32(null);
21537
22174
  const [announcement, setAnnouncement] = useState40("");
21538
22175
  const dispatch = useCallback35(
21539
22176
  (command2) => {
@@ -21556,6 +22193,8 @@ function TimelineEditorWidget({ editor, blockId }) {
21556
22193
  // editor's cursor/selection hot path.
21557
22194
  [sourceText, sourceTimeline]
21558
22195
  );
22196
+ const editorEditable = editor.isEditable;
22197
+ const editable = editorEditable && sourceSafe;
21559
22198
  const positioned = useMemo29(() => {
21560
22199
  if (!view) return [];
21561
22200
  const normalized = asciiTimelineToTemplateData(view.timeline).tracks;
@@ -21603,22 +22242,58 @@ function TimelineEditorWidget({ editor, blockId }) {
21603
22242
  if (selectedEventId && ids.has(selectedEventId)) return;
21604
22243
  setSelectedEventId(view.timeline.tracks[0]?.events[0]?.id ?? null);
21605
22244
  }, [selectedEventId, view]);
22245
+ useEffect32(() => {
22246
+ if (editable) return;
22247
+ setAddingTrackId(null);
22248
+ setEditingTrackId(null);
22249
+ setHover(null);
22250
+ }, [editable]);
22251
+ useEffect32(() => {
22252
+ if (!editingTrackId || view?.timeline.tracks.some((track) => track.id === editingTrackId)) {
22253
+ return;
22254
+ }
22255
+ setEditingTrackId(null);
22256
+ }, [editingTrackId, view]);
21606
22257
  if (!view) return null;
21607
- const editorEditable = editor.isEditable;
21608
- const editable = editorEditable && sourceSafe;
21609
22258
  const totalEvents = positioned.length;
22259
+ const addTrack = () => {
22260
+ if (!editable) return;
22261
+ const result = dispatch({ kind: "addTrack" });
22262
+ if (result.applied && result.eventId && result.trackId) {
22263
+ setAddingTrackId(null);
22264
+ setEditingTrackId(result.trackId);
22265
+ setHover(null);
22266
+ setSelectedEventId(result.eventId);
22267
+ setAnnouncement("New timeline line added. Edit its name on the line.");
22268
+ }
22269
+ };
21610
22270
  const addEvent = (trackId, position) => {
21611
22271
  if (!editable) return;
21612
22272
  const result = dispatch({ kind: "addEvent", trackId, position: clamp01(position) });
21613
22273
  if (result.applied && result.eventId) {
22274
+ setAddingTrackId(null);
22275
+ setEditingTrackId(null);
22276
+ setHover(null);
21614
22277
  setSelectedEventId(result.eventId);
21615
22278
  setAnnouncement("New timeline point added. Edit its text below.");
21616
22279
  }
21617
22280
  };
21618
22281
  const selectEvent = (event, trackLabel) => {
22282
+ setAddingTrackId(null);
22283
+ setHover(null);
21619
22284
  setSelectedEventId(event.id);
21620
22285
  setAnnouncement(`${event.label || "Timeline point"} selected on ${trackLabel}.`);
21621
22286
  };
22287
+ const deleteTrack = (trackId, trackLabel) => {
22288
+ const remaining = positioned.filter((point) => point.trackId !== trackId);
22289
+ const result = dispatch({ kind: "removeTrack", trackId });
22290
+ if (!result.applied) return;
22291
+ setAddingTrackId(null);
22292
+ setEditingTrackId((current) => current === trackId ? null : current);
22293
+ setHover(null);
22294
+ if (selected?.trackId === trackId) setSelectedEventId(remaining[0]?.event.id ?? null);
22295
+ setAnnouncement(`${trackLabel} line deleted.`);
22296
+ };
21622
22297
  const dragPosition = (event) => {
21623
22298
  const rail = event.currentTarget.closest(".squisq-ascii-timeline-rail");
21624
22299
  if (!rail) return null;
@@ -21626,291 +22301,408 @@ function TimelineEditorWidget({ editor, blockId }) {
21626
22301
  if (rect.width <= 0 || !Number.isFinite(event.clientX)) return null;
21627
22302
  return clamp01((event.clientX - rect.left) / rect.width);
21628
22303
  };
21629
- return /* @__PURE__ */ jsxs34("section", { className: "squisq-ascii-timeline-editor", "aria-label": "Timeline editor", children: [
21630
- /* @__PURE__ */ jsxs34("header", { className: "squisq-ascii-timeline-header", children: [
21631
- /* @__PURE__ */ jsxs34("span", { children: [
21632
- /* @__PURE__ */ jsx45(Icon, { icon: "fa-solid fa-timeline" }),
21633
- " Timeline"
21634
- ] }),
21635
- editable ? /* @__PURE__ */ jsx45("small", { children: "Click the line to add a point \xB7 Drag dots to move" }) : editorEditable ? /* @__PURE__ */ jsx45("small", { children: "Source repair needed" }) : /* @__PURE__ */ jsx45("small", { children: "Read only" })
21636
- ] }),
21637
- /* @__PURE__ */ jsxs34(
21638
- "div",
21639
- {
21640
- className: "squisq-ascii-timeline-canvas",
21641
- style: { "--squisq-ascii-timeline-track-count": view.timeline.tracks.length },
21642
- children: [
21643
- view.timeline.links.length > 0 ? /* @__PURE__ */ jsx45(
21644
- "svg",
21645
- {
21646
- className: "squisq-ascii-timeline-branches",
21647
- viewBox: `0 0 100 ${Math.max(1, view.timeline.tracks.length) * 96}`,
21648
- preserveAspectRatio: "none",
21649
- "aria-hidden": "true",
21650
- children: view.timeline.links.map((link, index) => {
21651
- const source = displayedPositioned.find((point) => point.event.id === link.source);
21652
- const target = displayedPositioned.find((point) => point.event.id === link.target);
21653
- if (!source || !target) return null;
21654
- const sx = source.position * 100;
21655
- const tx = target.position * 100;
21656
- const sy = source.trackIndex * 96 + 48;
21657
- const ty = target.trackIndex * 96 + 48;
21658
- const sameTrack = source.trackIndex === target.trackIndex;
21659
- const controlY = sameTrack ? sy + (source.event.side === "below" ? -30 : 30) : (sy + ty) / 2;
21660
- return /* @__PURE__ */ jsx45(
21661
- "path",
21662
- {
21663
- d: `M ${sx} ${sy} C ${sx} ${controlY}, ${tx} ${controlY}, ${tx} ${ty}`,
21664
- vectorEffect: "non-scaling-stroke"
21665
- },
21666
- `${link.source}-${link.target}-${index}`
21667
- );
21668
- })
21669
- }
21670
- ) : null,
21671
- view.timeline.tracks.map((track, trackIndex) => {
21672
- const trackLabel = track.label || `Track ${trackIndex + 1}`;
21673
- const events = track.events.map((event) => ({ event, position: positionById.get(event.id) ?? 0 })).sort((a, b) => a.position - b.position);
21674
- const eventPositions = events.map((entry) => entry.position);
21675
- const gaps = insertionGaps(eventPositions);
21676
- return /* @__PURE__ */ jsxs34("div", { className: "squisq-ascii-timeline-track", children: [
21677
- /* @__PURE__ */ jsxs34("div", { className: "squisq-ascii-timeline-track-label", title: trackLabel, children: [
21678
- /* @__PURE__ */ jsx45("span", { children: trackLabel }),
21679
- editable ? /* @__PURE__ */ jsx45(
21680
- "button",
21681
- {
21682
- type: "button",
21683
- className: "squisq-ascii-timeline-track-add",
21684
- "aria-label": `Add point to ${trackLabel} timeline`,
21685
- title: "Add point in the largest available gap",
21686
- onClick: () => addEvent(track.id, largestGap(eventPositions)),
21687
- children: "+"
21688
- }
21689
- ) : null
21690
- ] }),
21691
- /* @__PURE__ */ jsxs34(
21692
- "div",
22304
+ return /* @__PURE__ */ jsxs34(
22305
+ "section",
22306
+ {
22307
+ className: "squisq-ascii-timeline-editor",
22308
+ "aria-label": "Timeline editor",
22309
+ onKeyDown: (event) => {
22310
+ if (event.key !== "Escape" || !addingTrackId) return;
22311
+ setAddingTrackId(null);
22312
+ setHover(null);
22313
+ setAnnouncement("Add point cancelled.");
22314
+ },
22315
+ children: [
22316
+ /* @__PURE__ */ jsxs34("header", { className: "squisq-ascii-timeline-header", children: [
22317
+ /* @__PURE__ */ jsxs34("span", { children: [
22318
+ /* @__PURE__ */ jsx45(Icon, { icon: "fa-solid fa-timeline" }),
22319
+ " Timeline"
22320
+ ] }),
22321
+ /* @__PURE__ */ jsxs34("div", { className: "squisq-ascii-timeline-header-actions", children: [
22322
+ editable && addingTrackId ? /* @__PURE__ */ jsx45("small", { children: "Click the line to add a point \xB7 Esc to cancel" }) : editable ? /* @__PURE__ */ jsx45("small", { children: "Use + to add a point \xB7 Drag dots to move" }) : editorEditable ? /* @__PURE__ */ jsx45("small", { children: "Source repair needed" }) : /* @__PURE__ */ jsx45("small", { children: "Read only" }),
22323
+ editable ? /* @__PURE__ */ jsxs34("button", { type: "button", onClick: addTrack, children: [
22324
+ /* @__PURE__ */ jsx45(Icon, { icon: "fa-solid fa-plus" }),
22325
+ " Add line"
22326
+ ] }) : null
22327
+ ] })
22328
+ ] }),
22329
+ /* @__PURE__ */ jsxs34(
22330
+ "div",
22331
+ {
22332
+ className: "squisq-ascii-timeline-canvas",
22333
+ style: { "--squisq-ascii-timeline-track-count": view.timeline.tracks.length },
22334
+ children: [
22335
+ view.timeline.links.length > 0 ? /* @__PURE__ */ jsx45(
22336
+ "svg",
21693
22337
  {
21694
- className: `squisq-ascii-timeline-rail${editable ? "" : " squisq-ascii-timeline-rail--readonly"}`,
21695
- role: "group",
21696
- "aria-label": `${trackLabel} timeline rail`,
21697
- title: editable ? "Click anywhere on the line to add a point" : void 0,
21698
- onPointerMove: (event) => {
21699
- if (!editable || dragged) return;
21700
- const rect = event.currentTarget.getBoundingClientRect();
21701
- if (rect.width <= 0) return;
21702
- setHover({
21703
- trackId: track.id,
21704
- position: clamp01((event.clientX - rect.left) / rect.width)
21705
- });
21706
- },
21707
- onPointerLeave: () => setHover((current) => current?.trackId === track.id ? null : current),
21708
- onClick: (event) => {
21709
- if (!editable || event.target !== event.currentTarget) return;
21710
- const rect = event.currentTarget.getBoundingClientRect();
21711
- if (rect.width <= 0) return;
21712
- addEvent(track.id, (event.clientX - rect.left) / rect.width);
21713
- },
21714
- children: [
21715
- /* @__PURE__ */ jsx45("span", { className: "squisq-ascii-timeline-rail-line", "aria-hidden": "true" }),
21716
- hover?.trackId === track.id && !dragged ? /* @__PURE__ */ jsx45(
21717
- "span",
22338
+ className: "squisq-ascii-timeline-branches",
22339
+ viewBox: `0 0 100 ${Math.max(1, view.timeline.tracks.length) * 96}`,
22340
+ preserveAspectRatio: "none",
22341
+ "aria-hidden": "true",
22342
+ children: view.timeline.links.map((link, index) => {
22343
+ const source = displayedPositioned.find((point) => point.event.id === link.source);
22344
+ const target = displayedPositioned.find((point) => point.event.id === link.target);
22345
+ if (!source || !target) return null;
22346
+ const sx = source.position * 100;
22347
+ const tx = target.position * 100;
22348
+ const sy = source.trackIndex * 96 + 48;
22349
+ const ty = target.trackIndex * 96 + 48;
22350
+ const sameTrack = source.trackIndex === target.trackIndex;
22351
+ const controlY = sameTrack ? sy + (source.event.side === "below" ? -30 : 30) : (sy + ty) / 2;
22352
+ return /* @__PURE__ */ jsx45(
22353
+ "path",
21718
22354
  {
21719
- className: "squisq-ascii-timeline-add-ghost",
21720
- style: { left: `${hover.position * 100}%` },
21721
- "aria-hidden": "true",
21722
- children: "+"
21723
- }
21724
- ) : null,
21725
- editable && !dragged ? gaps.map((position) => /* @__PURE__ */ jsx45(
21726
- "button",
22355
+ d: `M ${sx} ${sy} C ${sx} ${controlY}, ${tx} ${controlY}, ${tx} ${ty}`,
22356
+ vectorEffect: "non-scaling-stroke"
22357
+ },
22358
+ `${link.source}-${link.target}-${index}`
22359
+ );
22360
+ })
22361
+ }
22362
+ ) : null,
22363
+ view.timeline.tracks.map((track, trackIndex) => {
22364
+ const trackLabel = track.label || `Track ${trackIndex + 1}`;
22365
+ const events = track.events.map((event) => ({ event, position: positionById.get(event.id) ?? 0 })).sort((a, b) => a.position - b.position);
22366
+ const eventPositions = events.map((entry) => entry.position);
22367
+ const gaps = insertionGaps(eventPositions);
22368
+ const addingPoint = editable && addingTrackId === track.id;
22369
+ return /* @__PURE__ */ jsxs34("div", { className: "squisq-ascii-timeline-track", children: [
22370
+ /* @__PURE__ */ jsxs34("div", { className: "squisq-ascii-timeline-track-label", children: [
22371
+ /* @__PURE__ */ jsx45(
22372
+ InlineTrackLabel,
21727
22373
  {
21728
- type: "button",
21729
- className: "squisq-ascii-timeline-gap-add",
21730
- style: { left: `${position * 100}%` },
21731
- "aria-label": `Add point to ${trackLabel} at ${Math.round(position * 100)} percent`,
21732
- title: "Add point",
21733
- onClick: (event) => {
21734
- event.stopPropagation();
21735
- addEvent(track.id, position);
22374
+ trackId: track.id,
22375
+ label: trackLabel,
22376
+ editing: editable && editingTrackId === track.id,
22377
+ editable,
22378
+ dispatch,
22379
+ onStart: () => {
22380
+ setAddingTrackId(null);
22381
+ setHover(null);
22382
+ setEditingTrackId(track.id);
21736
22383
  },
21737
- children: "+"
21738
- },
21739
- position
21740
- )) : null,
21741
- events.map(({ event, position }, eventIndex) => {
21742
- const selectedPoint = selectedEventId === event.id;
21743
- const draggedPoint = dragged?.eventId === event.id;
21744
- const side = event.side ?? (eventIndex % 2 === 0 ? "above" : "below");
21745
- const descriptionSide = event.descriptionSide ?? side;
21746
- return /* @__PURE__ */ jsxs34(
21747
- "span",
22384
+ onFinish: () => setEditingTrackId(null)
22385
+ }
22386
+ ),
22387
+ editable ? /* @__PURE__ */ jsxs34(Fragment15, { children: [
22388
+ /* @__PURE__ */ jsx45(
22389
+ "button",
21748
22390
  {
21749
- className: `squisq-ascii-timeline-point squisq-ascii-timeline-point--${side}${draggedPoint ? " squisq-ascii-timeline-point--dragging" : ""}`,
21750
- style: { left: `${position * 100}%` },
21751
- children: [
21752
- event.callout !== false ? /* @__PURE__ */ jsx45(
21753
- "span",
21754
- {
21755
- className: `squisq-ascii-timeline-callout squisq-ascii-timeline-callout--${side}`,
21756
- children: event.label
21757
- }
21758
- ) : null,
21759
- event.callout !== false && event.description ? /* @__PURE__ */ jsx45(
21760
- "span",
21761
- {
21762
- className: `squisq-ascii-timeline-description squisq-ascii-timeline-description--${descriptionSide}`,
21763
- children: event.description
21764
- }
21765
- ) : null,
21766
- /* @__PURE__ */ jsx45(
21767
- "button",
21768
- {
21769
- type: "button",
21770
- className: `squisq-ascii-timeline-marker squisq-ascii-timeline-marker--${event.marker ?? "filled"}${selectedPoint ? " squisq-ascii-timeline-marker--selected" : ""}`,
21771
- "aria-label": `Edit ${event.label || "timeline point"}, ${trackLabel}, ${Math.round(position * 100)} percent`,
21772
- "aria-pressed": selectedPoint,
21773
- "aria-roledescription": editable ? "draggable timeline point" : void 0,
21774
- title: editable ? `${event.description || event.label || "Timeline point"} \xB7 Drag to move` : event.description || event.label,
21775
- draggable: false,
21776
- onPointerDown: (pointerEvent) => {
21777
- if (!editable || pointerEvent.button !== void 0 && pointerEvent.button !== 0) {
21778
- return;
21779
- }
21780
- const point = dragPosition(pointerEvent);
21781
- if (point === null) return;
21782
- pointerEvent.stopPropagation();
21783
- setHover(null);
21784
- setSelectedEventId(event.id);
21785
- suppressClickEventId.current = null;
21786
- setDragged({
21787
- eventId: event.id,
21788
- pointerId: pointerEvent.pointerId,
21789
- startClientX: pointerEvent.clientX,
21790
- startPosition: position,
21791
- position,
21792
- moved: false
21793
- });
21794
- pointerEvent.currentTarget.setPointerCapture?.(pointerEvent.pointerId);
21795
- },
21796
- onPointerMove: (pointerEvent) => {
21797
- if (!editable || !draggedPoint || dragged.pointerId !== pointerEvent.pointerId) {
21798
- return;
21799
- }
21800
- const point = dragPosition(pointerEvent);
21801
- if (point === null) return;
21802
- pointerEvent.preventDefault();
21803
- pointerEvent.stopPropagation();
21804
- setDragged(
21805
- (current) => current?.eventId === event.id && current.pointerId === pointerEvent.pointerId ? {
21806
- ...current,
21807
- position: point,
21808
- moved: current.moved || Math.abs(pointerEvent.clientX - current.startClientX) >= 3
21809
- } : current
21810
- );
21811
- },
21812
- onPointerUp: (pointerEvent) => {
21813
- if (!draggedPoint || dragged.pointerId !== pointerEvent.pointerId) {
21814
- return;
21815
- }
21816
- const point = dragPosition(pointerEvent);
21817
- const finalPosition = point ?? dragged.position;
21818
- const moved = dragged.moved || Math.abs(pointerEvent.clientX - dragged.startClientX) >= 3;
21819
- pointerEvent.preventDefault();
21820
- pointerEvent.stopPropagation();
21821
- if (pointerEvent.currentTarget.hasPointerCapture?.(pointerEvent.pointerId)) {
21822
- pointerEvent.currentTarget.releasePointerCapture(
21823
- pointerEvent.pointerId
21824
- );
21825
- }
21826
- setDragged(null);
21827
- if (!moved) {
21828
- selectEvent(event, trackLabel);
21829
- return;
21830
- }
21831
- suppressClickEventId.current = event.id;
21832
- const result = dispatch({
21833
- kind: "updateEvent",
21834
- eventId: event.id,
21835
- patch: { position: finalPosition }
21836
- });
21837
- if (result.applied) {
21838
- setAnnouncement(
21839
- `${event.label || "Timeline point"} moved to ${Math.round(finalPosition * 100)} percent.`
21840
- );
21841
- } else if (!result.reason && Math.abs(finalPosition - dragged.startPosition) > 1e-3) {
21842
- setAnnouncement(
21843
- "Timeline points cannot overlap. Move the point elsewhere."
21844
- );
21845
- }
21846
- },
21847
- onPointerCancel: (pointerEvent) => {
21848
- if (!draggedPoint || dragged.pointerId !== pointerEvent.pointerId) {
21849
- return;
22391
+ type: "button",
22392
+ className: "squisq-ascii-timeline-track-delete",
22393
+ "aria-label": `Delete line: ${trackLabel}`,
22394
+ disabled: view.timeline.tracks.length <= 1,
22395
+ title: view.timeline.tracks.length > 1 ? `Delete ${trackLabel} line and all of its points` : "A timeline needs one line",
22396
+ onClick: () => deleteTrack(track.id, trackLabel),
22397
+ children: /* @__PURE__ */ jsx45(Icon, { icon: "fa-solid fa-trash" })
22398
+ }
22399
+ ),
22400
+ /* @__PURE__ */ jsx45(
22401
+ "button",
22402
+ {
22403
+ type: "button",
22404
+ className: "squisq-ascii-timeline-track-add",
22405
+ "aria-label": `Add point to ${trackLabel} timeline`,
22406
+ "aria-pressed": addingPoint,
22407
+ title: addingPoint ? "Cancel adding a point" : "Choose where to add a point",
22408
+ onClick: () => {
22409
+ setEditingTrackId(null);
22410
+ setAddingTrackId((current) => current === track.id ? null : track.id);
22411
+ setHover(null);
22412
+ setAnnouncement(
22413
+ addingPoint ? "Add point cancelled." : `Choose where to add a point on ${trackLabel}.`
22414
+ );
22415
+ },
22416
+ children: "+"
22417
+ }
22418
+ )
22419
+ ] }) : null
22420
+ ] }),
22421
+ /* @__PURE__ */ jsxs34(
22422
+ "div",
22423
+ {
22424
+ className: `squisq-ascii-timeline-rail${addingPoint ? " squisq-ascii-timeline-rail--adding" : ""}${editable ? "" : " squisq-ascii-timeline-rail--readonly"}`,
22425
+ role: "group",
22426
+ "aria-label": `${trackLabel} timeline rail`,
22427
+ title: addingPoint ? "Click anywhere on the line to add a point" : void 0,
22428
+ onPointerMove: (event) => {
22429
+ if (!addingPoint || dragged) return;
22430
+ const rect = event.currentTarget.getBoundingClientRect();
22431
+ if (rect.width <= 0) return;
22432
+ setHover({
22433
+ trackId: track.id,
22434
+ position: clamp01((event.clientX - rect.left) / rect.width)
22435
+ });
22436
+ },
22437
+ onPointerLeave: () => setHover((current) => current?.trackId === track.id ? null : current),
22438
+ onClick: (event) => {
22439
+ if (!addingPoint || event.target !== event.currentTarget) return;
22440
+ const rect = event.currentTarget.getBoundingClientRect();
22441
+ if (rect.width <= 0) return;
22442
+ addEvent(track.id, (event.clientX - rect.left) / rect.width);
22443
+ },
22444
+ children: [
22445
+ /* @__PURE__ */ jsx45("span", { className: "squisq-ascii-timeline-rail-line", "aria-hidden": "true" }),
22446
+ addingPoint && hover?.trackId === track.id && !dragged ? /* @__PURE__ */ jsx45(
22447
+ "span",
22448
+ {
22449
+ className: "squisq-ascii-timeline-add-ghost",
22450
+ style: { left: `${hover.position * 100}%` },
22451
+ "aria-hidden": "true",
22452
+ children: "+"
22453
+ }
22454
+ ) : null,
22455
+ addingPoint && !dragged ? gaps.map((position) => /* @__PURE__ */ jsx45(
22456
+ "button",
22457
+ {
22458
+ type: "button",
22459
+ className: "squisq-ascii-timeline-gap-add",
22460
+ style: { left: `${position * 100}%` },
22461
+ "aria-label": `Add point to ${trackLabel} at ${Math.round(position * 100)} percent`,
22462
+ title: "Add point",
22463
+ onClick: (event) => {
22464
+ event.stopPropagation();
22465
+ addEvent(track.id, position);
22466
+ },
22467
+ children: "+"
22468
+ },
22469
+ position
22470
+ )) : null,
22471
+ events.map(({ event, position }, eventIndex) => {
22472
+ const selectedPoint = selectedEventId === event.id;
22473
+ const draggedPoint = dragged?.eventId === event.id;
22474
+ const side = event.side ?? (eventIndex % 2 === 0 ? "above" : "below");
22475
+ const descriptionSide = event.descriptionSide ?? side;
22476
+ return /* @__PURE__ */ jsxs34(
22477
+ "span",
22478
+ {
22479
+ className: `squisq-ascii-timeline-point squisq-ascii-timeline-point--${side}${draggedPoint ? " squisq-ascii-timeline-point--dragging" : ""}`,
22480
+ style: { left: `${position * 100}%` },
22481
+ children: [
22482
+ event.callout !== false ? /* @__PURE__ */ jsx45(
22483
+ "span",
22484
+ {
22485
+ className: `squisq-ascii-timeline-callout squisq-ascii-timeline-callout--${side}`,
22486
+ children: event.label
21850
22487
  }
21851
- if (pointerEvent.currentTarget.hasPointerCapture?.(pointerEvent.pointerId)) {
21852
- pointerEvent.currentTarget.releasePointerCapture(
21853
- pointerEvent.pointerId
21854
- );
22488
+ ) : null,
22489
+ event.callout !== false && event.description ? /* @__PURE__ */ jsx45(
22490
+ "span",
22491
+ {
22492
+ className: `squisq-ascii-timeline-description squisq-ascii-timeline-description--${descriptionSide}`,
22493
+ children: event.description
21855
22494
  }
21856
- setDragged(null);
21857
- setAnnouncement("Timeline point move cancelled.");
21858
- },
21859
- onClick: (clickEvent) => {
21860
- clickEvent.stopPropagation();
21861
- if (suppressClickEventId.current === event.id) {
21862
- suppressClickEventId.current = null;
21863
- return;
22495
+ ) : null,
22496
+ /* @__PURE__ */ jsx45(
22497
+ "button",
22498
+ {
22499
+ type: "button",
22500
+ className: `squisq-ascii-timeline-marker squisq-ascii-timeline-marker--${event.marker ?? "filled"}${selectedPoint ? " squisq-ascii-timeline-marker--selected" : ""}`,
22501
+ "aria-label": `Edit ${event.label || "timeline point"}, ${trackLabel}, ${Math.round(position * 100)} percent`,
22502
+ "aria-pressed": selectedPoint,
22503
+ "aria-roledescription": editable ? "draggable timeline point" : void 0,
22504
+ title: editable ? `${event.description || event.label || "Timeline point"} \xB7 Drag to move` : event.description || event.label,
22505
+ draggable: false,
22506
+ onPointerDown: (pointerEvent) => {
22507
+ if (!editable || pointerEvent.button !== void 0 && pointerEvent.button !== 0) {
22508
+ return;
22509
+ }
22510
+ const point = dragPosition(pointerEvent);
22511
+ if (point === null) return;
22512
+ pointerEvent.stopPropagation();
22513
+ setAddingTrackId(null);
22514
+ setHover(null);
22515
+ setSelectedEventId(event.id);
22516
+ suppressClickEventId.current = null;
22517
+ setDragged({
22518
+ eventId: event.id,
22519
+ pointerId: pointerEvent.pointerId,
22520
+ startClientX: pointerEvent.clientX,
22521
+ startPosition: position,
22522
+ position,
22523
+ moved: false
22524
+ });
22525
+ pointerEvent.currentTarget.setPointerCapture?.(pointerEvent.pointerId);
22526
+ },
22527
+ onPointerMove: (pointerEvent) => {
22528
+ if (!editable || !draggedPoint || dragged.pointerId !== pointerEvent.pointerId) {
22529
+ return;
22530
+ }
22531
+ const point = dragPosition(pointerEvent);
22532
+ if (point === null) return;
22533
+ pointerEvent.preventDefault();
22534
+ pointerEvent.stopPropagation();
22535
+ setDragged(
22536
+ (current) => current?.eventId === event.id && current.pointerId === pointerEvent.pointerId ? {
22537
+ ...current,
22538
+ position: point,
22539
+ moved: current.moved || Math.abs(pointerEvent.clientX - current.startClientX) >= 3
22540
+ } : current
22541
+ );
22542
+ },
22543
+ onPointerUp: (pointerEvent) => {
22544
+ if (!draggedPoint || dragged.pointerId !== pointerEvent.pointerId) {
22545
+ return;
22546
+ }
22547
+ const point = dragPosition(pointerEvent);
22548
+ const finalPosition = point ?? dragged.position;
22549
+ const moved = dragged.moved || Math.abs(pointerEvent.clientX - dragged.startClientX) >= 3;
22550
+ pointerEvent.preventDefault();
22551
+ pointerEvent.stopPropagation();
22552
+ if (pointerEvent.currentTarget.hasPointerCapture?.(pointerEvent.pointerId)) {
22553
+ pointerEvent.currentTarget.releasePointerCapture(
22554
+ pointerEvent.pointerId
22555
+ );
22556
+ }
22557
+ setDragged(null);
22558
+ if (!moved) {
22559
+ selectEvent(event, trackLabel);
22560
+ return;
22561
+ }
22562
+ suppressClickEventId.current = event.id;
22563
+ const result = dispatch({
22564
+ kind: "updateEvent",
22565
+ eventId: event.id,
22566
+ patch: { position: finalPosition }
22567
+ });
22568
+ if (result.applied) {
22569
+ setAnnouncement(
22570
+ `${event.label || "Timeline point"} moved to ${Math.round(finalPosition * 100)} percent.`
22571
+ );
22572
+ } else if (!result.reason && Math.abs(finalPosition - dragged.startPosition) > 1e-3) {
22573
+ setAnnouncement(
22574
+ "Timeline points cannot overlap. Move the point elsewhere."
22575
+ );
22576
+ }
22577
+ },
22578
+ onPointerCancel: (pointerEvent) => {
22579
+ if (!draggedPoint || dragged.pointerId !== pointerEvent.pointerId) {
22580
+ return;
22581
+ }
22582
+ if (pointerEvent.currentTarget.hasPointerCapture?.(pointerEvent.pointerId)) {
22583
+ pointerEvent.currentTarget.releasePointerCapture(
22584
+ pointerEvent.pointerId
22585
+ );
22586
+ }
22587
+ setDragged(null);
22588
+ setAnnouncement("Timeline point move cancelled.");
22589
+ },
22590
+ onClick: (clickEvent) => {
22591
+ clickEvent.stopPropagation();
22592
+ if (suppressClickEventId.current === event.id) {
22593
+ suppressClickEventId.current = null;
22594
+ return;
22595
+ }
22596
+ selectEvent(event, trackLabel);
22597
+ },
22598
+ onKeyDown: (keyEvent) => navigateTrackPoints(keyEvent)
21864
22599
  }
21865
- selectEvent(event, trackLabel);
21866
- },
21867
- onKeyDown: (keyEvent) => navigateTrackPoints(keyEvent)
21868
- }
21869
- )
21870
- ]
21871
- },
21872
- event.id
21873
- );
21874
- }),
21875
- track.endLabel ? /* @__PURE__ */ jsx45("span", { className: "squisq-ascii-timeline-end-label", children: track.endLabel }) : null
21876
- ]
21877
- }
21878
- )
21879
- ] }, track.id);
21880
- })
21881
- ]
21882
- }
21883
- ),
21884
- view.timeline.links.length > 0 ? /* @__PURE__ */ jsx45("ul", { className: "squisq-ascii-timeline-link-list", "aria-label": "Timeline branches", children: view.timeline.links.map((link, index) => /* @__PURE__ */ jsxs34("li", { children: [
21885
- link.source,
21886
- " \u2192 ",
21887
- link.target,
21888
- link.label ? `: ${link.label}` : ""
21889
- ] }, `${link.source}-${link.target}-${index}`)) }) : null,
21890
- selected ? /* @__PURE__ */ jsx45(
21891
- EventInspector,
21892
- {
21893
- selected,
21894
- editable,
21895
- canDelete: totalEvents > 1,
21896
- dispatch,
21897
- onDelete: () => {
21898
- const remaining = positioned.filter((point) => point.event.id !== selected.event.id);
21899
- const result = dispatch({ kind: "removeEvent", eventId: selected.event.id });
21900
- if (result.applied) {
21901
- setSelectedEventId(remaining[0]?.event.id ?? null);
21902
- setAnnouncement(`${selected.event.label} deleted.`);
22600
+ )
22601
+ ]
22602
+ },
22603
+ event.id
22604
+ );
22605
+ }),
22606
+ track.endLabel ? /* @__PURE__ */ jsx45("span", { className: "squisq-ascii-timeline-end-label", children: track.endLabel }) : null
22607
+ ]
22608
+ }
22609
+ )
22610
+ ] }, track.id);
22611
+ })
22612
+ ]
21903
22613
  }
22614
+ ),
22615
+ view.timeline.links.length > 0 ? /* @__PURE__ */ jsx45("ul", { className: "squisq-ascii-timeline-link-list", "aria-label": "Timeline branches", children: view.timeline.links.map((link, index) => /* @__PURE__ */ jsxs34("li", { children: [
22616
+ link.source,
22617
+ " \u2192 ",
22618
+ link.target,
22619
+ link.label ? `: ${link.label}` : ""
22620
+ ] }, `${link.source}-${link.target}-${index}`)) }) : null,
22621
+ selected ? /* @__PURE__ */ jsx45(
22622
+ EventInspector,
22623
+ {
22624
+ selected,
22625
+ editable,
22626
+ canDelete: totalEvents > 1,
22627
+ dispatch,
22628
+ onDelete: () => {
22629
+ const remaining = positioned.filter((point) => point.event.id !== selected.event.id);
22630
+ const result = dispatch({ kind: "removeEvent", eventId: selected.event.id });
22631
+ if (result.applied) {
22632
+ setSelectedEventId(remaining[0]?.event.id ?? null);
22633
+ setAnnouncement(`${selected.event.label} deleted.`);
22634
+ }
22635
+ }
22636
+ },
22637
+ `${selected.trackId}:${selected.event.id}`
22638
+ ) : null,
22639
+ !sourceSafe ? /* @__PURE__ */ jsxs34("div", { className: "squisq-ascii-timeline-warnings", role: "status", children: [
22640
+ "Visual editing paused: this timeline contains source the canvas cannot safely rewrite. Repair it in Source view first.",
22641
+ view.warnings.length > 0 ? ` (${view.warnings.length} parser note${view.warnings.length === 1 ? "" : "s"})` : ""
22642
+ ] }) : null,
22643
+ /* @__PURE__ */ jsx45("div", { className: "squisq-sr-only", "aria-live": "polite", children: announcement })
22644
+ ]
22645
+ }
22646
+ );
22647
+ }
22648
+ function InlineTrackLabel({
22649
+ trackId,
22650
+ label,
22651
+ editing,
22652
+ editable,
22653
+ dispatch,
22654
+ onStart,
22655
+ onFinish
22656
+ }) {
22657
+ const [draft, setDraft] = useState40(label);
22658
+ useEffect32(() => {
22659
+ if (editing) setDraft(label);
22660
+ }, [editing, label]);
22661
+ const commitLabel = () => {
22662
+ const next = draft.trim();
22663
+ if (!next) {
22664
+ setDraft(label);
22665
+ } else if (next !== label) {
22666
+ const result = dispatch({ kind: "updateTrack", trackId, label: next });
22667
+ if (!result.applied) setDraft(label);
22668
+ }
22669
+ onFinish();
22670
+ };
22671
+ if (!editable) return /* @__PURE__ */ jsx45("span", { className: "squisq-ascii-timeline-track-name", children: label });
22672
+ return editing ? /* @__PURE__ */ jsx45(
22673
+ "input",
22674
+ {
22675
+ className: "squisq-ascii-timeline-track-name-input",
22676
+ "aria-label": `Rename line: ${label}`,
22677
+ value: draft,
22678
+ autoFocus: true,
22679
+ onFocus: (event) => event.currentTarget.select(),
22680
+ onChange: (event) => setDraft(event.target.value),
22681
+ onBlur: commitLabel,
22682
+ onKeyDown: (event) => {
22683
+ if (event.key === "Enter") {
22684
+ event.preventDefault();
22685
+ event.stopPropagation();
22686
+ commitLabel();
22687
+ } else if (event.key === "Escape") {
22688
+ event.preventDefault();
22689
+ event.stopPropagation();
22690
+ setDraft(label);
22691
+ onFinish();
21904
22692
  }
21905
- },
21906
- `${selected.trackId}:${selected.event.id}`
21907
- ) : null,
21908
- !sourceSafe ? /* @__PURE__ */ jsxs34("div", { className: "squisq-ascii-timeline-warnings", role: "status", children: [
21909
- "Visual editing paused: this timeline contains source the canvas cannot safely rewrite. Repair it in Source view first.",
21910
- view.warnings.length > 0 ? ` (${view.warnings.length} parser note${view.warnings.length === 1 ? "" : "s"})` : ""
21911
- ] }) : null,
21912
- /* @__PURE__ */ jsx45("div", { className: "squisq-sr-only", "aria-live": "polite", children: announcement })
21913
- ] });
22693
+ }
22694
+ }
22695
+ ) : /* @__PURE__ */ jsx45(
22696
+ "button",
22697
+ {
22698
+ type: "button",
22699
+ className: "squisq-ascii-timeline-track-name",
22700
+ "aria-label": `Rename line: ${label}`,
22701
+ title: `Rename ${label}`,
22702
+ onClick: onStart,
22703
+ children: label
22704
+ }
22705
+ );
21914
22706
  }
21915
22707
  function EventInspector({
21916
22708
  selected,
@@ -22075,19 +22867,6 @@ function insertionGaps(positions) {
22075
22867
  }
22076
22868
  return gaps;
22077
22869
  }
22078
- function largestGap(positions) {
22079
- if (positions.length === 0) return 0.5;
22080
- const sorted = [0, ...positions.map(clamp01).sort((a, b) => a - b), 1];
22081
- let start = sorted[0];
22082
- let end = sorted[1];
22083
- for (let index = 1; index < sorted.length - 1; index++) {
22084
- if (sorted[index + 1] - sorted[index] > end - start) {
22085
- start = sorted[index];
22086
- end = sorted[index + 1];
22087
- }
22088
- }
22089
- return (start + end) / 2;
22090
- }
22091
22870
  function navigateTrackPoints(event) {
22092
22871
  if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
22093
22872
  const rail = event.currentTarget.closest(".squisq-ascii-timeline-rail");
@@ -22475,7 +23254,7 @@ var InlineIcon = Node2.create({
22475
23254
  });
22476
23255
 
22477
23256
  // src/ImageNodeView.tsx
22478
- import { useEffect as useEffect33, useRef as useRef32, useState as useState41 } from "react";
23257
+ import { useEffect as useEffect33, useRef as useRef33, useState as useState41 } from "react";
22479
23258
  import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
22480
23259
  import Image2 from "@tiptap/extension-image";
22481
23260
 
@@ -22491,13 +23270,13 @@ function normalizeMalformedAssetUrl(src) {
22491
23270
  }
22492
23271
 
22493
23272
  // src/ImageNodeView.tsx
22494
- import { Fragment as Fragment15, jsx as jsx46, jsxs as jsxs35 } from "react/jsx-runtime";
23273
+ import { Fragment as Fragment16, jsx as jsx46, jsxs as jsxs35 } from "react/jsx-runtime";
22495
23274
  function ImageComponent({ node, selected, editor, updateAttributes: updateAttributes2 }) {
22496
23275
  const { src, alt, title, width } = node.attrs;
22497
23276
  const { mediaProvider, imageDisplayMode, openImageEdit, mediaRevision } = useEditorContext();
22498
23277
  const [resolvedSrc, setResolvedSrc] = useState41(src);
22499
23278
  const [hovered, setHovered] = useState41(false);
22500
- const imgRef = useRef32(null);
23279
+ const imgRef = useRef33(null);
22501
23280
  const [previewWidth, setPreviewWidth] = useState41(null);
22502
23281
  const isThumbnail = imageDisplayMode === "thumbnail";
22503
23282
  const isEditable = editor?.isEditable ?? true;
@@ -22619,7 +23398,7 @@ function ImageComponent({ node, selected, editor, updateAttributes: updateAttrib
22619
23398
  ]
22620
23399
  }
22621
23400
  ),
22622
- showResize && /* @__PURE__ */ jsxs35(Fragment15, { children: [
23401
+ showResize && /* @__PURE__ */ jsxs35(Fragment16, { children: [
22623
23402
  /* @__PURE__ */ jsx46(
22624
23403
  "span",
22625
23404
  {
@@ -22827,7 +23606,7 @@ var TiptapAudio = Node2.create({
22827
23606
  });
22828
23607
 
22829
23608
  // src/BlockPropertiesPopover.tsx
22830
- import { useEffect as useEffect35, useId as useId8, useRef as useRef33, useState as useState43 } from "react";
23609
+ import { useEffect as useEffect35, useId as useId8, useRef as useRef34, useState as useState43 } from "react";
22831
23610
  import { createPortal as createPortal10 } from "react-dom";
22832
23611
  import { jsx as jsx49, jsxs as jsxs36 } from "react/jsx-runtime";
22833
23612
  var TRANSITION_FLYOUT = ".squisq-transition-flyout";
@@ -22841,7 +23620,7 @@ function BlockPropertiesPopover({
22841
23620
  accentColor,
22842
23621
  onClose
22843
23622
  }) {
22844
- const panelRef = useRef33(null);
23623
+ const panelRef = useRef34(null);
22845
23624
  const panelId = `squisq-block-props-portal-${useId8().replace(/:/g, "")}`;
22846
23625
  const [inner, setInner] = useState43(blockAttrs);
22847
23626
  const [templateInner, setTemplateInner] = useState43(templateParams);
@@ -23383,19 +24162,19 @@ function WysiwygEditor({
23383
24162
  },
23384
24163
  [docTemplates, onDocTemplatesChange]
23385
24164
  );
23386
- const mentionProviderRef = useRef34(mentionProvider);
24165
+ const mentionProviderRef = useRef35(mentionProvider);
23387
24166
  useEffect36(() => {
23388
24167
  mentionProviderRef.current = mentionProvider;
23389
24168
  }, [mentionProvider]);
23390
24169
  const resolvedPlaceholder = useMemo30(() => placeholder ?? pickEmptyPrompt(), [placeholder]);
23391
- const isExternalUpdate = useRef34(false);
23392
- const lastSourceRef = useRef34(editorSource);
23393
- const mediaProviderRef = useRef34(mediaProvider);
24170
+ const isExternalUpdate = useRef35(false);
24171
+ const lastSourceRef = useRef35(editorSource);
24172
+ const mediaProviderRef = useRef35(mediaProvider);
23394
24173
  useEffect36(() => {
23395
24174
  mediaProviderRef.current = mediaProvider;
23396
24175
  }, [mediaProvider]);
23397
- const frontmatterRef = useRef34(stripFrontmatter(editorSource).frontmatter);
23398
- const submitOnEnterRef = useRef34(submitOnEnter);
24176
+ const frontmatterRef = useRef35(stripFrontmatter(editorSource).frontmatter);
24177
+ const submitOnEnterRef = useRef35(submitOnEnter);
23399
24178
  useEffect36(() => {
23400
24179
  submitOnEnterRef.current = submitOnEnter;
23401
24180
  }, [submitOnEnter]);
@@ -23584,7 +24363,7 @@ function WysiwygEditor({
23584
24363
  useEffect36(() => {
23585
24364
  if (editor) editor.setEditable(!readOnly);
23586
24365
  }, [editor, readOnly]);
23587
- const containerRef = useRef34(null);
24366
+ const containerRef = useRef35(null);
23588
24367
  const [badgeMenu, setBadgeMenu] = useState44(null);
23589
24368
  const [propsMenu, setPropsMenu] = useState44(null);
23590
24369
  const closeBadgeMenu = useCallback36(() => {
@@ -23861,7 +24640,7 @@ function moveSelectionToDropPoint(view, event) {
23861
24640
  }
23862
24641
 
23863
24642
  // src/InlinePreviewGutter.tsx
23864
- import { useLayoutEffect as useLayoutEffect4, useMemo as useMemo32, useRef as useRef35, useState as useState46 } from "react";
24643
+ import { useLayoutEffect as useLayoutEffect4, useMemo as useMemo32, useRef as useRef36, useState as useState46 } from "react";
23865
24644
  import { VIEWPORT_PRESETS as VIEWPORT_PRESETS4 } from "@bendyline/squisq/schemas";
23866
24645
  import {
23867
24646
  flattenBlocks as flattenBlocks4,
@@ -24071,7 +24850,7 @@ function sameEntries(a, b) {
24071
24850
  }
24072
24851
 
24073
24852
  // src/InlinePreviewGutter.tsx
24074
- import { Fragment as Fragment16, jsx as jsx51, jsxs as jsxs38 } from "react/jsx-runtime";
24853
+ import { Fragment as Fragment17, jsx as jsx51, jsxs as jsxs38 } from "react/jsx-runtime";
24075
24854
  function isAnnotated(block) {
24076
24855
  const annotation = block.sourceHeading?.templateAnnotation;
24077
24856
  if (!annotation) return false;
@@ -24237,7 +25016,7 @@ function InlinePreviewGutter({
24237
25016
  mediaProvider = null
24238
25017
  }) {
24239
25018
  const { doc } = useEditorContext();
24240
- const gutterRef = useRef35(null);
25019
+ const gutterRef = useRef36(null);
24241
25020
  const { entries: headingEntries, scrollToBlock } = useHeadingLayout(gutterRef);
24242
25021
  const previewSettings = usePreviewSettingsOptional();
24243
25022
  const activeTheme = previewSettings?.activeTheme ?? DEFAULT_THEME3;
@@ -24457,7 +25236,7 @@ function InlinePreviewGutter({
24457
25236
  children: [
24458
25237
  /* @__PURE__ */ jsxs38("div", { className: "squisq-inline-preview-card-label", children: [
24459
25238
  /* @__PURE__ */ jsx51("span", { className: "squisq-inline-preview-card-template", children: templateLabel(item.template) }),
24460
- item.headingText && /* @__PURE__ */ jsxs38(Fragment16, { children: [
25239
+ item.headingText && /* @__PURE__ */ jsxs38(Fragment17, { children: [
24461
25240
  /* @__PURE__ */ jsx51("span", { className: "squisq-inline-preview-card-sep", children: "\u2014" }),
24462
25241
  /* @__PURE__ */ jsx51("span", { className: "squisq-inline-preview-card-title", children: item.headingText })
24463
25242
  ] })
@@ -24870,7 +25649,7 @@ import {
24870
25649
  useCallback as useCallback38,
24871
25650
  useEffect as useEffect38,
24872
25651
  useMemo as useMemo34,
24873
- useRef as useRef36,
25652
+ useRef as useRef37,
24874
25653
  useState as useState47
24875
25654
  } from "react";
24876
25655
  import { flattenBlocks as flattenBlocks6, hasTemplate as hasTemplate3 } from "@bendyline/squisq/doc";
@@ -24986,10 +25765,10 @@ function OutlinePanel({ width, className, readOnly = false }) {
24986
25765
  goToBlockByLine,
24987
25766
  activeBlockStartLine
24988
25767
  } = useEditorContext();
24989
- const paneRef = useRef36(null);
25768
+ const paneRef = useRef37(null);
24990
25769
  const { scrollToBlock } = useHeadingLayout(paneRef);
24991
25770
  const cursorActiveId = useActiveOutlineBlockId();
24992
- const activeDragRef = useRef36(null);
25771
+ const activeDragRef = useRef37(null);
24993
25772
  const [draggedBlockId, setDraggedBlockId] = useState47(null);
24994
25773
  const [dropTarget, setDropTarget] = useState47(null);
24995
25774
  const blockModeActiveId = useMemo34(() => {
@@ -25335,7 +26114,7 @@ function bumpHeadingLevelInSource(source, line, delta) {
25335
26114
  }
25336
26115
 
25337
26116
  // src/codeContext/CodeContextZones.tsx
25338
- import { useCallback as useCallback40, useEffect as useEffect40, useMemo as useMemo36, useRef as useRef38, useState as useState48 } from "react";
26117
+ import { useCallback as useCallback40, useEffect as useEffect40, useMemo as useMemo36, useRef as useRef39, useState as useState48 } from "react";
25339
26118
  import { createPortal as createPortal11 } from "react-dom";
25340
26119
 
25341
26120
  // src/codeContext/diffContextSections.ts
@@ -25459,7 +26238,7 @@ function setOrdinal(zone, ordinal) {
25459
26238
  // src/codeContext/CodeContextSectionView.tsx
25460
26239
  import { parseMarkdown as parseMarkdown7 } from "@bendyline/squisq/markdown";
25461
26240
  import { MarkdownRenderer } from "@bendyline/squisq-react";
25462
- import { useCallback as useCallback39, useEffect as useEffect39, useMemo as useMemo35, useRef as useRef37 } from "react";
26241
+ import { useCallback as useCallback39, useEffect as useEffect39, useMemo as useMemo35, useRef as useRef38 } from "react";
25463
26242
  import { jsx as jsx55, jsxs as jsxs40 } from "react/jsx-runtime";
25464
26243
  function CodeContextSectionView({
25465
26244
  section,
@@ -25470,7 +26249,7 @@ function CodeContextSectionView({
25470
26249
  onRevealLine,
25471
26250
  onMeasure
25472
26251
  }) {
25473
- const rootRef = useRef37(null);
26252
+ const rootRef = useRef38(null);
25474
26253
  const stripNodes = useMemo35(
25475
26254
  () => parseMarkdown7(section.summaryMarkdown).children,
25476
26255
  [section.summaryMarkdown]
@@ -25548,13 +26327,13 @@ function CodeContextSectionView({
25548
26327
  }
25549
26328
 
25550
26329
  // src/codeContext/CodeContextZones.tsx
25551
- import { Fragment as Fragment17, jsx as jsx56 } from "react/jsx-runtime";
26330
+ import { Fragment as Fragment18, jsx as jsx56 } from "react/jsx-runtime";
25552
26331
  function CodeContextZones({ options }) {
25553
26332
  const { monacoEditor } = useEditorContext();
25554
26333
  const [manager, setManager] = useState48(null);
25555
26334
  const [, setZonesVersion] = useState48(0);
25556
26335
  const [expandedById, setExpandedById] = useState48({});
25557
- const seenIds = useRef38(/* @__PURE__ */ new Set());
26336
+ const seenIds = useRef39(/* @__PURE__ */ new Set());
25558
26337
  useEffect40(() => {
25559
26338
  if (!monacoEditor) return;
25560
26339
  const mgr = new CodeContextZoneManager(monacoEditor);
@@ -25617,7 +26396,7 @@ function CodeContextZones({ options }) {
25617
26396
  [monacoEditor]
25618
26397
  );
25619
26398
  if (!manager) return null;
25620
- return /* @__PURE__ */ jsx56(Fragment17, { children: resolved.map(({ section }) => {
26399
+ return /* @__PURE__ */ jsx56(Fragment18, { children: resolved.map(({ section }) => {
25621
26400
  const domNode = manager.getDomNode(section.id);
25622
26401
  if (!domNode) return null;
25623
26402
  return createPortal11(
@@ -25749,7 +26528,7 @@ function BlockCardView({
25749
26528
  }
25750
26529
 
25751
26530
  // src/TimelineTrack.tsx
25752
- import { useCallback as useCallback42, useEffect as useEffect42, useMemo as useMemo37, useRef as useRef40, useState as useState50 } from "react";
26531
+ import { useCallback as useCallback42, useEffect as useEffect42, useMemo as useMemo37, useRef as useRef41, useState as useState50 } from "react";
25753
26532
  import {
25754
26533
  resolveMediaSchedule,
25755
26534
  getDocPlaybackDuration,
@@ -25943,7 +26722,7 @@ function collectEmbeddedMedia(block) {
25943
26722
  }
25944
26723
 
25945
26724
  // src/useTimelineClock.ts
25946
- import { useCallback as useCallback41, useEffect as useEffect41, useRef as useRef39, useState as useState49 } from "react";
26725
+ import { useCallback as useCallback41, useEffect as useEffect41, useRef as useRef40, useState as useState49 } from "react";
25947
26726
  function advanceTime(prev, dt, total) {
25948
26727
  if (total <= 0) return 0;
25949
26728
  return Math.min(total, Math.max(0, prev + dt));
@@ -25951,8 +26730,8 @@ function advanceTime(prev, dt, total) {
25951
26730
  function useTimelineClock(total) {
25952
26731
  const [currentTime, setCurrentTime] = useState49(0);
25953
26732
  const [isPlaying, setIsPlaying] = useState49(false);
25954
- const rafRef = useRef39(null);
25955
- const lastRef = useRef39(0);
26733
+ const rafRef = useRef40(null);
26734
+ const lastRef = useRef40(0);
25956
26735
  useEffect41(() => {
25957
26736
  setCurrentTime((t) => Math.min(t, Math.max(0, total)));
25958
26737
  }, [total]);
@@ -26014,7 +26793,7 @@ function TimelineTrack({ height = 160 }) {
26014
26793
  } = useEditorContext();
26015
26794
  const [drag, setDrag] = useState50(null);
26016
26795
  const [pxPerSecond, setPxPerSecond] = useState50(DEFAULT_PX_PER_SECOND);
26017
- const scrollRef = useRef40(null);
26796
+ const scrollRef = useRef41(null);
26018
26797
  const blocks = useMemo37(() => doc ? flattenBlocks7(doc.blocks) : [], [doc]);
26019
26798
  const previewSettings = usePreviewSettingsOptional();
26020
26799
  const previewTheme = previewSettings?.activeTheme ?? DEFAULT_THEME5;
@@ -26074,7 +26853,7 @@ function TimelineTrack({ height = 160 }) {
26074
26853
  },
26075
26854
  [blocks]
26076
26855
  );
26077
- const followedBlockRef = useRef40(null);
26856
+ const followedBlockRef = useRef41(null);
26078
26857
  useEffect42(() => {
26079
26858
  if (!isPlaying) {
26080
26859
  followedBlockRef.current = null;
@@ -26144,9 +26923,9 @@ function TimelineTrack({ height = 160 }) {
26144
26923
  ro.observe(el);
26145
26924
  return () => ro.disconnect();
26146
26925
  }, []);
26147
- const scaleRef = useRef40(pxPerSecond);
26926
+ const scaleRef = useRef41(pxPerSecond);
26148
26927
  scaleRef.current = pxPerSecond;
26149
- const dragRef = useRef40(null);
26928
+ const dragRef = useRef41(null);
26150
26929
  dragRef.current = drag;
26151
26930
  const isDragging = drag != null && !drag.committed;
26152
26931
  useEffect42(() => {
@@ -26482,7 +27261,7 @@ function clipName(src) {
26482
27261
  }
26483
27262
 
26484
27263
  // src/PreviewPanel.tsx
26485
- import { useState as useState58, useEffect as useEffect51, useMemo as useMemo44, useCallback as useCallback49, useRef as useRef50 } from "react";
27264
+ import { useState as useState58, useEffect as useEffect51, useMemo as useMemo44, useCallback as useCallback49, useRef as useRef51 } from "react";
26486
27265
  import { createPortal as createPortal14 } from "react-dom";
26487
27266
  import { DocPlayer, LinearDocView, useMediaProvider } from "@bendyline/squisq-react";
26488
27267
  import { applyTransform } from "@bendyline/squisq/transform";
@@ -26618,7 +27397,7 @@ function buildDocumentPreviewMarkdown(doc) {
26618
27397
  }
26619
27398
 
26620
27399
  // src/PlainHtmlPreview.tsx
26621
- import { useEffect as useEffect43, useMemo as useMemo38, useRef as useRef41, useState as useState51 } from "react";
27400
+ import { useEffect as useEffect43, useMemo as useMemo38, useRef as useRef42, useState as useState51 } from "react";
26622
27401
  import { parseMarkdown as parseMarkdown8 } from "@bendyline/squisq/markdown";
26623
27402
 
26624
27403
  // src/utils/collectInlineFontAwesomeCss.ts
@@ -26684,7 +27463,7 @@ function PlainHtmlPreview({
26684
27463
  style,
26685
27464
  globalKeyboardShortcuts = false
26686
27465
  }) {
26687
- const iframeRef = useRef41(null);
27466
+ const iframeRef = useRef42(null);
26688
27467
  const mdDoc = useMemo38(() => parseMarkdown8(markdown), [markdown]);
26689
27468
  const [resolvedImages, setResolvedImages] = useState51(null);
26690
27469
  useEffect43(() => {
@@ -26813,12 +27592,12 @@ function collectImageRefs(doc) {
26813
27592
  }
26814
27593
 
26815
27594
  // src/teleprompter/TeleprompterView.tsx
26816
- import { useCallback as useCallback47, useEffect as useEffect49, useMemo as useMemo42, useRef as useRef48, useState as useState56 } from "react";
27595
+ import { useCallback as useCallback47, useEffect as useEffect49, useMemo as useMemo42, useRef as useRef49, useState as useState56 } from "react";
26817
27596
  import { createPortal as createPortal12 } from "react-dom";
26818
27597
  import { wordIndexAtTime } from "@bendyline/squisq/narration";
26819
27598
 
26820
27599
  // src/teleprompter/useTeleprompter.ts
26821
- import { useCallback as useCallback44, useEffect as useEffect45, useMemo as useMemo39, useRef as useRef43, useState as useState53 } from "react";
27600
+ import { useCallback as useCallback44, useEffect as useEffect45, useMemo as useMemo39, useRef as useRef44, useState as useState53 } from "react";
26822
27601
  import {
26823
27602
  buildNarrationScript,
26824
27603
  createNarrationSession,
@@ -26828,7 +27607,7 @@ import {
26828
27607
  } from "@bendyline/squisq/narration";
26829
27608
 
26830
27609
  // src/teleprompter/useMicAnalysis.ts
26831
- import { useCallback as useCallback43, useEffect as useEffect44, useRef as useRef42, useState as useState52 } from "react";
27610
+ import { useCallback as useCallback43, useEffect as useEffect44, useRef as useRef43, useState as useState52 } from "react";
26832
27611
 
26833
27612
  // src/teleprompter/pcmWorklet.ts
26834
27613
  var PCM_WORKLET_NAME = "squisq-pcm-tap";
@@ -26881,9 +27660,9 @@ function useMicAnalysis() {
26881
27660
  const [devices, setDevices] = useState52([]);
26882
27661
  const [stream, setStream] = useState52(null);
26883
27662
  const [sampleRate, setSampleRate] = useState52(null);
26884
- const graphRef = useRef42(null);
26885
- const listenersRef = useRef42(/* @__PURE__ */ new Set());
26886
- const generationRef = useRef42(0);
27663
+ const graphRef = useRef43(null);
27664
+ const listenersRef = useRef43(/* @__PURE__ */ new Set());
27665
+ const generationRef = useRef43(0);
26887
27666
  const refreshDevices = useCallback43(async () => {
26888
27667
  try {
26889
27668
  const all = await navigator.mediaDevices.enumerateDevices();
@@ -27075,17 +27854,17 @@ function useTeleprompter(opts) {
27075
27854
  const [countdownRemaining, setCountdownRemaining] = useState53(null);
27076
27855
  const [view, setView] = useState53({ wordPos: 0, micLevel: 0, voiceActive: false });
27077
27856
  const mic = useMicAnalysis();
27078
- const scriptRef = useRef43(script);
27079
- const prefsRef = useRef43(prefs);
27080
- const transportRef = useRef43(transport);
27081
- const wordPosRef = useRef43(0);
27082
- const sessionRef = useRef43(null);
27083
- const sessionKeyRef = useRef43("");
27084
- const lastPublishRef = useRef43(0);
27085
- const tickSubsRef = useRef43(/* @__PURE__ */ new Set());
27086
- const levelRef = useRef43(0);
27087
- const voiceRef = useRef43(false);
27088
- const countdownTimerRef = useRef43(null);
27857
+ const scriptRef = useRef44(script);
27858
+ const prefsRef = useRef44(prefs);
27859
+ const transportRef = useRef44(transport);
27860
+ const wordPosRef = useRef44(0);
27861
+ const sessionRef = useRef44(null);
27862
+ const sessionKeyRef = useRef44("");
27863
+ const lastPublishRef = useRef44(0);
27864
+ const tickSubsRef = useRef44(/* @__PURE__ */ new Set());
27865
+ const levelRef = useRef44(0);
27866
+ const voiceRef = useRef44(false);
27867
+ const countdownTimerRef = useRef44(null);
27089
27868
  scriptRef.current = script;
27090
27869
  prefsRef.current = prefs;
27091
27870
  transportRef.current = transport;
@@ -27347,7 +28126,7 @@ function useTeleprompter(opts) {
27347
28126
  }
27348
28127
 
27349
28128
  // src/teleprompter/useFloatingWindow.ts
27350
- import { useCallback as useCallback45, useEffect as useEffect46, useMemo as useMemo40, useRef as useRef44, useState as useState54 } from "react";
28129
+ import { useCallback as useCallback45, useEffect as useEffect46, useMemo as useMemo40, useRef as useRef45, useState as useState54 } from "react";
27351
28130
 
27352
28131
  // src/teleprompter/floatingWindow.ts
27353
28132
  function detectFloatTiers() {
@@ -27590,7 +28369,7 @@ html,body{margin:0;height:100%;}#squisq-float-root{height:100%;display:flex;}`;
27590
28369
  var FLOAT_WIDTH = 380;
27591
28370
  var FLOAT_HEIGHT = 540;
27592
28371
  function useFloatingWindow(styleCss) {
27593
- const managerRef = useRef44(null);
28372
+ const managerRef = useRef45(null);
27594
28373
  if (managerRef.current === null) {
27595
28374
  managerRef.current = createFloatingWindowManager({ styleCss });
27596
28375
  }
@@ -27630,7 +28409,7 @@ function useFloatingWindow(styleCss) {
27630
28409
  }
27631
28410
 
27632
28411
  // src/teleprompter/TeleprompterSurface.tsx
27633
- import { memo as memo2, useEffect as useEffect47, useMemo as useMemo41, useRef as useRef45 } from "react";
28412
+ import { memo as memo2, useEffect as useEffect47, useMemo as useMemo41, useRef as useRef46 } from "react";
27634
28413
 
27635
28414
  // src/teleprompter/scrollModel.ts
27636
28415
  var EYE_LINE_FRACTION = 0.35;
@@ -27961,7 +28740,7 @@ var TELEPROMPTER_CSS = `
27961
28740
  `;
27962
28741
 
27963
28742
  // src/teleprompter/TeleprompterSurface.tsx
27964
- import { Fragment as Fragment18, jsx as jsx60, jsxs as jsxs43 } from "react/jsx-runtime";
28743
+ import { Fragment as Fragment19, jsx as jsx60, jsxs as jsxs43 } from "react/jsx-runtime";
27965
28744
  function groupScript(script) {
27966
28745
  return script.blocks.map((range) => {
27967
28746
  const paragraphs = [];
@@ -27986,7 +28765,7 @@ var ScriptColumn = memo2(function ScriptColumn2({
27986
28765
  compact
27987
28766
  }) {
27988
28767
  const groups = useMemo41(() => groupScript(script), [script]);
27989
- return /* @__PURE__ */ jsx60(Fragment18, { children: groups.map((group) => /* @__PURE__ */ jsxs43("section", { "data-block-id": group.blockId, children: [
28768
+ return /* @__PURE__ */ jsx60(Fragment19, { children: groups.map((group) => /* @__PURE__ */ jsxs43("section", { "data-block-id": group.blockId, children: [
27990
28769
  !compact && group.heading ? /* @__PURE__ */ jsx60("span", { className: "squisq-teleprompter-block-marker", children: group.heading }) : null,
27991
28770
  group.paragraphs.map((paragraph) => /* @__PURE__ */ jsx60("p", { className: "squisq-teleprompter-para", children: paragraph.tokenIndexes.map((idx) => /* @__PURE__ */ jsxs43("span", { className: "squisq-teleprompter-word", "data-token-idx": idx, children: [
27992
28771
  script.tokens[idx].text,
@@ -28006,9 +28785,9 @@ function TeleprompterSurface({
28006
28785
  compact = false,
28007
28786
  onSeekToken
28008
28787
  }) {
28009
- const surfaceRef = useRef45(null);
28010
- const columnRef = useRef45(null);
28011
- const wordPosRef = useRef45(wordPos);
28788
+ const surfaceRef = useRef46(null);
28789
+ const columnRef = useRef46(null);
28790
+ const wordPosRef = useRef46(wordPos);
28012
28791
  wordPosRef.current = wordPos;
28013
28792
  const vars = useMemo41(() => prompterVarsFromTheme(theme), [theme]);
28014
28793
  useEffect47(() => {
@@ -28105,7 +28884,7 @@ function TeleprompterSurface({
28105
28884
  children: /* @__PURE__ */ jsx60(ScriptColumn, { script, compact })
28106
28885
  }
28107
28886
  ) }),
28108
- lineGuide ? /* @__PURE__ */ jsxs43(Fragment18, { children: [
28887
+ lineGuide ? /* @__PURE__ */ jsxs43(Fragment19, { children: [
28109
28888
  /* @__PURE__ */ jsx60(
28110
28889
  "div",
28111
28890
  {
@@ -28129,7 +28908,7 @@ function TeleprompterSurface({
28129
28908
  }
28130
28909
 
28131
28910
  // src/teleprompter/TeleprompterControls.tsx
28132
- import { Fragment as Fragment19, jsx as jsx61, jsxs as jsxs44 } from "react/jsx-runtime";
28911
+ import { Fragment as Fragment20, jsx as jsx61, jsxs as jsxs44 } from "react/jsx-runtime";
28133
28912
  var TIER_LABELS = {
28134
28913
  "document-pip": "Floating window (always on top)",
28135
28914
  "video-pip": "Picture-in-picture (read-only)",
@@ -28213,7 +28992,7 @@ function TeleprompterControls({ controller, float, recordSlot }) {
28213
28992
  children: "\u{1F399} Voice pace"
28214
28993
  }
28215
28994
  ),
28216
- prefs.voiceTracking ? /* @__PURE__ */ jsxs44(Fragment19, { children: [
28995
+ prefs.voiceTracking ? /* @__PURE__ */ jsxs44(Fragment20, { children: [
28217
28996
  /* @__PURE__ */ jsx61("label", { htmlFor: "squisq-prompter-sensitivity", title: "Voice detection sensitivity", children: "Sens." }),
28218
28997
  /* @__PURE__ */ jsx61(
28219
28998
  "input",
@@ -28299,7 +29078,7 @@ function TeleprompterControls({ controller, float, recordSlot }) {
28299
29078
  )
28300
29079
  ] }),
28301
29080
  recordSlot,
28302
- float.supportedTiers.length > 0 ? /* @__PURE__ */ jsx61("span", { className: "squisq-teleprompter-group", style: { marginLeft: "auto" }, children: float.isOpen ? /* @__PURE__ */ jsx61("button", { type: "button", onClick: float.close, children: "\u21E4 Bring back" }) : /* @__PURE__ */ jsxs44(Fragment19, { children: [
29081
+ float.supportedTiers.length > 0 ? /* @__PURE__ */ jsx61("span", { className: "squisq-teleprompter-group", style: { marginLeft: "auto" }, children: float.isOpen ? /* @__PURE__ */ jsx61("button", { type: "button", onClick: float.close, children: "\u21E4 Bring back" }) : /* @__PURE__ */ jsxs44(Fragment20, { children: [
28303
29082
  float.supportedTiers.length > 1 ? /* @__PURE__ */ jsx61(
28304
29083
  "select",
28305
29084
  {
@@ -28332,10 +29111,10 @@ function TeleprompterControls({ controller, float, recordSlot }) {
28332
29111
  }
28333
29112
 
28334
29113
  // src/teleprompter/TeleprompterSelfView.tsx
28335
- import { useRef as useRef46 } from "react";
29114
+ import { useRef as useRef47 } from "react";
28336
29115
  import { jsx as jsx62 } from "react/jsx-runtime";
28337
29116
  function TeleprompterSelfView({ stream }) {
28338
- const videoRef = useRef46(null);
29117
+ const videoRef = useRef47(null);
28339
29118
  useStreamPreview(videoRef, stream);
28340
29119
  if (!stream) return null;
28341
29120
  return /* @__PURE__ */ jsx62(
@@ -28465,7 +29244,7 @@ function drawPrompterFrame(canvas, frame) {
28465
29244
  }
28466
29245
 
28467
29246
  // src/teleprompter/recording/useNarrationRecorder.ts
28468
- import { useCallback as useCallback46, useEffect as useEffect48, useRef as useRef47, useState as useState55 } from "react";
29247
+ import { useCallback as useCallback46, useEffect as useEffect48, useRef as useRef48, useState as useState55 } from "react";
28469
29248
  import {
28470
29249
  alignNarration
28471
29250
  } from "@bendyline/squisq/narration";
@@ -28499,8 +29278,8 @@ function useNarrationRecorder(options) {
28499
29278
  const [withCamera, setWithCamera] = useState55(false);
28500
29279
  const [cameraStream, setCameraStream] = useState55(null);
28501
29280
  const [take, setTake] = useState55(null);
28502
- const captureRef = useRef47(null);
28503
- const optionsRef = useRef47(options);
29281
+ const captureRef = useRef48(null);
29282
+ const optionsRef = useRef48(options);
28504
29283
  optionsRef.current = options;
28505
29284
  const teardownCapture = useCallback46(() => {
28506
29285
  const capture = captureRef.current;
@@ -28802,13 +29581,13 @@ async function executeNarrationSave(plan, take, deps) {
28802
29581
  }
28803
29582
 
28804
29583
  // src/teleprompter/TeleprompterView.tsx
28805
- import { Fragment as Fragment20, jsx as jsx63, jsxs as jsxs45 } from "react/jsx-runtime";
29584
+ import { Fragment as Fragment21, jsx as jsx63, jsxs as jsxs45 } from "react/jsx-runtime";
28806
29585
  function TeleprompterView(props) {
28807
29586
  const { doc, theme, presentationTarget = null, recording = null } = props;
28808
29587
  const controller = useTeleprompter({ doc });
28809
29588
  const float = useFloatingWindow(TELEPROMPTER_CSS);
28810
- const rootRef = useRef48(null);
28811
- const controllerRef = useRef48(controller);
29589
+ const rootRef = useRef49(null);
29590
+ const controllerRef = useRef49(controller);
28812
29591
  controllerRef.current = controller;
28813
29592
  const [saveNotice, setSaveNotice] = useState56(null);
28814
29593
  const recorder = useNarrationRecorder({
@@ -28819,7 +29598,7 @@ function TeleprompterView(props) {
28819
29598
  onRecordingStart: () => controllerRef.current.play(),
28820
29599
  onRecordingStop: () => controllerRef.current.pause()
28821
29600
  });
28822
- const recorderRef = useRef48(recorder);
29601
+ const recorderRef = useRef49(recorder);
28823
29602
  recorderRef.current = recorder;
28824
29603
  useEffect49(() => {
28825
29604
  const ownerDoc = rootRef.current?.ownerDocument;
@@ -28828,7 +29607,7 @@ function TeleprompterView(props) {
28828
29607
  useEffect49(() => {
28829
29608
  if (presentationTarget) ensureTeleprompterStyles(presentationTarget.ownerDocument);
28830
29609
  }, [presentationTarget]);
28831
- const canvasFrameRef = useRef48(null);
29610
+ const canvasFrameRef = useRef49(null);
28832
29611
  canvasFrameRef.current = controller.script ? {
28833
29612
  script: controller.script,
28834
29613
  fontSizePx: controller.prefs.fontSizePx,
@@ -28927,7 +29706,7 @@ function TeleprompterView(props) {
28927
29706
  ]
28928
29707
  );
28929
29708
  if (!controller.script) {
28930
- return /* @__PURE__ */ jsxs45(Fragment20, { children: [
29709
+ return /* @__PURE__ */ jsxs45(Fragment21, { children: [
28931
29710
  /* @__PURE__ */ jsx63("div", { ref: rootRef, className: "squisq-teleprompter-root", "data-testid": "teleprompter-view", children: /* @__PURE__ */ jsx63("div", { className: "squisq-teleprompter-float-note", children: /* @__PURE__ */ jsx63("p", { children: "Nothing to narrate yet \u2014 add some content to the document." }) }) }),
28932
29711
  presentationTarget ? createPortal12(
28933
29712
  /* @__PURE__ */ jsx63("div", { className: "squisq-presentation-teleprompter", "aria-label": "Audience presentation", children: /* @__PURE__ */ jsx63("div", { className: "squisq-teleprompter-float-note", children: /* @__PURE__ */ jsx63("p", { children: "Nothing to narrate yet \u2014 add some content to the document." }) }) }),
@@ -28938,7 +29717,7 @@ function TeleprompterView(props) {
28938
29717
  const script = controller.script;
28939
29718
  const portalOpen = float.portalTarget !== null;
28940
29719
  const busyRecording = recorder.state === "recording" || recorder.state === "starting";
28941
- const recordSlot = recording ? /* @__PURE__ */ jsx63("span", { className: "squisq-teleprompter-group", "data-testid": "teleprompter-record", children: recorder.state === "idle" || recorder.state === "error" ? /* @__PURE__ */ jsxs45(Fragment20, { children: [
29720
+ const recordSlot = recording ? /* @__PURE__ */ jsx63("span", { className: "squisq-teleprompter-group", "data-testid": "teleprompter-record", children: recorder.state === "idle" || recorder.state === "error" ? /* @__PURE__ */ jsxs45(Fragment21, { children: [
28942
29721
  /* @__PURE__ */ jsx63(
28943
29722
  "button",
28944
29723
  {
@@ -29038,7 +29817,7 @@ import {
29038
29817
  useId as useId9,
29039
29818
  useLayoutEffect as useLayoutEffect5,
29040
29819
  useMemo as useMemo43,
29041
- useRef as useRef49,
29820
+ useRef as useRef50,
29042
29821
  useState as useState57
29043
29822
  } from "react";
29044
29823
  import { createPortal as createPortal13 } from "react-dom";
@@ -29105,12 +29884,12 @@ function PresentationModeProvider({ rootRef, children }) {
29105
29884
  const [activeTarget, setActiveTarget] = useState57(null);
29106
29885
  const [popupRoot, setPopupRoot] = useState57(null);
29107
29886
  const [error, setError] = useState57(null);
29108
- const activeTargetRef = useRef49(activeTarget);
29887
+ const activeTargetRef = useRef50(activeTarget);
29109
29888
  activeTargetRef.current = activeTarget;
29110
- const previousActiveTargetRef = useRef49(null);
29111
- const returnFocusRef = useRef49(null);
29112
- const popupRef = useRef49(null);
29113
- const popupCleanupRef = useRef49(null);
29889
+ const previousActiveTargetRef = useRef50(null);
29890
+ const returnFocusRef = useRef50(null);
29891
+ const popupRef = useRef50(null);
29892
+ const popupCleanupRef = useRef50(null);
29114
29893
  const fullscreenSupported = typeof document !== "undefined" && typeof document.documentElement.requestFullscreen === "function";
29115
29894
  const releasePopup = useCallback48((closeWindow) => {
29116
29895
  const popup = popupRef.current;
@@ -29349,8 +30128,8 @@ var MENU_GAP = 4;
29349
30128
  function PresentationModeControl() {
29350
30129
  const { selectedTarget, activeTarget, fullscreenSupported, selectTarget, start, stop } = usePresentationMode();
29351
30130
  const { colorScheme } = useEditorContext();
29352
- const triggerRef = useRef49(null);
29353
- const menuRef = useRef49(null);
30131
+ const triggerRef = useRef50(null);
30132
+ const menuRef = useRef50(null);
29354
30133
  const [open, setOpen] = useState57(false);
29355
30134
  const [anchor, setAnchor] = useState57(null);
29356
30135
  const selected = PRESENTATION_OPTIONS.find((option) => option.target === selectedTarget) ?? PRESENTATION_OPTIONS[0];
@@ -29515,7 +30294,7 @@ function PresentationModeControl() {
29515
30294
  }
29516
30295
 
29517
30296
  // src/PreviewPanel.tsx
29518
- import { Fragment as Fragment21, jsx as jsx65, jsxs as jsxs47 } from "react/jsx-runtime";
30297
+ import { Fragment as Fragment22, jsx as jsx65, jsxs as jsxs47 } from "react/jsx-runtime";
29519
30298
  function PreviewPanel({ basePath = "/", className, workspaceContainer }) {
29520
30299
  const {
29521
30300
  doc,
@@ -29539,8 +30318,8 @@ function PreviewPanel({ basePath = "/", className, workspaceContainer }) {
29539
30318
  activeCaptionsEnabled,
29540
30319
  activeCoverSlide
29541
30320
  } = usePreviewSettings();
29542
- const mainSurfaceRef = useRef50(null);
29543
- const popupSurfaceRef = useRef50(null);
30321
+ const mainSurfaceRef = useRef51(null);
30322
+ const popupSurfaceRef = useRef51(null);
29544
30323
  const [playbackState, setPlaybackState] = useState58(null);
29545
30324
  const handlePlaybackStateChange = useCallback49((next) => {
29546
30325
  setPlaybackState(next);
@@ -29759,7 +30538,7 @@ function PreviewPanel({ basePath = "/", className, workspaceContainer }) {
29759
30538
  overflow: "hidden",
29760
30539
  minHeight: 0
29761
30540
  };
29762
- return /* @__PURE__ */ jsxs47(Fragment21, { children: [
30541
+ return /* @__PURE__ */ jsxs47(Fragment22, { children: [
29763
30542
  /* @__PURE__ */ jsx65(
29764
30543
  "div",
29765
30544
  {
@@ -29800,14 +30579,14 @@ function PreviewPanel({ basePath = "/", className, workspaceContainer }) {
29800
30579
  }
29801
30580
 
29802
30581
  // src/ImageViewer.tsx
29803
- import { useCallback as useCallback50, useEffect as useEffect52, useRef as useRef51, useState as useState59 } from "react";
29804
- import { Fragment as Fragment22, jsx as jsx66, jsxs as jsxs48 } from "react/jsx-runtime";
30582
+ import { useCallback as useCallback50, useEffect as useEffect52, useRef as useRef52, useState as useState59 } from "react";
30583
+ import { Fragment as Fragment23, jsx as jsx66, jsxs as jsxs48 } from "react/jsx-runtime";
29805
30584
  var MIN_ZOOM = 0.1;
29806
30585
  var MAX_ZOOM = 16;
29807
30586
  var ZOOM_STEP = 1.25;
29808
30587
  function ImageViewer({ src, alt = "", className, theme = "light" }) {
29809
- const imgRef = useRef51(null);
29810
- const stageRef = useRef51(null);
30588
+ const imgRef = useRef52(null);
30589
+ const stageRef = useRef52(null);
29811
30590
  const [naturalSize, setNaturalSize] = useState59(null);
29812
30591
  const [fitZoom, setFitZoom] = useState59(1);
29813
30592
  const [state, setState2] = useState59({ mode: "fit" });
@@ -29859,7 +30638,7 @@ function ImageViewer({ src, alt = "", className, theme = "light" }) {
29859
30638
  }, [setZoom]);
29860
30639
  const onZoomIn = useCallback50(() => setZoom(effectiveZoom * ZOOM_STEP), [effectiveZoom, setZoom]);
29861
30640
  const onZoomOut = useCallback50(() => setZoom(effectiveZoom / ZOOM_STEP), [effectiveZoom, setZoom]);
29862
- const dragRef = useRef51(
30641
+ const dragRef = useRef52(
29863
30642
  null
29864
30643
  );
29865
30644
  const onMouseDown = useCallback50(
@@ -29966,7 +30745,7 @@ function ImageViewer({ src, alt = "", className, theme = "light" }) {
29966
30745
  ]
29967
30746
  }
29968
30747
  ),
29969
- /* @__PURE__ */ jsx66("div", { className: "squisq-image-viewer-status", children: naturalSize ? /* @__PURE__ */ jsxs48(Fragment22, { children: [
30748
+ /* @__PURE__ */ jsx66("div", { className: "squisq-image-viewer-status", children: naturalSize ? /* @__PURE__ */ jsxs48(Fragment23, { children: [
29970
30749
  /* @__PURE__ */ jsxs48("span", { children: [
29971
30750
  naturalSize.w,
29972
30751
  " \xD7 ",
@@ -29981,11 +30760,11 @@ function ImageViewer({ src, alt = "", className, theme = "light" }) {
29981
30760
  }
29982
30761
 
29983
30762
  // src/ImageEditor.tsx
29984
- import { useCallback as useCallback54, useRef as useRef57, useState as useState66 } from "react";
30763
+ import { useCallback as useCallback54, useRef as useRef58, useState as useState66 } from "react";
29985
30764
  import { exportImageEditDoc as exportImageEditDoc2 } from "@bendyline/squisq/imageEdit";
29986
30765
 
29987
30766
  // src/imageEditor/CanvasSurface.tsx
29988
- import { useCallback as useCallback51, useEffect as useEffect54, useId as useId10, useLayoutEffect as useLayoutEffect6, useRef as useRef52, useState as useState61 } from "react";
30767
+ import { useCallback as useCallback51, useEffect as useEffect54, useId as useId10, useLayoutEffect as useLayoutEffect6, useRef as useRef53, useState as useState61 } from "react";
29989
30768
  import { PathLayer as PathLayer2 } from "@bendyline/squisq-react";
29990
30769
 
29991
30770
  // src/imageEditor/layers/EditorImageLayer.tsx
@@ -30180,16 +30959,16 @@ function CanvasSurface({
30180
30959
  requestEditLayerId
30181
30960
  }) {
30182
30961
  const checkerId = `squisq-image-editor-checker-${useId10().replace(/:/g, "")}`;
30183
- const svgRef = useRef52(null);
30184
- const dragRef = useRef52(null);
30962
+ const svgRef = useRef53(null);
30963
+ const dragRef = useRef53(null);
30185
30964
  const [, forceRender] = useState61(0);
30186
30965
  const [cropDrag, setCropDrag] = useState61(null);
30187
30966
  const [shapeLineDrag, setShapeLineDrag] = useState61(null);
30188
- const internalWrapRef = useRef52(null);
30189
- const pendingScrollRef = useRef52(null);
30967
+ const internalWrapRef = useRef53(null);
30968
+ const pendingScrollRef = useRef53(null);
30190
30969
  const [zoomDrag, setZoomDrag] = useState61(null);
30191
30970
  const [editingLayerId, setEditingLayerId] = useState61(null);
30192
- const editTextareaRef = useRef52(null);
30971
+ const editTextareaRef = useRef53(null);
30193
30972
  const toCanvas = useCallback51(
30194
30973
  (clientX, clientY) => {
30195
30974
  const svg = svgRef.current;
@@ -30698,7 +31477,7 @@ function measureTextLayerBox(layer, fallback) {
30698
31477
  }
30699
31478
 
30700
31479
  // src/imageEditor/ImageVersionHistoryDropdown.tsx
30701
- import { useCallback as useCallback52, useEffect as useEffect55, useRef as useRef53, useState as useState62 } from "react";
31480
+ import { useCallback as useCallback52, useEffect as useEffect55, useRef as useRef54, useState as useState62 } from "react";
30702
31481
  import {
30703
31482
  exportImageEditDoc
30704
31483
  } from "@bendyline/squisq/imageEdit";
@@ -30715,9 +31494,9 @@ function ImageVersionHistoryDropdown({
30715
31494
  const [loading, setLoading] = useState62(false);
30716
31495
  const [busyTimestamp, setBusyTimestamp] = useState62(null);
30717
31496
  const [meta, setMeta2] = useState62({});
30718
- const popoverRef = useRef53(null);
30719
- const triggerRef = useRef53(null);
30720
- const urlsRef = useRef53(/* @__PURE__ */ new Set());
31497
+ const popoverRef = useRef54(null);
31498
+ const triggerRef = useRef54(null);
31499
+ const urlsRef = useRef54(/* @__PURE__ */ new Set());
30721
31500
  useEffect55(() => {
30722
31501
  if (!open) return;
30723
31502
  let cancelled = false;
@@ -30966,7 +31745,7 @@ function emptyDoc() {
30966
31745
  }
30967
31746
 
30968
31747
  // src/imageEditor/LayersPanel.tsx
30969
- import { useEffect as useEffect56, useRef as useRef54, useState as useState63 } from "react";
31748
+ import { useEffect as useEffect56, useRef as useRef55, useState as useState63 } from "react";
30970
31749
 
30971
31750
  // src/imageEditor/icons.tsx
30972
31751
  import { jsx as jsx73 } from "react/jsx-runtime";
@@ -31022,7 +31801,7 @@ var ADD_LAYER_OPTIONS = [
31022
31801
  ];
31023
31802
  function LayersPanel({ doc, selectedLayerId, dispatch, onAddLayer }) {
31024
31803
  const [addMenuOpen, setAddMenuOpen] = useState63(false);
31025
- const addMenuRef = useRef54(null);
31804
+ const addMenuRef = useRef55(null);
31026
31805
  useEffect56(() => {
31027
31806
  if (!addMenuOpen) return;
31028
31807
  const closeOnOutsideClick = (event) => {
@@ -31194,7 +31973,7 @@ function defaultLayerName(layer) {
31194
31973
  }
31195
31974
 
31196
31975
  // src/imageEditor/PropertiesPanel.tsx
31197
- import { Fragment as Fragment23, jsx as jsx75, jsxs as jsxs53 } from "react/jsx-runtime";
31976
+ import { Fragment as Fragment24, jsx as jsx75, jsxs as jsxs53 } from "react/jsx-runtime";
31198
31977
  function PropertiesPanel({ doc, selectedLayerId, dispatch }) {
31199
31978
  const selected = selectedLayerId ? doc.layers.find((l) => l.id === selectedLayerId) ?? null : null;
31200
31979
  return /* @__PURE__ */ jsxs53("div", { className: "squisq-image-editor-properties", "data-testid": "image-editor-properties", children: [
@@ -31246,7 +32025,7 @@ function LayerSection({
31246
32025
  dispatch
31247
32026
  }) {
31248
32027
  const update = (patch) => dispatch({ type: "update-layer", layerId: layer.id, patch });
31249
- return /* @__PURE__ */ jsxs53(Fragment23, { children: [
32028
+ return /* @__PURE__ */ jsxs53(Fragment24, { children: [
31250
32029
  /* @__PURE__ */ jsxs53("fieldset", { className: "squisq-image-editor-fieldset", children: [
31251
32030
  /* @__PURE__ */ jsx75("legend", { children: "Layer" }),
31252
32031
  /* @__PURE__ */ jsx75(TextField, { label: "Name", value: layer.name ?? "", onChange: (name) => update({ name }) }),
@@ -31590,7 +32369,7 @@ function normalizeColor2(v) {
31590
32369
  }
31591
32370
 
31592
32371
  // src/imageEditor/Toolbar.tsx
31593
- import { useRef as useRef55, useState as useState64, useEffect as useEffect57 } from "react";
32372
+ import { useRef as useRef56, useState as useState64, useEffect as useEffect57 } from "react";
31594
32373
  import { jsx as jsx76, jsxs as jsxs54 } from "react/jsx-runtime";
31595
32374
  function RedlineArrowIcon() {
31596
32375
  return /* @__PURE__ */ jsxs54("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", "aria-hidden": "true", children: [
@@ -31697,7 +32476,7 @@ function Toolbar2({
31697
32476
  onZoomFit,
31698
32477
  onZoom1to1
31699
32478
  }) {
31700
- const internalImageInputRef = useRef55(null);
32479
+ const internalImageInputRef = useRef56(null);
31701
32480
  const fileInputRef = imageInputRef ?? internalImageInputRef;
31702
32481
  const [shapePaletteOpen, setShapePaletteOpen] = useState64(false);
31703
32482
  const onFilePicked = async (file) => {
@@ -31900,8 +32679,8 @@ function Toolbar2({
31900
32679
  }
31901
32680
  function ExportDropdown({ onExport }) {
31902
32681
  const [open, setOpen] = useState64(false);
31903
- const wrapRef = useRef55(null);
31904
- const triggerRef = useRef55(null);
32682
+ const wrapRef = useRef56(null);
32683
+ const triggerRef = useRef56(null);
31905
32684
  useEffect57(() => {
31906
32685
  if (!open) return;
31907
32686
  function onDocClick(e2) {
@@ -31977,7 +32756,7 @@ function probeDims(file) {
31977
32756
  }
31978
32757
 
31979
32758
  // src/imageEditor/useImageEditor.ts
31980
- import { useCallback as useCallback53, useEffect as useEffect58, useMemo as useMemo45, useReducer as useReducer2, useRef as useRef56, useState as useState65 } from "react";
32759
+ import { useCallback as useCallback53, useEffect as useEffect58, useMemo as useMemo45, useReducer as useReducer2, useRef as useRef57, useState as useState65 } from "react";
31981
32760
  import {
31982
32761
  IMAGE_EDIT_ASSETS_PREFIX,
31983
32762
  IMAGE_EDIT_STATE_FILENAME,
@@ -32093,7 +32872,7 @@ function useImageEditor(options) {
32093
32872
  );
32094
32873
  const [ready, setReady] = useState65(false);
32095
32874
  const [error, setError] = useState65(null);
32096
- const seededOnLoadRef = useRef56(false);
32875
+ const seededOnLoadRef = useRef57(false);
32097
32876
  useEffect58(() => {
32098
32877
  let cancelled = false;
32099
32878
  setReady(false);
@@ -32124,11 +32903,11 @@ function useImageEditor(options) {
32124
32903
  cancelled = true;
32125
32904
  };
32126
32905
  }, [container, stateFilename, initialSrc]);
32127
- const persistTimerRef = useRef56(null);
32128
- const docRef = useRef56(null);
32129
- const dirtyRef = useRef56(false);
32130
- const revisionRef = useRef56(0);
32131
- const previousDocRef = useRef56(null);
32906
+ const persistTimerRef = useRef57(null);
32907
+ const docRef = useRef57(null);
32908
+ const dirtyRef = useRef57(false);
32909
+ const revisionRef = useRef57(0);
32910
+ const previousDocRef = useRef57(null);
32132
32911
  const nextDoc = state?.doc ?? null;
32133
32912
  if (nextDoc !== previousDocRef.current) {
32134
32913
  previousDocRef.current = nextDoc;
@@ -32136,8 +32915,8 @@ function useImageEditor(options) {
32136
32915
  }
32137
32916
  docRef.current = nextDoc;
32138
32917
  dirtyRef.current = state?.dirty ?? false;
32139
- const writeQueueRef = useRef56(Promise.resolve());
32140
- const persistTargetRef = useRef56({ container, stateFilename });
32918
+ const writeQueueRef = useRef57(Promise.resolve());
32919
+ const persistTargetRef = useRef57({ container, stateFilename });
32141
32920
  persistTargetRef.current = { container, stateFilename };
32142
32921
  const enqueueWrite = useCallback53(
32143
32922
  (doc, revision, markClean) => {
@@ -32224,8 +33003,8 @@ function useImageEditor(options) {
32224
33003
  }, versioningAutoSaveIdleMs);
32225
33004
  return () => clearTimeout(timer);
32226
33005
  }, [versioning, versioningAutoSaveIdleMs, state?.doc]);
32227
- const urlCacheRef = useRef56(/* @__PURE__ */ new Map());
32228
- const urlCacheGenerationRef = useRef56(0);
33006
+ const urlCacheRef = useRef57(/* @__PURE__ */ new Map());
33007
+ const urlCacheGenerationRef = useRef57(0);
32229
33008
  const resolveAssetUrl = useCallback53(
32230
33009
  async (path) => {
32231
33010
  const cache2 = urlCacheRef.current;
@@ -32627,8 +33406,8 @@ function ImageEditor(props) {
32627
33406
  versioningAutoSaveIdleMs
32628
33407
  });
32629
33408
  const [historyRefreshKey, setHistoryRefreshKey] = useState66(0);
32630
- const surfaceRef = useRef57(null);
32631
- const imageInputRef = useRef57(null);
33409
+ const surfaceRef = useRef58(null);
33410
+ const imageInputRef = useRef58(null);
32632
33411
  const [zoom, setZoom] = useState66(1);
32633
33412
  const handleZoomIn = useCallback54(() => {
32634
33413
  setZoom((z) => ZOOM_STEPS.find((s) => s > z + 1e-3) ?? 16);
@@ -32879,7 +33658,7 @@ function ImageEditor(props) {
32879
33658
  }
32880
33659
 
32881
33660
  // src/MediaBin.tsx
32882
- import { useState as useState67, useEffect as useEffect59, useRef as useRef58, useCallback as useCallback55 } from "react";
33661
+ import { useState as useState67, useEffect as useEffect59, useRef as useRef59, useCallback as useCallback55 } from "react";
32883
33662
  import { jsx as jsx78, jsxs as jsxs56 } from "react/jsx-runtime";
32884
33663
  function formatSize(bytes) {
32885
33664
  if (bytes < 1024) return `${bytes} B`;
@@ -32933,9 +33712,9 @@ function MediaBin({
32933
33712
  const [loading, setLoading] = useState67(false);
32934
33713
  const [isDropActive, setIsDropActive] = useState67(false);
32935
33714
  const [contextMenu, setContextMenu] = useState67(null);
32936
- const fileInputRef = useRef58(null);
32937
- const contextMenuRef = useRef58(null);
32938
- const dropDepthRef = useRef58(0);
33715
+ const fileInputRef = useRef59(null);
33716
+ const contextMenuRef = useRef59(null);
33717
+ const dropDepthRef = useRef59(0);
32939
33718
  const updateEntries = useCallback55(
32940
33719
  async (provider) => {
32941
33720
  const list = sortMediaEntries(filterVisibleMediaEntries(await provider.listMedia()));
@@ -33265,7 +34044,7 @@ ${formatSize(entry.size)}`,
33265
34044
 
33266
34045
  // src/DropZoneOverlay.tsx
33267
34046
  import { useState as useState68 } from "react";
33268
- import { Fragment as Fragment24, jsx as jsx79, jsxs as jsxs57 } from "react/jsx-runtime";
34047
+ import { Fragment as Fragment25, jsx as jsx79, jsxs as jsxs57 } from "react/jsx-runtime";
33269
34048
  function DropZoneOverlay({
33270
34049
  dragContentType,
33271
34050
  zoneProps,
@@ -33286,7 +34065,7 @@ function DropZoneOverlay({
33286
34065
  variant: "media"
33287
34066
  }
33288
34067
  ),
33289
- showText && /* @__PURE__ */ jsxs57(Fragment24, { children: [
34068
+ showText && /* @__PURE__ */ jsxs57(Fragment25, { children: [
33290
34069
  /* @__PURE__ */ jsx79(
33291
34070
  DropZone,
33292
34071
  {
@@ -33365,7 +34144,7 @@ function DropZone({
33365
34144
  }
33366
34145
 
33367
34146
  // src/Tooltip.tsx
33368
- import { useEffect as useEffect60, useLayoutEffect as useLayoutEffect7, useRef as useRef59, useState as useState69 } from "react";
34147
+ import { useEffect as useEffect60, useLayoutEffect as useLayoutEffect7, useRef as useRef60, useState as useState69 } from "react";
33369
34148
  import { createPortal as createPortal15 } from "react-dom";
33370
34149
 
33371
34150
  // src/tooltipPlacement.ts
@@ -33382,10 +34161,10 @@ import { jsx as jsx80 } from "react/jsx-runtime";
33382
34161
  var SHOW_DELAY_MS = 180;
33383
34162
  function TooltipLayer() {
33384
34163
  const [state, setState2] = useState69(null);
33385
- const tooltipRef = useRef59(null);
33386
- const timerRef = useRef59(null);
33387
- const currentTargetRef = useRef59(null);
33388
- const visibleRef = useRef59(false);
34164
+ const tooltipRef = useRef60(null);
34165
+ const timerRef = useRef60(null);
34166
+ const currentTargetRef = useRef60(null);
34167
+ const visibleRef = useRef60(false);
33389
34168
  useLayoutEffect7(() => {
33390
34169
  if (!state) return;
33391
34170
  const node = tooltipRef.current;
@@ -33717,7 +34496,7 @@ import {
33717
34496
  createMediaProviderFromContainer
33718
34497
  } from "@bendyline/squisq/storage";
33719
34498
  import { MediaContext as MediaContext6 } from "@bendyline/squisq-react";
33720
- import { Fragment as Fragment25, jsx as jsx81, jsxs as jsxs58 } from "react/jsx-runtime";
34499
+ import { Fragment as Fragment26, jsx as jsx81, jsxs as jsxs58 } from "react/jsx-runtime";
33721
34500
  function EditorShell({
33722
34501
  initialMarkdown = "",
33723
34502
  initialView = "wysiwyg",
@@ -33879,7 +34658,7 @@ function EditorShellInner({
33879
34658
  outlineWidth,
33880
34659
  themeOverride
33881
34660
  }) {
33882
- const shellRef = useRef60(null);
34661
+ const shellRef = useRef61(null);
33883
34662
  const {
33884
34663
  activeView,
33885
34664
  markdownSource,
@@ -33920,7 +34699,7 @@ function EditorShellInner({
33920
34699
  () => collectMediaReferencesFromMarkdown(markdownSource),
33921
34700
  [markdownSource]
33922
34701
  );
33923
- const imageEditFallbackContainerRef = useRef60(null);
34702
+ const imageEditFallbackContainerRef = useRef61(null);
33924
34703
  if (imageEditFallbackContainerRef.current === null) {
33925
34704
  imageEditFallbackContainerRef.current = new MemoryContentContainer();
33926
34705
  }
@@ -34093,7 +34872,7 @@ ${snippet}` : snippet);
34093
34872
  fileCount: mediaCount,
34094
34873
  onToggleFiles: !isCodeMode && filesToggleEnabled ? handleToggleFiles : void 0,
34095
34874
  slotLeft: toolbarSlotLeft,
34096
- slotAfterTabs: !isCodeMode && isPreview && /* @__PURE__ */ jsxs58(Fragment25, { children: [
34875
+ slotAfterTabs: !isCodeMode && isPreview && /* @__PURE__ */ jsxs58(Fragment26, { children: [
34097
34876
  /* @__PURE__ */ jsx81(PreviewToolbarControls, {}),
34098
34877
  /* @__PURE__ */ jsx81(PresentationModeControl, {})
34099
34878
  ] }),
@@ -34279,9 +35058,9 @@ function ImageEditModal({
34279
35058
  versioningAutoSaveIdleMs,
34280
35059
  shellTheme
34281
35060
  }) {
34282
- const modalRef = useRef60(null);
34283
- const surfaceRef = useRef60(null);
34284
- const onCloseRef = useRef60(onClose);
35061
+ const modalRef = useRef61(null);
35062
+ const surfaceRef = useRef61(null);
35063
+ const onCloseRef = useRef61(onClose);
34285
35064
  onCloseRef.current = onClose;
34286
35065
  const extension = relativePath.split(/[?#]/, 1)[0]?.split(".").pop()?.toLowerCase();
34287
35066
  const saveFormat = extension === "png" ? "png" : extension === "jpg" || extension === "jpeg" ? "jpeg" : extension === "webp" ? "webp" : null;
@@ -34533,7 +35312,7 @@ function ViewSwitcher({ className }) {
34533
35312
  }
34534
35313
 
34535
35314
  // src/ThemeCustomizerPanel.tsx
34536
- import { useCallback as useCallback57, useEffect as useEffect62, useMemo as useMemo48, useRef as useRef61, useState as useState71 } from "react";
35315
+ import { useCallback as useCallback57, useEffect as useEffect62, useMemo as useMemo48, useRef as useRef62, useState as useState71 } from "react";
34537
35316
  import { deriveScale, isHex as isHex2, serializeTheme } from "@bendyline/squisq/schemas";
34538
35317
  import { jsx as jsx84, jsxs as jsxs61 } from "react/jsx-runtime";
34539
35318
  function ThemeCustomizerPanel({
@@ -34545,8 +35324,8 @@ function ThemeCustomizerPanel({
34545
35324
  }) {
34546
35325
  const [open, setOpen] = useState71(false);
34547
35326
  const [draft, setDraft] = useState71(() => themeToDraft(value));
34548
- const containerRef = useRef61(null);
34549
- const externalIdRef = useRef61(value?.id ?? null);
35327
+ const containerRef = useRef62(null);
35328
+ const externalIdRef = useRef62(value?.id ?? null);
34550
35329
  useEffect62(() => {
34551
35330
  const incomingId = value?.id ?? null;
34552
35331
  if (incomingId !== externalIdRef.current) {
@@ -34871,14 +35650,14 @@ import {
34871
35650
  import { useId as useId12 } from "react";
34872
35651
 
34873
35652
  // src/jsonEditor/editors.tsx
34874
- import { useEffect as useEffect65, useId as useId11, useRef as useRef63, useState as useState72 } from "react";
35653
+ import { useEffect as useEffect65, useId as useId11, useRef as useRef64, useState as useState72 } from "react";
34875
35654
  import {
34876
35655
  appendPointer,
34877
35656
  arrayItemKind
34878
35657
  } from "@bendyline/squisq/jsonForm";
34879
35658
 
34880
35659
  // src/jsonEditor/EmbeddedRichTextField.tsx
34881
- import { useEffect as useEffect64, useRef as useRef62 } from "react";
35660
+ import { useEffect as useEffect64, useRef as useRef63 } from "react";
34882
35661
  import { useEditor as useEditor4, EditorContent as EditorContent3 } from "@tiptap/react";
34883
35662
  import StarterKit3 from "@tiptap/starter-kit";
34884
35663
  import Table2 from "@tiptap/extension-table";
@@ -34891,8 +35670,8 @@ import Placeholder2 from "@tiptap/extension-placeholder";
34891
35670
  import { jsx as jsx86 } from "react/jsx-runtime";
34892
35671
  function EmbeddedRichTextField(props) {
34893
35672
  const { value, onChange, readOnly = false, placeholder, className } = props;
34894
- const isExternalUpdate = useRef62(false);
34895
- const lastValueRef = useRef62(value);
35673
+ const isExternalUpdate = useRef63(false);
35674
+ const lastValueRef = useRef63(value);
34896
35675
  const editor = useEditor4({
34897
35676
  editable: !readOnly,
34898
35677
  extensions: [
@@ -34977,7 +35756,7 @@ function TextEditor(props) {
34977
35756
  function MultilineEditor(props) {
34978
35757
  const { value, schema, pointer, disabled } = props;
34979
35758
  const { setAtPath } = useJsonEditor();
34980
- const ref = useRef63(null);
35759
+ const ref = useRef64(null);
34981
35760
  useEffect65(() => {
34982
35761
  const ta = ref.current;
34983
35762
  if (!ta) return;
@@ -35676,11 +36455,12 @@ function JsonEditor(props) {
35676
36455
  // src/recorder/RecorderButton.tsx
35677
36456
  import { useCallback as useCallback58, useState as useState73 } from "react";
35678
36457
  import { createPortal as createPortal16 } from "react-dom";
35679
- import { Fragment as Fragment26, jsx as jsx90, jsxs as jsxs64 } from "react/jsx-runtime";
36458
+ import { Fragment as Fragment27, jsx as jsx90, jsxs as jsxs64 } from "react/jsx-runtime";
35680
36459
  function RecorderButton({
35681
36460
  mediaProvider,
35682
36461
  container = null,
35683
36462
  initialMode = "mic",
36463
+ colorScheme = "light",
35684
36464
  onSave,
35685
36465
  label = "Record",
35686
36466
  style,
@@ -35695,7 +36475,7 @@ function RecorderButton({
35695
36475
  },
35696
36476
  [onSave]
35697
36477
  );
35698
- return /* @__PURE__ */ jsxs64(Fragment26, { children: [
36478
+ return /* @__PURE__ */ jsxs64(Fragment27, { children: [
35699
36479
  /* @__PURE__ */ jsx90("button", { type: "button", onClick: handleOpen, style, disabled, children: label }),
35700
36480
  open && typeof document !== "undefined" && createPortal16(
35701
36481
  /* @__PURE__ */ jsx90(
@@ -35704,6 +36484,7 @@ function RecorderButton({
35704
36484
  mediaProvider,
35705
36485
  container,
35706
36486
  initialMode,
36487
+ colorScheme,
35707
36488
  onClose: handleClose,
35708
36489
  onSave: handleSave
35709
36490
  }
@@ -35878,6 +36659,7 @@ export {
35878
36659
  toggleAsciiSource,
35879
36660
  toggleDirOp,
35880
36661
  transitionLabel,
36662
+ translateDiagramOp,
35881
36663
  updateTimelineEventOp,
35882
36664
  useAsciiDiagramData,
35883
36665
  useBlockNavigator,