@mulmoclaude/mulmoscript-plugin 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/contract-BHqxMUWm.cjs +82 -0
  2. package/dist/contract-BHqxMUWm.cjs.map +1 -0
  3. package/dist/contract-BnIl6w_f.js +53 -0
  4. package/dist/contract-BnIl6w_f.js.map +1 -0
  5. package/dist/core/contract.d.ts +80 -6
  6. package/dist/core/contract.d.ts.map +1 -1
  7. package/dist/core/definition.d.ts.map +1 -1
  8. package/dist/core/paths.d.ts +41 -0
  9. package/dist/core/paths.d.ts.map +1 -1
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/lang/de.d.ts.map +1 -1
  13. package/dist/lang/en.d.ts.map +1 -1
  14. package/dist/lang/es.d.ts.map +1 -1
  15. package/dist/lang/fr.d.ts.map +1 -1
  16. package/dist/lang/ja.d.ts.map +1 -1
  17. package/dist/lang/ko.d.ts.map +1 -1
  18. package/dist/lang/messages.d.ts +4 -0
  19. package/dist/lang/messages.d.ts.map +1 -1
  20. package/dist/lang/ptBR.d.ts.map +1 -1
  21. package/dist/lang/zh.d.ts.map +1 -1
  22. package/dist/{plugin-BOq5YkTl.cjs → plugin-C5JC2lNR.cjs} +51 -1
  23. package/dist/plugin-C5JC2lNR.cjs.map +1 -0
  24. package/dist/{plugin-CAhxJ5WM.js → plugin-Jo8iGv4K.js} +40 -2
  25. package/dist/plugin-Jo8iGv4K.js.map +1 -0
  26. package/dist/server/dispatch.d.ts +0 -6
  27. package/dist/server/dispatch.d.ts.map +1 -1
  28. package/dist/server/ops.d.ts +29 -18
  29. package/dist/server/ops.d.ts.map +1 -1
  30. package/dist/server/types.d.ts +64 -2
  31. package/dist/server/types.d.ts.map +1 -1
  32. package/dist/server.cjs +423 -110
  33. package/dist/server.cjs.map +1 -1
  34. package/dist/server.js +423 -110
  35. package/dist/server.js.map +1 -1
  36. package/dist/style.css +46 -18
  37. package/dist/vue/View.vue.d.ts.map +1 -1
  38. package/dist/vue/composables/useDeckEditor.d.ts +1 -1
  39. package/dist/vue/composables/useDeckEditor.d.ts.map +1 -1
  40. package/dist/vue/helpers.d.ts +11 -8
  41. package/dist/vue/helpers.d.ts.map +1 -1
  42. package/dist/vue/index.d.ts +1 -1
  43. package/dist/vue/index.d.ts.map +1 -1
  44. package/dist/vue/subscription.d.ts +16 -0
  45. package/dist/vue/subscription.d.ts.map +1 -0
  46. package/dist/vue/transport.d.ts +16 -5
  47. package/dist/vue/transport.d.ts.map +1 -1
  48. package/dist/vue.cjs +244 -110
  49. package/dist/vue.cjs.map +1 -1
  50. package/dist/vue.js +244 -110
  51. package/dist/vue.js.map +1 -1
  52. package/package.json +11 -11
  53. package/dist/contract-BhN3yKJC.cjs +0 -36
  54. package/dist/contract-BhN3yKJC.cjs.map +0 -1
  55. package/dist/contract-CIt1ZAtt.js +0 -19
  56. package/dist/contract-CIt1ZAtt.js.map +0 -1
  57. package/dist/plugin-BOq5YkTl.cjs.map +0 -1
  58. package/dist/plugin-CAhxJ5WM.js.map +0 -1
package/dist/vue.cjs CHANGED
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_plugin = require("./plugin-BOq5YkTl.cjs");
6
- const require_contract = require("./contract-BhN3yKJC.cjs");
5
+ const require_plugin = require("./plugin-C5JC2lNR.cjs");
6
+ const require_contract = require("./contract-BHqxMUWm.cjs");
7
7
  let _mulmocast_types = require("@mulmocast/types");
8
8
  let vue = require("vue");
9
9
  let _mulmocast_beat_editor = require("@mulmocast/beat-editor");
@@ -119,22 +119,20 @@ function beatMayHaveMovie(beat) {
119
119
  function isBeatImageReference(beat) {
120
120
  return beat.image?.type === "beat";
121
121
  }
122
- /** Pure check: is every beat in the script a `slide`-typed beat?
123
- * When true, the View mounts `@mulmocast/beat-editor`'s
124
- * `BeatListEditor` instead of the per-beat list UI (#1575).
125
- * Empty / missing `beats[]` returns false there's nothing to edit
126
- * as a deck, fall through to the existing UI which renders an empty
127
- * state. Mixed scripts (any non-`slide` beat) also return false; that
128
- * case is deferred to a future phase. */
129
- function isAllSlideDeck(script) {
122
+ /** Whether the beat editor has anything to edit.
123
+ *
124
+ * Any beat type, not just `slide`: `@mulmocast/beat-editor` renders and edits all eight
125
+ * (`textSlide` / `markdown` / `chart` / `mermaid` / `image` / `movie` / `slide` /
126
+ * `html_tailwind`). The all-slide test this replaces was a limit of the OLD iframe deck
127
+ * editor, which only understood decks it stayed in place through the migration and kept a
128
+ * markdown script read-only for no reason.
129
+ *
130
+ * Empty / missing `beats[]` is false: there is nothing to edit, and the per-beat list already
131
+ * renders an empty state. */
132
+ function hasEditableBeats(script) {
130
133
  if (!isRecord(script)) return false;
131
134
  const { beats } = script;
132
- if (!Array.isArray(beats) || beats.length === 0) return false;
133
- return beats.every((beat) => {
134
- if (!isRecord(beat)) return false;
135
- const { image } = beat;
136
- return isRecord(image) && image.type === "slide";
137
- });
135
+ return Array.isArray(beats) && beats.length > 0;
138
136
  }
139
137
  /** Resolve the beat the View should render at `index`: the user's
140
138
  * in-place edit (`overrides`) wins over the on-disk beat, and an
@@ -221,6 +219,57 @@ function focusLeftContainer(container, movedTo) {
221
219
  return !container.contains(movedTo);
222
220
  }
223
221
  //#endregion
222
+ //#region src/vue/subscription.ts
223
+ /** A caller that predates roots is asking for the host's default root. */
224
+ var DEFAULT_ROOT_GETTER = () => void 0;
225
+ /**
226
+ * The object form is checked at runtime, not only by the type.
227
+ *
228
+ * This package is published, so a JavaScript consumer reaches these functions
229
+ * with no call-site checking at all. An object missing `root` type-checks
230
+ * nowhere and passed through here unchanged, and the failure surfaced much
231
+ * later as `sub.root is not a function` — thrown inside a pubsub delivery,
232
+ * where there is no caller left to catch it and the only symptom is a View
233
+ * that silently stops updating (CodeRabbit on #3015).
234
+ *
235
+ * A missing `root` is the one field with a safe answer: absent means the
236
+ * default root, which is exactly what a caller who did not think about roots
237
+ * intended. A missing `filePath` or `handler` has no such answer, so it
238
+ * throws HERE, at the subscribe call, where the stack points at the bug.
239
+ */
240
+ function checkedSubscription(subscription, label) {
241
+ if (typeof subscription.filePath !== "function" || typeof subscription.handler !== "function") throw new TypeError(`${label}: filePath and handler must both be functions`);
242
+ if (subscription.root === void 0) return {
243
+ ...subscription,
244
+ root: DEFAULT_ROOT_GETTER
245
+ };
246
+ if (typeof subscription.root !== "function") throw new TypeError(`${label}: root must be a function returning the root id, or be omitted`);
247
+ return subscription;
248
+ }
249
+ function normalizeGenerationSubscription(first, legacyHandler) {
250
+ if (typeof first !== "function") return checkedSubscription(first, "onGenerationEvent(subscription)");
251
+ if (!legacyHandler) throw new TypeError("onGenerationEvent(filePath, handler): handler is required");
252
+ return {
253
+ filePath: first,
254
+ root: DEFAULT_ROOT_GETTER,
255
+ handler: legacyHandler
256
+ };
257
+ }
258
+ function normalizeScriptChangedSubscription(first, legacyOwnOrigin, legacyHandler) {
259
+ if (typeof first !== "function") {
260
+ const checked = checkedSubscription(first, "onScriptChanged(subscription)");
261
+ if (typeof checked.ownOrigin !== "string") throw new TypeError("onScriptChanged(subscription): ownOrigin must be a string");
262
+ return checked;
263
+ }
264
+ if (typeof legacyOwnOrigin !== "string" || !legacyHandler) throw new TypeError("onScriptChanged(filePath, ownOrigin, handler): ownOrigin and handler are required");
265
+ return {
266
+ filePath: first,
267
+ root: DEFAULT_ROOT_GETTER,
268
+ ownOrigin: legacyOwnOrigin,
269
+ handler: legacyHandler
270
+ };
271
+ }
272
+ //#endregion
224
273
  //#region src/vue/transport.ts
225
274
  var GENERATION_EVENT_KINDS = /* @__PURE__ */ new Set([
226
275
  "beatImage",
@@ -229,26 +278,53 @@ var GENERATION_EVENT_KINDS = /* @__PURE__ */ new Set([
229
278
  "movie",
230
279
  "pdf"
231
280
  ]);
281
+ /**
282
+ * A field that is absent, or a string.
283
+ *
284
+ * `undefined` is a legitimate answer for both `root` and `origin`, so a
285
+ * present-but-wrong-typed value CANNOT be flattened into it: `root: 42` read
286
+ * as "no root" makes a named root's event look like a default-root one, and
287
+ * the subscriber then routes another repository's script to this View
288
+ * (CodeRabbit on #3015 — the same shape the dispatch boundary was just fixed
289
+ * for). Malformed means malformed; the caller rejects the payload.
290
+ */
291
+ function optionalString(value) {
292
+ if (value === void 0) return {
293
+ ok: true,
294
+ value: void 0
295
+ };
296
+ return typeof value === "string" ? {
297
+ ok: true,
298
+ value
299
+ } : { ok: false };
300
+ }
232
301
  function parseScriptChangedEvent(payload) {
233
302
  if (!isRecord(payload)) return null;
234
- const { filePath, origin } = payload;
303
+ const { filePath, origin, root } = payload;
235
304
  if (typeof filePath !== "string") return null;
305
+ const parsedOrigin = optionalString(origin);
306
+ const parsedRoot = optionalString(root);
307
+ if (!parsedOrigin.ok || !parsedRoot.ok) return null;
236
308
  return {
237
309
  filePath,
238
- ...typeof origin === "string" ? { origin } : {}
310
+ ...parsedOrigin.value !== void 0 ? { origin: parsedOrigin.value } : {},
311
+ ...parsedRoot.value !== void 0 ? { root: parsedRoot.value } : {}
239
312
  };
240
313
  }
241
314
  function parseGenerationEvent(payload) {
242
315
  if (!isRecord(payload)) return null;
243
- const { kind, filePath, key, done, error } = payload;
316
+ const { kind, filePath, key, done, error, root } = payload;
244
317
  if (typeof kind !== "string" || !GENERATION_EVENT_KINDS.has(kind)) return null;
245
318
  if (typeof filePath !== "string" || typeof key !== "string" || typeof done !== "boolean") return null;
319
+ const parsedRoot = optionalString(root);
320
+ if (!parsedRoot.ok) return null;
246
321
  return {
247
322
  kind,
248
323
  filePath,
249
324
  key,
250
325
  done,
251
- ...typeof error === "string" ? { error } : {}
326
+ ...typeof error === "string" ? { error } : {},
327
+ ...parsedRoot.value !== void 0 ? { root: parsedRoot.value } : {}
252
328
  };
253
329
  }
254
330
  function useMulmoScriptTransport() {
@@ -275,12 +351,13 @@ function useMulmoScriptTransport() {
275
351
  data: result
276
352
  };
277
353
  }
278
- function onGenerationEvent(filePath, handler) {
354
+ function onGenerationEvent(first, legacyHandler) {
355
+ const { filePath, root, handler } = normalizeGenerationSubscription(first, legacyHandler);
279
356
  return runtime.pubsub.subscribe(require_contract.GENERATION_EVENT, (payload) => {
280
357
  const event = parseGenerationEvent(payload);
281
358
  if (!event) return;
282
359
  const current = filePath();
283
- if (!current || event.filePath !== current) return;
360
+ if (!current || event.filePath !== current || !require_contract.sameRoot(event.root, root())) return;
284
361
  handler(event);
285
362
  });
286
363
  }
@@ -291,11 +368,12 @@ function useMulmoScriptTransport() {
291
368
  * on them would rebuild the element the caret is in on every keystroke, so they are dropped
292
369
  * here. A write from the agent carries no origin and always reaches the handler.
293
370
  */
294
- function onScriptChanged(filePath, ownOrigin, handler) {
371
+ function onScriptChanged(first, legacyOwnOrigin, legacyHandler) {
372
+ const { filePath, root, ownOrigin, handler } = normalizeScriptChangedSubscription(first, legacyOwnOrigin, legacyHandler);
295
373
  return runtime.pubsub.subscribe(require_contract.SCRIPT_CHANGED_EVENT, (payload) => {
296
374
  const event = parseScriptChangedEvent(payload);
297
375
  if (!event) return;
298
- if (!require_contract.shouldReloadForScriptChange(event, filePath(), ownOrigin)) return;
376
+ if (!require_contract.shouldReloadForScriptChange(event, filePath(), ownOrigin, root())) return;
299
377
  handler();
300
378
  });
301
379
  }
@@ -608,7 +686,7 @@ var DECK_SAVE_DEBOUNCE_MS = 300;
608
686
  */
609
687
  var EDITOR_ORIGIN = `deck-editor-${Math.random().toString(36).slice(2)}`;
610
688
  function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
611
- const isDeck = (0, vue.computed)(() => isAllSlideDeck(effectiveScript.value));
689
+ const canEditBeats = (0, vue.computed)(() => hasEditableBeats(effectiveScript.value));
612
690
  const deckScriptInput = (0, vue.computed)(() => effectiveScript.value);
613
691
  let deckSaveTimer = null;
614
692
  let pendingDeckScript = null;
@@ -652,13 +730,18 @@ function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
652
730
  * flushing cannot clobber it out of order.
653
731
  */
654
732
  function watchForeignWrites(reload) {
655
- return api.onScriptChanged(() => filePath.value, EDITOR_ORIGIN, () => {
656
- flushPendingDeckSave();
657
- reload();
733
+ return api.onScriptChanged({
734
+ filePath: () => filePath.value,
735
+ root: () => void 0,
736
+ ownOrigin: EDITOR_ORIGIN,
737
+ handler: () => {
738
+ flushPendingDeckSave();
739
+ reload();
740
+ }
658
741
  });
659
742
  }
660
743
  return {
661
- isDeck,
744
+ canEditBeats,
662
745
  deckScriptInput,
663
746
  onDeckUpdate,
664
747
  flushPendingDeckSave,
@@ -672,6 +755,8 @@ function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
672
755
  var useT = (0, gui_chat_protocol_vue.createUseT)({
673
756
  de: {
674
757
  beatCount: (count) => count === 1 ? `${count} Beat` : `${count} Beats`,
758
+ editTab: "Bearbeiten",
759
+ mediaTab: "Medien",
675
760
  movie: "Video",
676
761
  generating: "Wird generiert…",
677
762
  rendering: "Wird gerendert…",
@@ -704,6 +789,8 @@ var useT = (0, gui_chat_protocol_vue.createUseT)({
704
789
  },
705
790
  en: {
706
791
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
792
+ editTab: "Edit",
793
+ mediaTab: "Media",
707
794
  movie: "Movie",
708
795
  generating: "Generating…",
709
796
  rendering: "Rendering…",
@@ -736,6 +823,8 @@ var useT = (0, gui_chat_protocol_vue.createUseT)({
736
823
  },
737
824
  es: {
738
825
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
826
+ editTab: "Editar",
827
+ mediaTab: "Medios",
739
828
  movie: "Vídeo",
740
829
  generating: "Generando…",
741
830
  rendering: "Renderizando…",
@@ -768,6 +857,8 @@ var useT = (0, gui_chat_protocol_vue.createUseT)({
768
857
  },
769
858
  fr: {
770
859
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
860
+ editTab: "Éditer",
861
+ mediaTab: "Médias",
771
862
  movie: "Film",
772
863
  generating: "Génération…",
773
864
  rendering: "Rendu…",
@@ -800,6 +891,8 @@ var useT = (0, gui_chat_protocol_vue.createUseT)({
800
891
  },
801
892
  ja: {
802
893
  beatCount: (count) => `${count} ビート`,
894
+ editTab: "編集",
895
+ mediaTab: "メディア",
803
896
  movie: "動画",
804
897
  generating: "生成中…",
805
898
  rendering: "レンダリング中…",
@@ -832,6 +925,8 @@ var useT = (0, gui_chat_protocol_vue.createUseT)({
832
925
  },
833
926
  ko: {
834
927
  beatCount: (count) => `${count}개 비트`,
928
+ editTab: "편집",
929
+ mediaTab: "미디어",
835
930
  movie: "영상",
836
931
  generating: "생성 중…",
837
932
  rendering: "렌더링 중…",
@@ -864,6 +959,8 @@ var useT = (0, gui_chat_protocol_vue.createUseT)({
864
959
  },
865
960
  "pt-BR": {
866
961
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
962
+ editTab: "Editar",
963
+ mediaTab: "Mídia",
867
964
  movie: "Vídeo",
868
965
  generating: "Gerando…",
869
966
  rendering: "Renderizando…",
@@ -896,6 +993,8 @@ var useT = (0, gui_chat_protocol_vue.createUseT)({
896
993
  },
897
994
  zh: {
898
995
  beatCount: (count) => `${count} 个 beat`,
996
+ editTab: "编辑",
997
+ mediaTab: "媒体",
899
998
  movie: "视频",
900
999
  generating: "生成中…",
901
1000
  rendering: "渲染中…",
@@ -1325,115 +1424,120 @@ var _hoisted_10 = { class: "flex-1 min-w-0" };
1325
1424
  var _hoisted_11 = { class: "font-medium" };
1326
1425
  var _hoisted_12 = { class: "break-words whitespace-pre-wrap mt-0.5" };
1327
1426
  var _hoisted_13 = ["disabled"];
1328
- var _hoisted_14 = { class: "flex gap-3 items-stretch" };
1329
- var _hoisted_15 = [
1427
+ var _hoisted_14 = {
1428
+ key: 2,
1429
+ class: "flex shrink-0 gap-1 px-2 pt-1 text-[11px]"
1430
+ };
1431
+ var _hoisted_15 = { class: "flex gap-3 items-stretch" };
1432
+ var _hoisted_16 = [
1330
1433
  "onDragover",
1331
1434
  "onDragleave",
1332
1435
  "onDrop"
1333
1436
  ];
1334
- var _hoisted_16 = ["data-testid"];
1335
- var _hoisted_17 = ["src", "data-testid"];
1336
- var _hoisted_18 = [
1437
+ var _hoisted_17 = ["data-testid"];
1438
+ var _hoisted_18 = ["src", "data-testid"];
1439
+ var _hoisted_19 = [
1337
1440
  "title",
1338
1441
  "aria-label",
1339
1442
  "data-testid",
1340
1443
  "onClick"
1341
1444
  ];
1342
- var _hoisted_19 = [
1445
+ var _hoisted_20 = [
1343
1446
  "src",
1344
1447
  "alt",
1345
1448
  "onClick"
1346
1449
  ];
1347
- var _hoisted_20 = [
1450
+ var _hoisted_21 = [
1348
1451
  "title",
1349
1452
  "aria-label",
1350
1453
  "data-testid",
1351
1454
  "onClick"
1352
1455
  ];
1353
- var _hoisted_21 = {
1456
+ var _hoisted_22 = {
1354
1457
  key: 0,
1355
1458
  class: "animate-spin w-5 h-5",
1356
1459
  viewBox: "0 0 24 24",
1357
1460
  fill: "none"
1358
1461
  };
1359
- var _hoisted_22 = {
1462
+ var _hoisted_23 = {
1360
1463
  key: 1,
1361
1464
  class: "material-icons text-3xl"
1362
1465
  };
1363
- var _hoisted_23 = ["disabled", "onClick"];
1364
- var _hoisted_24 = {
1466
+ var _hoisted_24 = ["disabled", "onClick"];
1467
+ var _hoisted_25 = {
1365
1468
  key: 3,
1366
1469
  class: "w-full aspect-video flex flex-col items-center justify-center gap-1 p-2"
1367
1470
  };
1368
- var _hoisted_25 = { class: "text-xs text-green-500" };
1369
- var _hoisted_26 = {
1471
+ var _hoisted_26 = { class: "text-xs text-green-500" };
1472
+ var _hoisted_27 = {
1370
1473
  key: 1,
1371
1474
  class: "text-xs text-red-400 text-center"
1372
1475
  };
1373
- var _hoisted_27 = {
1476
+ var _hoisted_28 = {
1374
1477
  key: 0,
1375
1478
  class: "text-xs text-gray-400 text-center italic leading-relaxed px-1"
1376
1479
  };
1377
- var _hoisted_28 = {
1480
+ var _hoisted_29 = {
1378
1481
  key: 1,
1379
1482
  class: "text-xs text-gray-300"
1380
1483
  };
1381
- var _hoisted_29 = {
1484
+ var _hoisted_30 = {
1382
1485
  key: 2,
1383
1486
  class: "absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none"
1384
1487
  };
1385
- var _hoisted_30 = { class: "text-xs text-blue-500 font-medium" };
1386
- var _hoisted_31 = {
1488
+ var _hoisted_31 = { class: "text-xs text-blue-500 font-medium" };
1489
+ var _hoisted_32 = {
1387
1490
  key: 3,
1388
1491
  class: "absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
1389
1492
  };
1390
- var _hoisted_32 = ["onClick"];
1391
- var _hoisted_33 = { class: "flex flex-col flex-1 min-w-0 px-2 py-1.5" };
1392
- var _hoisted_34 = { class: "text-sm text-gray-800 leading-relaxed" };
1393
- var _hoisted_35 = { class: "flex justify-between mt-auto pt-1" };
1394
- var _hoisted_36 = { class: "flex items-center gap-1" };
1395
- var _hoisted_37 = {
1493
+ var _hoisted_33 = ["onClick"];
1494
+ var _hoisted_34 = { class: "flex flex-col flex-1 min-w-0 px-2 py-1.5" };
1495
+ var _hoisted_35 = { class: "text-sm text-gray-800 leading-relaxed" };
1496
+ var _hoisted_36 = { class: "flex justify-between mt-auto pt-1" };
1497
+ var _hoisted_37 = { class: "flex items-center gap-1" };
1498
+ var _hoisted_38 = {
1396
1499
  key: 0,
1397
1500
  class: "animate-spin w-3 h-3 text-green-400",
1398
1501
  viewBox: "0 0 24 24",
1399
1502
  fill: "none"
1400
1503
  };
1401
- var _hoisted_38 = ["onClick"];
1402
- var _hoisted_39 = ["title"];
1403
- var _hoisted_40 = ["disabled", "onClick"];
1404
- var _hoisted_41 = ["onClick"];
1405
- var _hoisted_42 = [
1504
+ var _hoisted_39 = ["onClick"];
1505
+ var _hoisted_40 = ["title"];
1506
+ var _hoisted_41 = ["disabled", "onClick"];
1507
+ var _hoisted_42 = ["onClick"];
1508
+ var _hoisted_43 = [
1406
1509
  "title",
1407
1510
  "data-testid",
1408
1511
  "onClick"
1409
1512
  ];
1410
- var _hoisted_43 = {
1513
+ var _hoisted_44 = {
1411
1514
  key: 0,
1412
1515
  class: "border-t border-gray-100"
1413
1516
  };
1414
- var _hoisted_44 = ["onUpdate:modelValue", "data-testid"];
1415
- var _hoisted_45 = { class: "flex items-center justify-end gap-2 px-2 pb-2" };
1416
- var _hoisted_46 = {
1517
+ var _hoisted_45 = ["onUpdate:modelValue", "data-testid"];
1518
+ var _hoisted_46 = { class: "flex items-center justify-end gap-2 px-2 pb-2" };
1519
+ var _hoisted_47 = {
1417
1520
  key: 0,
1418
1521
  class: "text-xs text-red-600",
1419
1522
  role: "alert"
1420
1523
  };
1421
- var _hoisted_47 = [
1524
+ var _hoisted_48 = [
1422
1525
  "disabled",
1423
1526
  "data-testid",
1424
1527
  "onClick"
1425
1528
  ];
1426
- var _hoisted_48 = {
1529
+ var _hoisted_49 = {
1427
1530
  key: 0,
1428
1531
  class: "flex items-center justify-center h-32 text-gray-400 text-sm"
1429
1532
  };
1430
- var _hoisted_49 = { class: "bottom-bar-wrapper" };
1431
- var _hoisted_50 = { class: "editor-actions" };
1432
- var _hoisted_51 = ["disabled"];
1433
- var _hoisted_52 = ["title"];
1434
- var _hoisted_53 = { class: "material-icons" };
1533
+ var _hoisted_50 = { class: "bottom-bar-wrapper" };
1534
+ var _hoisted_51 = { class: "editor-actions" };
1535
+ var _hoisted_52 = ["disabled"];
1536
+ var _hoisted_53 = ["title"];
1537
+ var _hoisted_54 = { class: "material-icons" };
1435
1538
  var SILENT_BEAT_DEFAULT_SEC = 3;
1436
1539
  var MS_PER_SECOND = 1e3;
1540
+ var BEAT_TAB_BASE = "rounded px-2 py-0.5 font-sans";
1437
1541
  var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.defineComponent)({
1438
1542
  __name: "View",
1439
1543
  props: { selectedResult: {} },
@@ -1600,12 +1704,27 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1600
1704
  }
1601
1705
  });
1602
1706
  }
1603
- const { isDeck, deckScriptInput, onDeckUpdate, flushPendingDeckSave, watchForeignWrites } = useDeckEditor({
1707
+ const { canEditBeats, deckScriptInput, onDeckUpdate, flushPendingDeckSave, watchForeignWrites } = useDeckEditor({
1604
1708
  api,
1605
1709
  filePath,
1606
1710
  effectiveScript,
1607
1711
  commitScript
1608
1712
  });
1713
+ /**
1714
+ * Which pane the beats are shown in.
1715
+ *
1716
+ * `edit` is the beat editor — every beat type, edited in place. `media` is the per-beat list,
1717
+ * which is the only place audio / image / movie generation lives. A script with nothing to edit
1718
+ * has only the list, so the switch is hidden and this is ignored.
1719
+ *
1720
+ * `media` is the default because opening the script is what triggers rendering each beat's
1721
+ * image: the auto-render on mount lives in that list, so defaulting to `edit` silently stopped
1722
+ * thumbnails from being produced at all. Someone who wants to edit clicks once; nobody has to
1723
+ * click to get the previews they always got.
1724
+ */
1725
+ const beatPane = (0, vue.ref)("media");
1726
+ const showBeatEditor = (0, vue.computed)(() => canEditBeats.value && beatPane.value === "edit");
1727
+ const beatPaneTabClass = (active) => [BEAT_TAB_BASE, active ? "bg-gray-700 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"];
1609
1728
  const unsubscribeForeignWrites = watchForeignWrites(() => {
1610
1729
  refreshScriptFromDisk();
1611
1730
  });
@@ -1980,14 +2099,18 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1980
2099
  }
1981
2100
  (0, vue.onMounted)(initializeScript);
1982
2101
  (0, vue.watch)(() => props.selectedResult, initializeScript);
1983
- const unsubscribeGenerationEvents = api.onGenerationEvent(() => filePath.value, (event) => {
1984
- if (!event.done) {
1985
- reflectGenerationStart(event);
1986
- return;
2102
+ const unsubscribeGenerationEvents = api.onGenerationEvent({
2103
+ filePath: () => filePath.value,
2104
+ root: () => void 0,
2105
+ handler: (event) => {
2106
+ if (!event.done) {
2107
+ reflectGenerationStart(event);
2108
+ return;
2109
+ }
2110
+ reflectGenerationFinish(event).catch((err) => {
2111
+ console.error("[presentMulmoScript] reload on finish failed:", err);
2112
+ });
1987
2113
  }
1988
- reflectGenerationFinish(event).catch((err) => {
1989
- console.error("[presentMulmoScript] reload on finish failed:", err);
1990
- });
1991
2114
  });
1992
2115
  function reflectGenerationStart(entry) {
1993
2116
  if (entry.kind === "beatImage") {
@@ -2062,7 +2185,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2062
2185
  "onDownloadPdf"
2063
2186
  ])]),
2064
2187
  (0, vue.unref)(movieError) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_9, [
2065
- _cache[3] || (_cache[3] = (0, vue.createElementVNode)("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
2188
+ _cache[5] || (_cache[5] = (0, vue.createElementVNode)("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
2066
2189
  (0, vue.createElementVNode)("div", _hoisted_10, [(0, vue.createElementVNode)("div", _hoisted_11, (0, vue.toDisplayString)((0, vue.unref)(m).movieGenerationFailed), 1), (0, vue.createElementVNode)("div", _hoisted_12, (0, vue.toDisplayString)((0, vue.unref)(movieError)), 1)]),
2067
2190
  (0, vue.createElementVNode)("button", {
2068
2191
  class: "shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50",
@@ -2102,8 +2225,19 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2102
2225
  "onCharDrop",
2103
2226
  "onRenderCharacter"
2104
2227
  ])) : (0, vue.createCommentVNode)("", true),
2105
- (0, vue.unref)(isDeck) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
2106
- key: 2,
2228
+ (0, vue.unref)(canEditBeats) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_14, [(0, vue.createElementVNode)("button", {
2229
+ type: "button",
2230
+ class: (0, vue.normalizeClass)(beatPaneTabClass(beatPane.value === "edit")),
2231
+ "data-testid": "mulmo-script-tab-edit",
2232
+ onClick: _cache[1] || (_cache[1] = ($event) => beatPane.value = "edit")
2233
+ }, (0, vue.toDisplayString)((0, vue.unref)(m).editTab), 3), (0, vue.createElementVNode)("button", {
2234
+ type: "button",
2235
+ class: (0, vue.normalizeClass)(beatPaneTabClass(beatPane.value === "media")),
2236
+ "data-testid": "mulmo-script-tab-media",
2237
+ onClick: _cache[2] || (_cache[2] = ($event) => beatPane.value = "media")
2238
+ }, (0, vue.toDisplayString)((0, vue.unref)(m).mediaTab), 3)])) : (0, vue.createCommentVNode)("", true),
2239
+ showBeatEditor.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
2240
+ key: 3,
2107
2241
  class: "flex-1 overflow-hidden",
2108
2242
  "data-testid": "mulmo-script-deck-editor",
2109
2243
  onFocusout: onDeckFocusOut
@@ -2111,7 +2245,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2111
2245
  beats: deckBeats.value,
2112
2246
  "onUpdate:beats": onDeckBeatsUpdate
2113
2247
  }, null, 8, ["beats"])], 32)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
2114
- key: 3,
2248
+ key: 4,
2115
2249
  ref_key: "beatListEl",
2116
2250
  ref: beatListEl,
2117
2251
  class: "flex-1 overflow-y-auto p-2 space-y-1.5"
@@ -2119,7 +2253,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2119
2253
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
2120
2254
  key: index,
2121
2255
  class: "rounded-lg border border-gray-200 overflow-hidden"
2122
- }, [(0, vue.createElementVNode)("div", _hoisted_14, [(0, vue.createElementVNode)("div", {
2256
+ }, [(0, vue.createElementVNode)("div", _hoisted_15, [(0, vue.createElementVNode)("div", {
2123
2257
  class: (0, vue.normalizeClass)(["relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors", beatDragOver[index] ? "bg-blue-50" : ""]),
2124
2258
  onDragover: ($event) => onBeatDragOver($event, index),
2125
2259
  onDragleave: ($event) => onBeatDragLeave(index),
@@ -2128,27 +2262,27 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2128
2262
  (0, vue.createElementVNode)("div", {
2129
2263
  class: "absolute top-1.5 left-1.5 z-10 px-1.5 py-0.5 rounded bg-black/55 text-white text-xs font-medium leading-none pointer-events-none",
2130
2264
  "data-testid": `mulmo-script-beat-number-${index}`
2131
- }, (0, vue.toDisplayString)(index + 1), 9, _hoisted_16),
2265
+ }, (0, vue.toDisplayString)(index + 1), 9, _hoisted_17),
2132
2266
  (0, vue.unref)(beatMovieOpen)[index] && (0, vue.unref)(beatMovieUrls)[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [(0, vue.createElementVNode)("video", {
2133
2267
  src: (0, vue.unref)(beatMovieUrls)[index],
2134
2268
  class: "w-full object-contain",
2135
2269
  controls: "",
2136
2270
  autoplay: "",
2137
2271
  "data-testid": `mulmo-script-beat-movie-player-${index}`
2138
- }, null, 8, _hoisted_17), (0, vue.createElementVNode)("button", {
2272
+ }, null, 8, _hoisted_18), (0, vue.createElementVNode)("button", {
2139
2273
  class: "absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50",
2140
2274
  title: (0, vue.unref)(m).close,
2141
2275
  "aria-label": (0, vue.unref)(m).close,
2142
2276
  "data-testid": `mulmo-script-beat-movie-close-${index}`,
2143
2277
  onClick: (0, vue.withModifiers)(($event) => (0, vue.unref)(closeBeatMovie)(index), ["stop"])
2144
- }, [..._cache[4] || (_cache[4] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_18)], 64)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, [
2278
+ }, [..._cache[6] || (_cache[6] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_19)], 64)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, [
2145
2279
  renderedImages[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("img", {
2146
2280
  key: 0,
2147
2281
  src: renderedImages[index],
2148
2282
  class: "w-full object-contain cursor-zoom-in",
2149
2283
  alt: `Beat ${index + 1}`,
2150
2284
  onClick: ($event) => openLightbox(index)
2151
- }, null, 8, _hoisted_19)) : (0, vue.createCommentVNode)("", true),
2285
+ }, null, 8, _hoisted_20)) : (0, vue.createCommentVNode)("", true),
2152
2286
  renderedImages[index] && (0, vue.unref)(beatMovies)[index] && canFetchMedia.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
2153
2287
  key: 1,
2154
2288
  class: "absolute inset-0 m-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70",
@@ -2156,7 +2290,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2156
2290
  "aria-label": (0, vue.unref)(m).play,
2157
2291
  "data-testid": `mulmo-script-beat-movie-play-${index}`,
2158
2292
  onClick: (0, vue.withModifiers)(($event) => (0, vue.unref)(playBeatMovie)(index), ["stop"])
2159
- }, [(0, vue.unref)(beatMovieLoading)[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("svg", _hoisted_21, [..._cache[5] || (_cache[5] = [(0, vue.createElementVNode)("circle", {
2293
+ }, [(0, vue.unref)(beatMovieLoading)[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("svg", _hoisted_22, [..._cache[7] || (_cache[7] = [(0, vue.createElementVNode)("circle", {
2160
2294
  class: "opacity-25",
2161
2295
  cx: "12",
2162
2296
  cy: "12",
@@ -2167,13 +2301,13 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2167
2301
  class: "opacity-75",
2168
2302
  fill: "currentColor",
2169
2303
  d: "M4 12a8 8 0 018-8v8H4z"
2170
- }, null, -1)])])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_22, "play_arrow"))], 8, _hoisted_20)) : (0, vue.createCommentVNode)("", true),
2304
+ }, null, -1)])])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_23, "play_arrow"))], 8, _hoisted_21)) : (0, vue.createCommentVNode)("", true),
2171
2305
  renderedImages[index] && renderState[index] !== "rendering" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
2172
2306
  key: 2,
2173
2307
  class: "absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed",
2174
2308
  disabled: (0, vue.unref)(movieGenerating),
2175
2309
  onClick: (0, vue.withModifiers)(($event) => regenerateBeat(index), ["stop"])
2176
- }, " ↺ ", 8, _hoisted_23)) : !renderedImages[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_24, [renderState[index] === "rendering" || (0, vue.unref)(movieGenerating) && !renderedImages[index] && effectiveBeat$1(index).imagePrompt ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [_cache[6] || (_cache[6] = (0, vue.createElementVNode)("svg", {
2310
+ }, " ↺ ", 8, _hoisted_24)) : !renderedImages[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_25, [renderState[index] === "rendering" || (0, vue.unref)(movieGenerating) && !renderedImages[index] && effectiveBeat$1(index).imagePrompt ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [_cache[8] || (_cache[8] = (0, vue.createElementVNode)("svg", {
2177
2311
  class: "animate-spin w-4 h-4 text-green-400",
2178
2312
  viewBox: "0 0 24 24",
2179
2313
  fill: "none"
@@ -2188,15 +2322,15 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2188
2322
  class: "opacity-75",
2189
2323
  fill: "currentColor",
2190
2324
  d: "M4 12a8 8 0 018-8v8H4z"
2191
- })], -1)), (0, vue.createElementVNode)("span", _hoisted_25, (0, vue.toDisplayString)((0, vue.unref)(m).rendering), 1)], 64)) : renderState[index] === "error" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_26, (0, vue.toDisplayString)(renderErrors[index]), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 2 }, [effectiveBeat$1(index).imagePrompt ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_27, (0, vue.toDisplayString)(effectiveBeat$1(index).imagePrompt), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_28, (0, vue.toDisplayString)(beat.image?.type ?? "—"), 1))], 64))])) : (0, vue.createCommentVNode)("", true)
2325
+ })], -1)), (0, vue.createElementVNode)("span", _hoisted_26, (0, vue.toDisplayString)((0, vue.unref)(m).rendering), 1)], 64)) : renderState[index] === "error" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_27, (0, vue.toDisplayString)(renderErrors[index]), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 2 }, [effectiveBeat$1(index).imagePrompt ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_28, (0, vue.toDisplayString)(effectiveBeat$1(index).imagePrompt), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_29, (0, vue.toDisplayString)(beat.image?.type ?? "—"), 1))], 64))])) : (0, vue.createCommentVNode)("", true)
2192
2326
  ], 64)),
2193
- beatDragOver[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_29, [(0, vue.createElementVNode)("span", _hoisted_30, (0, vue.toDisplayString)((0, vue.unref)(m).drop), 1)])) : !renderedImages[index] && renderState[index] !== "rendering" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_31, (0, vue.toDisplayString)((0, vue.unref)(m).orDropImage), 1)) : (0, vue.createCommentVNode)("", true),
2327
+ beatDragOver[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_30, [(0, vue.createElementVNode)("span", _hoisted_31, (0, vue.toDisplayString)((0, vue.unref)(m).drop), 1)])) : !renderedImages[index] && renderState[index] !== "rendering" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_32, (0, vue.toDisplayString)((0, vue.unref)(m).orDropImage), 1)) : (0, vue.createCommentVNode)("", true),
2194
2328
  !renderedImages[index] && renderState[index] !== "rendering" && !(0, vue.unref)(movieGenerating) && !(0, vue.unref)(isBeatImageReference)(effectiveBeat$1(index)) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
2195
2329
  key: 4,
2196
2330
  class: "absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50",
2197
2331
  onClick: ($event) => renderBeat(index)
2198
- }, (0, vue.toDisplayString)((0, vue.unref)(m).generate), 9, _hoisted_32)) : (0, vue.createCommentVNode)("", true)
2199
- ], 42, _hoisted_15), (0, vue.createElementVNode)("div", _hoisted_33, [(0, vue.createElementVNode)("span", _hoisted_34, (0, vue.toDisplayString)(effectiveBeat$1(index).text), 1), (0, vue.createElementVNode)("div", _hoisted_35, [(0, vue.createElementVNode)("div", _hoisted_36, [audioState[index] === "generating" || (0, vue.unref)(movieGenerating) && !beatAudios[index] && effectiveBeat$1(index).text ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("svg", _hoisted_37, [..._cache[7] || (_cache[7] = [(0, vue.createElementVNode)("circle", {
2332
+ }, (0, vue.toDisplayString)((0, vue.unref)(m).generate), 9, _hoisted_33)) : (0, vue.createCommentVNode)("", true)
2333
+ ], 42, _hoisted_16), (0, vue.createElementVNode)("div", _hoisted_34, [(0, vue.createElementVNode)("span", _hoisted_35, (0, vue.toDisplayString)(effectiveBeat$1(index).text), 1), (0, vue.createElementVNode)("div", _hoisted_36, [(0, vue.createElementVNode)("div", _hoisted_37, [audioState[index] === "generating" || (0, vue.unref)(movieGenerating) && !beatAudios[index] && effectiveBeat$1(index).text ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("svg", _hoisted_38, [..._cache[9] || (_cache[9] = [(0, vue.createElementVNode)("circle", {
2200
2334
  class: "opacity-25",
2201
2335
  cx: "12",
2202
2336
  cy: "12",
@@ -2211,24 +2345,24 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2211
2345
  key: 1,
2212
2346
  class: (0, vue.normalizeClass)(["text-xs px-2 py-0.5 rounded border", playingAudio.value?.index === index ? "border-red-400 text-red-600 hover:bg-red-50" : "border-green-400 text-green-600 hover:bg-green-50"]),
2213
2347
  onClick: ($event) => playAudio(index)
2214
- }, (0, vue.toDisplayString)(playingAudio.value?.index === index ? (0, vue.unref)(m).stop : (0, vue.unref)(m).play), 11, _hoisted_38)) : audioErrors[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 2 }, [(0, vue.createElementVNode)("span", {
2348
+ }, (0, vue.toDisplayString)(playingAudio.value?.index === index ? (0, vue.unref)(m).stop : (0, vue.unref)(m).play), 11, _hoisted_39)) : audioErrors[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 2 }, [(0, vue.createElementVNode)("span", {
2215
2349
  class: "text-xs text-red-400 truncate min-w-0 max-w-[20rem]",
2216
2350
  title: audioErrors[index]
2217
- }, (0, vue.toDisplayString)((0, vue.unref)(m).errPrefix) + " " + (0, vue.toDisplayString)(audioErrors[index]), 9, _hoisted_39), effectiveBeat$1(index).text ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
2351
+ }, (0, vue.toDisplayString)((0, vue.unref)(m).errPrefix) + " " + (0, vue.toDisplayString)(audioErrors[index]), 9, _hoisted_40), effectiveBeat$1(index).text ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
2218
2352
  key: 0,
2219
2353
  class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
2220
2354
  disabled: (0, vue.unref)(movieGenerating),
2221
2355
  onClick: ($event) => generateAudio(index)
2222
- }, " ↺ ", 8, _hoisted_40)) : (0, vue.createCommentVNode)("", true)], 64)) : effectiveBeat$1(index).text ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
2356
+ }, " ↺ ", 8, _hoisted_41)) : (0, vue.createCommentVNode)("", true)], 64)) : effectiveBeat$1(index).text ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
2223
2357
  key: 3,
2224
2358
  class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50",
2225
2359
  onClick: ($event) => generateAudio(index)
2226
- }, (0, vue.toDisplayString)((0, vue.unref)(m).generateAudio), 9, _hoisted_41)) : (0, vue.createCommentVNode)("", true)]), (0, vue.createElementVNode)("button", {
2360
+ }, (0, vue.toDisplayString)((0, vue.unref)(m).generateAudio), 9, _hoisted_42)) : (0, vue.createCommentVNode)("", true)]), (0, vue.createElementVNode)("button", {
2227
2361
  class: "text-gray-400 hover:text-gray-600",
2228
2362
  title: sourceOpen[index] ? "Hide source" : "Show source",
2229
2363
  "data-testid": `mulmo-script-beat-source-toggle-${index}`,
2230
2364
  onClick: ($event) => toggleSource(index)
2231
- }, [..._cache[8] || (_cache[8] = [(0, vue.createElementVNode)("svg", {
2365
+ }, [..._cache[10] || (_cache[10] = [(0, vue.createElementVNode)("svg", {
2232
2366
  xmlns: "http://www.w3.org/2000/svg",
2233
2367
  class: "w-3.5 h-3.5",
2234
2368
  viewBox: "0 0 24 24",
@@ -2237,36 +2371,36 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2237
2371
  "stroke-width": "2",
2238
2372
  "stroke-linecap": "round",
2239
2373
  "stroke-linejoin": "round"
2240
- }, [(0, vue.createElementVNode)("polyline", { points: "16 18 22 12 16 6" }), (0, vue.createElementVNode)("polyline", { points: "8 6 2 12 8 18" })], -1)])], 8, _hoisted_42)])])]), sourceOpen[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_43, [(0, vue.withDirectives)((0, vue.createElementVNode)("textarea", {
2374
+ }, [(0, vue.createElementVNode)("polyline", { points: "16 18 22 12 16 6" }), (0, vue.createElementVNode)("polyline", { points: "8 6 2 12 8 18" })], -1)])], 8, _hoisted_43)])])]), sourceOpen[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_44, [(0, vue.withDirectives)((0, vue.createElementVNode)("textarea", {
2241
2375
  "onUpdate:modelValue": ($event) => sourceText[index] = $event,
2242
2376
  class: (0, vue.normalizeClass)(["w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none", isValidBeat$1(index) ? "outline-none" : "outline outline-2 outline-red-400"]),
2243
2377
  rows: "8",
2244
2378
  spellcheck: "false",
2245
2379
  "data-testid": `mulmo-script-beat-source-textarea-${index}`
2246
- }, null, 10, _hoisted_44), [[vue.vModelText, sourceText[index]]]), (0, vue.createElementVNode)("div", _hoisted_45, [beatSaveErrors[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_46, (0, vue.toDisplayString)(beatSaveErrors[index].kind === "invalidJson" ? (0, vue.unref)(m).saveErrorInvalidJson(beatSaveErrors[index].error) : (0, vue.unref)(m).saveErrorSaveFailed(beatSaveErrors[index].error)), 1)) : (0, vue.createCommentVNode)("", true), (0, vue.createElementVNode)("button", {
2380
+ }, null, 10, _hoisted_45), [[vue.vModelText, sourceText[index]]]), (0, vue.createElementVNode)("div", _hoisted_46, [beatSaveErrors[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_47, (0, vue.toDisplayString)(beatSaveErrors[index].kind === "invalidJson" ? (0, vue.unref)(m).saveErrorInvalidJson(beatSaveErrors[index].error) : (0, vue.unref)(m).saveErrorSaveFailed(beatSaveErrors[index].error)), 1)) : (0, vue.createCommentVNode)("", true), (0, vue.createElementVNode)("button", {
2247
2381
  class: (0, vue.normalizeClass)(["px-2 py-1 text-xs rounded border", isValidBeat$1(index) && !beatSaving[index] ? "border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer" : "border-gray-200 text-gray-300 cursor-not-allowed"]),
2248
2382
  disabled: !isValidBeat$1(index) || !!beatSaving[index],
2249
2383
  "data-testid": `mulmo-script-beat-update-button-${index}`,
2250
2384
  onClick: ($event) => updateBeat(index)
2251
- }, (0, vue.toDisplayString)(beatSaving[index] ? (0, vue.unref)(m).saving : (0, vue.unref)(m).update), 11, _hoisted_47)])])) : (0, vue.createCommentVNode)("", true)]);
2252
- }), 128)), beats.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_48, (0, vue.toDisplayString)((0, vue.unref)(m).noBeats), 1)) : (0, vue.createCommentVNode)("", true)], 512)),
2253
- (0, vue.createElementVNode)("div", _hoisted_49, [(0, vue.createElementVNode)("details", {
2385
+ }, (0, vue.toDisplayString)(beatSaving[index] ? (0, vue.unref)(m).saving : (0, vue.unref)(m).update), 11, _hoisted_48)])])) : (0, vue.createCommentVNode)("", true)]);
2386
+ }), 128)), beats.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_49, (0, vue.toDisplayString)((0, vue.unref)(m).noBeats), 1)) : (0, vue.createCommentVNode)("", true)], 512)),
2387
+ (0, vue.createElementVNode)("div", _hoisted_50, [(0, vue.createElementVNode)("details", {
2254
2388
  ref_key: "sourceDetails",
2255
2389
  ref: sourceDetails,
2256
2390
  class: "script-source",
2257
- onToggle: _cache[2] || (_cache[2] = ($event) => onSourceToggle($event.target.open))
2391
+ onToggle: _cache[4] || (_cache[4] = ($event) => onSourceToggle($event.target.open))
2258
2392
  }, [
2259
2393
  (0, vue.createElementVNode)("summary", null, (0, vue.toDisplayString)((0, vue.unref)(m).editSource), 1),
2260
2394
  (0, vue.withDirectives)((0, vue.createElementVNode)("textarea", {
2261
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => editableSource.value = $event),
2395
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => editableSource.value = $event),
2262
2396
  class: (0, vue.normalizeClass)(["script-editor", { "script-editor-invalid": sourceChanged.value && !sourceValid.value }]),
2263
2397
  spellcheck: "false"
2264
2398
  }, null, 2), [[vue.vModelText, editableSource.value]]),
2265
- (0, vue.createElementVNode)("div", _hoisted_50, [(0, vue.createElementVNode)("button", {
2399
+ (0, vue.createElementVNode)("div", _hoisted_51, [(0, vue.createElementVNode)("button", {
2266
2400
  class: "apply-btn",
2267
2401
  disabled: !sourceChanged.value || !sourceValid.value,
2268
2402
  onClick: applySource
2269
- }, (0, vue.toDisplayString)((0, vue.unref)(m).applyChanges), 9, _hoisted_51), (0, vue.createElementVNode)("button", {
2403
+ }, (0, vue.toDisplayString)((0, vue.unref)(m).applyChanges), 9, _hoisted_52), (0, vue.createElementVNode)("button", {
2270
2404
  class: "cancel-btn",
2271
2405
  onClick: cancelSourceEdit
2272
2406
  }, (0, vue.toDisplayString)((0, vue.unref)(m).cancel), 1)])
@@ -2274,9 +2408,9 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
2274
2408
  class: "copy-btn",
2275
2409
  title: (0, vue.unref)(copied) ? "Copied!" : "Copy",
2276
2410
  onClick: copyText
2277
- }, [(0, vue.createElementVNode)("span", _hoisted_53, (0, vue.toDisplayString)((0, vue.unref)(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_52), [[vue.vShow, !editing.value]])]),
2411
+ }, [(0, vue.createElementVNode)("span", _hoisted_54, (0, vue.toDisplayString)((0, vue.unref)(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_53), [[vue.vShow, !editing.value]])]),
2278
2412
  lightbox.value ? ((0, vue.openBlock)(), (0, vue.createBlock)(BeatLightbox_default, {
2279
- key: 4,
2413
+ key: 5,
2280
2414
  lightbox: lightbox.value,
2281
2415
  "beat-count": beats.value.length,
2282
2416
  "beat-texts": beatTexts.value,
@@ -2312,7 +2446,7 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
2312
2446
  };
2313
2447
  //#endregion
2314
2448
  //#region src/vue/View.vue
2315
- var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-d78be72d"]]);
2449
+ var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-2d5f76ac"]]);
2316
2450
  //#endregion
2317
2451
  //#region src/vue/Preview.vue?vue&type=script&setup=true&lang.ts
2318
2452
  var _hoisted_1 = {