@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.js CHANGED
@@ -1,5 +1,5 @@
1
- import { a as pluginCore, d as errorMessage, f as TOOL_DEFINITION, p as TOOL_NAME } from "./plugin-CAhxJ5WM.js";
2
- import { n as SCRIPT_CHANGED_EVENT, r as shouldReloadForScriptChange, t as GENERATION_EVENT } from "./contract-CIt1ZAtt.js";
1
+ import { a as pluginCore, h as TOOL_NAME, m as TOOL_DEFINITION, p as errorMessage } from "./plugin-Jo8iGv4K.js";
2
+ import { a as shouldReloadForScriptChange, i as sameRoot, n as SCRIPT_CHANGED_EVENT, t as GENERATION_EVENT } from "./contract-BnIl6w_f.js";
3
3
  import { mulmoBeatSchema, mulmoScriptSchema } from "@mulmocast/types";
4
4
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineAsyncComponent, defineComponent, inject, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, vModelText, vShow, watch, withDirectives, withModifiers } from "vue";
5
5
  import { beatsOf, withBeats } from "@mulmocast/beat-editor";
@@ -115,22 +115,20 @@ function beatMayHaveMovie(beat) {
115
115
  function isBeatImageReference(beat) {
116
116
  return beat.image?.type === "beat";
117
117
  }
118
- /** Pure check: is every beat in the script a `slide`-typed beat?
119
- * When true, the View mounts `@mulmocast/beat-editor`'s
120
- * `BeatListEditor` instead of the per-beat list UI (#1575).
121
- * Empty / missing `beats[]` returns false there's nothing to edit
122
- * as a deck, fall through to the existing UI which renders an empty
123
- * state. Mixed scripts (any non-`slide` beat) also return false; that
124
- * case is deferred to a future phase. */
125
- function isAllSlideDeck(script) {
118
+ /** Whether the beat editor has anything to edit.
119
+ *
120
+ * Any beat type, not just `slide`: `@mulmocast/beat-editor` renders and edits all eight
121
+ * (`textSlide` / `markdown` / `chart` / `mermaid` / `image` / `movie` / `slide` /
122
+ * `html_tailwind`). The all-slide test this replaces was a limit of the OLD iframe deck
123
+ * editor, which only understood decks it stayed in place through the migration and kept a
124
+ * markdown script read-only for no reason.
125
+ *
126
+ * Empty / missing `beats[]` is false: there is nothing to edit, and the per-beat list already
127
+ * renders an empty state. */
128
+ function hasEditableBeats(script) {
126
129
  if (!isRecord(script)) return false;
127
130
  const { beats } = script;
128
- if (!Array.isArray(beats) || beats.length === 0) return false;
129
- return beats.every((beat) => {
130
- if (!isRecord(beat)) return false;
131
- const { image } = beat;
132
- return isRecord(image) && image.type === "slide";
133
- });
131
+ return Array.isArray(beats) && beats.length > 0;
134
132
  }
135
133
  /** Resolve the beat the View should render at `index`: the user's
136
134
  * in-place edit (`overrides`) wins over the on-disk beat, and an
@@ -217,6 +215,57 @@ function focusLeftContainer(container, movedTo) {
217
215
  return !container.contains(movedTo);
218
216
  }
219
217
  //#endregion
218
+ //#region src/vue/subscription.ts
219
+ /** A caller that predates roots is asking for the host's default root. */
220
+ var DEFAULT_ROOT_GETTER = () => void 0;
221
+ /**
222
+ * The object form is checked at runtime, not only by the type.
223
+ *
224
+ * This package is published, so a JavaScript consumer reaches these functions
225
+ * with no call-site checking at all. An object missing `root` type-checks
226
+ * nowhere and passed through here unchanged, and the failure surfaced much
227
+ * later as `sub.root is not a function` — thrown inside a pubsub delivery,
228
+ * where there is no caller left to catch it and the only symptom is a View
229
+ * that silently stops updating (CodeRabbit on #3015).
230
+ *
231
+ * A missing `root` is the one field with a safe answer: absent means the
232
+ * default root, which is exactly what a caller who did not think about roots
233
+ * intended. A missing `filePath` or `handler` has no such answer, so it
234
+ * throws HERE, at the subscribe call, where the stack points at the bug.
235
+ */
236
+ function checkedSubscription(subscription, label) {
237
+ if (typeof subscription.filePath !== "function" || typeof subscription.handler !== "function") throw new TypeError(`${label}: filePath and handler must both be functions`);
238
+ if (subscription.root === void 0) return {
239
+ ...subscription,
240
+ root: DEFAULT_ROOT_GETTER
241
+ };
242
+ if (typeof subscription.root !== "function") throw new TypeError(`${label}: root must be a function returning the root id, or be omitted`);
243
+ return subscription;
244
+ }
245
+ function normalizeGenerationSubscription(first, legacyHandler) {
246
+ if (typeof first !== "function") return checkedSubscription(first, "onGenerationEvent(subscription)");
247
+ if (!legacyHandler) throw new TypeError("onGenerationEvent(filePath, handler): handler is required");
248
+ return {
249
+ filePath: first,
250
+ root: DEFAULT_ROOT_GETTER,
251
+ handler: legacyHandler
252
+ };
253
+ }
254
+ function normalizeScriptChangedSubscription(first, legacyOwnOrigin, legacyHandler) {
255
+ if (typeof first !== "function") {
256
+ const checked = checkedSubscription(first, "onScriptChanged(subscription)");
257
+ if (typeof checked.ownOrigin !== "string") throw new TypeError("onScriptChanged(subscription): ownOrigin must be a string");
258
+ return checked;
259
+ }
260
+ if (typeof legacyOwnOrigin !== "string" || !legacyHandler) throw new TypeError("onScriptChanged(filePath, ownOrigin, handler): ownOrigin and handler are required");
261
+ return {
262
+ filePath: first,
263
+ root: DEFAULT_ROOT_GETTER,
264
+ ownOrigin: legacyOwnOrigin,
265
+ handler: legacyHandler
266
+ };
267
+ }
268
+ //#endregion
220
269
  //#region src/vue/transport.ts
221
270
  var GENERATION_EVENT_KINDS = /* @__PURE__ */ new Set([
222
271
  "beatImage",
@@ -225,26 +274,53 @@ var GENERATION_EVENT_KINDS = /* @__PURE__ */ new Set([
225
274
  "movie",
226
275
  "pdf"
227
276
  ]);
277
+ /**
278
+ * A field that is absent, or a string.
279
+ *
280
+ * `undefined` is a legitimate answer for both `root` and `origin`, so a
281
+ * present-but-wrong-typed value CANNOT be flattened into it: `root: 42` read
282
+ * as "no root" makes a named root's event look like a default-root one, and
283
+ * the subscriber then routes another repository's script to this View
284
+ * (CodeRabbit on #3015 — the same shape the dispatch boundary was just fixed
285
+ * for). Malformed means malformed; the caller rejects the payload.
286
+ */
287
+ function optionalString(value) {
288
+ if (value === void 0) return {
289
+ ok: true,
290
+ value: void 0
291
+ };
292
+ return typeof value === "string" ? {
293
+ ok: true,
294
+ value
295
+ } : { ok: false };
296
+ }
228
297
  function parseScriptChangedEvent(payload) {
229
298
  if (!isRecord(payload)) return null;
230
- const { filePath, origin } = payload;
299
+ const { filePath, origin, root } = payload;
231
300
  if (typeof filePath !== "string") return null;
301
+ const parsedOrigin = optionalString(origin);
302
+ const parsedRoot = optionalString(root);
303
+ if (!parsedOrigin.ok || !parsedRoot.ok) return null;
232
304
  return {
233
305
  filePath,
234
- ...typeof origin === "string" ? { origin } : {}
306
+ ...parsedOrigin.value !== void 0 ? { origin: parsedOrigin.value } : {},
307
+ ...parsedRoot.value !== void 0 ? { root: parsedRoot.value } : {}
235
308
  };
236
309
  }
237
310
  function parseGenerationEvent(payload) {
238
311
  if (!isRecord(payload)) return null;
239
- const { kind, filePath, key, done, error } = payload;
312
+ const { kind, filePath, key, done, error, root } = payload;
240
313
  if (typeof kind !== "string" || !GENERATION_EVENT_KINDS.has(kind)) return null;
241
314
  if (typeof filePath !== "string" || typeof key !== "string" || typeof done !== "boolean") return null;
315
+ const parsedRoot = optionalString(root);
316
+ if (!parsedRoot.ok) return null;
242
317
  return {
243
318
  kind,
244
319
  filePath,
245
320
  key,
246
321
  done,
247
- ...typeof error === "string" ? { error } : {}
322
+ ...typeof error === "string" ? { error } : {},
323
+ ...parsedRoot.value !== void 0 ? { root: parsedRoot.value } : {}
248
324
  };
249
325
  }
250
326
  function useMulmoScriptTransport() {
@@ -271,12 +347,13 @@ function useMulmoScriptTransport() {
271
347
  data: result
272
348
  };
273
349
  }
274
- function onGenerationEvent(filePath, handler) {
350
+ function onGenerationEvent(first, legacyHandler) {
351
+ const { filePath, root, handler } = normalizeGenerationSubscription(first, legacyHandler);
275
352
  return runtime.pubsub.subscribe(GENERATION_EVENT, (payload) => {
276
353
  const event = parseGenerationEvent(payload);
277
354
  if (!event) return;
278
355
  const current = filePath();
279
- if (!current || event.filePath !== current) return;
356
+ if (!current || event.filePath !== current || !sameRoot(event.root, root())) return;
280
357
  handler(event);
281
358
  });
282
359
  }
@@ -287,11 +364,12 @@ function useMulmoScriptTransport() {
287
364
  * on them would rebuild the element the caret is in on every keystroke, so they are dropped
288
365
  * here. A write from the agent carries no origin and always reaches the handler.
289
366
  */
290
- function onScriptChanged(filePath, ownOrigin, handler) {
367
+ function onScriptChanged(first, legacyOwnOrigin, legacyHandler) {
368
+ const { filePath, root, ownOrigin, handler } = normalizeScriptChangedSubscription(first, legacyOwnOrigin, legacyHandler);
291
369
  return runtime.pubsub.subscribe(SCRIPT_CHANGED_EVENT, (payload) => {
292
370
  const event = parseScriptChangedEvent(payload);
293
371
  if (!event) return;
294
- if (!shouldReloadForScriptChange(event, filePath(), ownOrigin)) return;
372
+ if (!shouldReloadForScriptChange(event, filePath(), ownOrigin, root())) return;
295
373
  handler();
296
374
  });
297
375
  }
@@ -604,7 +682,7 @@ var DECK_SAVE_DEBOUNCE_MS = 300;
604
682
  */
605
683
  var EDITOR_ORIGIN = `deck-editor-${Math.random().toString(36).slice(2)}`;
606
684
  function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
607
- const isDeck = computed(() => isAllSlideDeck(effectiveScript.value));
685
+ const canEditBeats = computed(() => hasEditableBeats(effectiveScript.value));
608
686
  const deckScriptInput = computed(() => effectiveScript.value);
609
687
  let deckSaveTimer = null;
610
688
  let pendingDeckScript = null;
@@ -648,13 +726,18 @@ function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
648
726
  * flushing cannot clobber it out of order.
649
727
  */
650
728
  function watchForeignWrites(reload) {
651
- return api.onScriptChanged(() => filePath.value, EDITOR_ORIGIN, () => {
652
- flushPendingDeckSave();
653
- reload();
729
+ return api.onScriptChanged({
730
+ filePath: () => filePath.value,
731
+ root: () => void 0,
732
+ ownOrigin: EDITOR_ORIGIN,
733
+ handler: () => {
734
+ flushPendingDeckSave();
735
+ reload();
736
+ }
654
737
  });
655
738
  }
656
739
  return {
657
- isDeck,
740
+ canEditBeats,
658
741
  deckScriptInput,
659
742
  onDeckUpdate,
660
743
  flushPendingDeckSave,
@@ -668,6 +751,8 @@ function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
668
751
  var useT = createUseT({
669
752
  de: {
670
753
  beatCount: (count) => count === 1 ? `${count} Beat` : `${count} Beats`,
754
+ editTab: "Bearbeiten",
755
+ mediaTab: "Medien",
671
756
  movie: "Video",
672
757
  generating: "Wird generiert…",
673
758
  rendering: "Wird gerendert…",
@@ -700,6 +785,8 @@ var useT = createUseT({
700
785
  },
701
786
  en: {
702
787
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
788
+ editTab: "Edit",
789
+ mediaTab: "Media",
703
790
  movie: "Movie",
704
791
  generating: "Generating…",
705
792
  rendering: "Rendering…",
@@ -732,6 +819,8 @@ var useT = createUseT({
732
819
  },
733
820
  es: {
734
821
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
822
+ editTab: "Editar",
823
+ mediaTab: "Medios",
735
824
  movie: "Vídeo",
736
825
  generating: "Generando…",
737
826
  rendering: "Renderizando…",
@@ -764,6 +853,8 @@ var useT = createUseT({
764
853
  },
765
854
  fr: {
766
855
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
856
+ editTab: "Éditer",
857
+ mediaTab: "Médias",
767
858
  movie: "Film",
768
859
  generating: "Génération…",
769
860
  rendering: "Rendu…",
@@ -796,6 +887,8 @@ var useT = createUseT({
796
887
  },
797
888
  ja: {
798
889
  beatCount: (count) => `${count} ビート`,
890
+ editTab: "編集",
891
+ mediaTab: "メディア",
799
892
  movie: "動画",
800
893
  generating: "生成中…",
801
894
  rendering: "レンダリング中…",
@@ -828,6 +921,8 @@ var useT = createUseT({
828
921
  },
829
922
  ko: {
830
923
  beatCount: (count) => `${count}개 비트`,
924
+ editTab: "편집",
925
+ mediaTab: "미디어",
831
926
  movie: "영상",
832
927
  generating: "생성 중…",
833
928
  rendering: "렌더링 중…",
@@ -860,6 +955,8 @@ var useT = createUseT({
860
955
  },
861
956
  "pt-BR": {
862
957
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
958
+ editTab: "Editar",
959
+ mediaTab: "Mídia",
863
960
  movie: "Vídeo",
864
961
  generating: "Gerando…",
865
962
  rendering: "Renderizando…",
@@ -892,6 +989,8 @@ var useT = createUseT({
892
989
  },
893
990
  zh: {
894
991
  beatCount: (count) => `${count} 个 beat`,
992
+ editTab: "编辑",
993
+ mediaTab: "媒体",
895
994
  movie: "视频",
896
995
  generating: "生成中…",
897
996
  rendering: "渲染中…",
@@ -1321,115 +1420,120 @@ var _hoisted_10 = { class: "flex-1 min-w-0" };
1321
1420
  var _hoisted_11 = { class: "font-medium" };
1322
1421
  var _hoisted_12 = { class: "break-words whitespace-pre-wrap mt-0.5" };
1323
1422
  var _hoisted_13 = ["disabled"];
1324
- var _hoisted_14 = { class: "flex gap-3 items-stretch" };
1325
- var _hoisted_15 = [
1423
+ var _hoisted_14 = {
1424
+ key: 2,
1425
+ class: "flex shrink-0 gap-1 px-2 pt-1 text-[11px]"
1426
+ };
1427
+ var _hoisted_15 = { class: "flex gap-3 items-stretch" };
1428
+ var _hoisted_16 = [
1326
1429
  "onDragover",
1327
1430
  "onDragleave",
1328
1431
  "onDrop"
1329
1432
  ];
1330
- var _hoisted_16 = ["data-testid"];
1331
- var _hoisted_17 = ["src", "data-testid"];
1332
- var _hoisted_18 = [
1433
+ var _hoisted_17 = ["data-testid"];
1434
+ var _hoisted_18 = ["src", "data-testid"];
1435
+ var _hoisted_19 = [
1333
1436
  "title",
1334
1437
  "aria-label",
1335
1438
  "data-testid",
1336
1439
  "onClick"
1337
1440
  ];
1338
- var _hoisted_19 = [
1441
+ var _hoisted_20 = [
1339
1442
  "src",
1340
1443
  "alt",
1341
1444
  "onClick"
1342
1445
  ];
1343
- var _hoisted_20 = [
1446
+ var _hoisted_21 = [
1344
1447
  "title",
1345
1448
  "aria-label",
1346
1449
  "data-testid",
1347
1450
  "onClick"
1348
1451
  ];
1349
- var _hoisted_21 = {
1452
+ var _hoisted_22 = {
1350
1453
  key: 0,
1351
1454
  class: "animate-spin w-5 h-5",
1352
1455
  viewBox: "0 0 24 24",
1353
1456
  fill: "none"
1354
1457
  };
1355
- var _hoisted_22 = {
1458
+ var _hoisted_23 = {
1356
1459
  key: 1,
1357
1460
  class: "material-icons text-3xl"
1358
1461
  };
1359
- var _hoisted_23 = ["disabled", "onClick"];
1360
- var _hoisted_24 = {
1462
+ var _hoisted_24 = ["disabled", "onClick"];
1463
+ var _hoisted_25 = {
1361
1464
  key: 3,
1362
1465
  class: "w-full aspect-video flex flex-col items-center justify-center gap-1 p-2"
1363
1466
  };
1364
- var _hoisted_25 = { class: "text-xs text-green-500" };
1365
- var _hoisted_26 = {
1467
+ var _hoisted_26 = { class: "text-xs text-green-500" };
1468
+ var _hoisted_27 = {
1366
1469
  key: 1,
1367
1470
  class: "text-xs text-red-400 text-center"
1368
1471
  };
1369
- var _hoisted_27 = {
1472
+ var _hoisted_28 = {
1370
1473
  key: 0,
1371
1474
  class: "text-xs text-gray-400 text-center italic leading-relaxed px-1"
1372
1475
  };
1373
- var _hoisted_28 = {
1476
+ var _hoisted_29 = {
1374
1477
  key: 1,
1375
1478
  class: "text-xs text-gray-300"
1376
1479
  };
1377
- var _hoisted_29 = {
1480
+ var _hoisted_30 = {
1378
1481
  key: 2,
1379
1482
  class: "absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none"
1380
1483
  };
1381
- var _hoisted_30 = { class: "text-xs text-blue-500 font-medium" };
1382
- var _hoisted_31 = {
1484
+ var _hoisted_31 = { class: "text-xs text-blue-500 font-medium" };
1485
+ var _hoisted_32 = {
1383
1486
  key: 3,
1384
1487
  class: "absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
1385
1488
  };
1386
- var _hoisted_32 = ["onClick"];
1387
- var _hoisted_33 = { class: "flex flex-col flex-1 min-w-0 px-2 py-1.5" };
1388
- var _hoisted_34 = { class: "text-sm text-gray-800 leading-relaxed" };
1389
- var _hoisted_35 = { class: "flex justify-between mt-auto pt-1" };
1390
- var _hoisted_36 = { class: "flex items-center gap-1" };
1391
- var _hoisted_37 = {
1489
+ var _hoisted_33 = ["onClick"];
1490
+ var _hoisted_34 = { class: "flex flex-col flex-1 min-w-0 px-2 py-1.5" };
1491
+ var _hoisted_35 = { class: "text-sm text-gray-800 leading-relaxed" };
1492
+ var _hoisted_36 = { class: "flex justify-between mt-auto pt-1" };
1493
+ var _hoisted_37 = { class: "flex items-center gap-1" };
1494
+ var _hoisted_38 = {
1392
1495
  key: 0,
1393
1496
  class: "animate-spin w-3 h-3 text-green-400",
1394
1497
  viewBox: "0 0 24 24",
1395
1498
  fill: "none"
1396
1499
  };
1397
- var _hoisted_38 = ["onClick"];
1398
- var _hoisted_39 = ["title"];
1399
- var _hoisted_40 = ["disabled", "onClick"];
1400
- var _hoisted_41 = ["onClick"];
1401
- var _hoisted_42 = [
1500
+ var _hoisted_39 = ["onClick"];
1501
+ var _hoisted_40 = ["title"];
1502
+ var _hoisted_41 = ["disabled", "onClick"];
1503
+ var _hoisted_42 = ["onClick"];
1504
+ var _hoisted_43 = [
1402
1505
  "title",
1403
1506
  "data-testid",
1404
1507
  "onClick"
1405
1508
  ];
1406
- var _hoisted_43 = {
1509
+ var _hoisted_44 = {
1407
1510
  key: 0,
1408
1511
  class: "border-t border-gray-100"
1409
1512
  };
1410
- var _hoisted_44 = ["onUpdate:modelValue", "data-testid"];
1411
- var _hoisted_45 = { class: "flex items-center justify-end gap-2 px-2 pb-2" };
1412
- var _hoisted_46 = {
1513
+ var _hoisted_45 = ["onUpdate:modelValue", "data-testid"];
1514
+ var _hoisted_46 = { class: "flex items-center justify-end gap-2 px-2 pb-2" };
1515
+ var _hoisted_47 = {
1413
1516
  key: 0,
1414
1517
  class: "text-xs text-red-600",
1415
1518
  role: "alert"
1416
1519
  };
1417
- var _hoisted_47 = [
1520
+ var _hoisted_48 = [
1418
1521
  "disabled",
1419
1522
  "data-testid",
1420
1523
  "onClick"
1421
1524
  ];
1422
- var _hoisted_48 = {
1525
+ var _hoisted_49 = {
1423
1526
  key: 0,
1424
1527
  class: "flex items-center justify-center h-32 text-gray-400 text-sm"
1425
1528
  };
1426
- var _hoisted_49 = { class: "bottom-bar-wrapper" };
1427
- var _hoisted_50 = { class: "editor-actions" };
1428
- var _hoisted_51 = ["disabled"];
1429
- var _hoisted_52 = ["title"];
1430
- var _hoisted_53 = { class: "material-icons" };
1529
+ var _hoisted_50 = { class: "bottom-bar-wrapper" };
1530
+ var _hoisted_51 = { class: "editor-actions" };
1531
+ var _hoisted_52 = ["disabled"];
1532
+ var _hoisted_53 = ["title"];
1533
+ var _hoisted_54 = { class: "material-icons" };
1431
1534
  var SILENT_BEAT_DEFAULT_SEC = 3;
1432
1535
  var MS_PER_SECOND = 1e3;
1536
+ var BEAT_TAB_BASE = "rounded px-2 py-0.5 font-sans";
1433
1537
  var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
1434
1538
  __name: "View",
1435
1539
  props: { selectedResult: {} },
@@ -1596,12 +1700,27 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1596
1700
  }
1597
1701
  });
1598
1702
  }
1599
- const { isDeck, deckScriptInput, onDeckUpdate, flushPendingDeckSave, watchForeignWrites } = useDeckEditor({
1703
+ const { canEditBeats, deckScriptInput, onDeckUpdate, flushPendingDeckSave, watchForeignWrites } = useDeckEditor({
1600
1704
  api,
1601
1705
  filePath,
1602
1706
  effectiveScript,
1603
1707
  commitScript
1604
1708
  });
1709
+ /**
1710
+ * Which pane the beats are shown in.
1711
+ *
1712
+ * `edit` is the beat editor — every beat type, edited in place. `media` is the per-beat list,
1713
+ * which is the only place audio / image / movie generation lives. A script with nothing to edit
1714
+ * has only the list, so the switch is hidden and this is ignored.
1715
+ *
1716
+ * `media` is the default because opening the script is what triggers rendering each beat's
1717
+ * image: the auto-render on mount lives in that list, so defaulting to `edit` silently stopped
1718
+ * thumbnails from being produced at all. Someone who wants to edit clicks once; nobody has to
1719
+ * click to get the previews they always got.
1720
+ */
1721
+ const beatPane = ref("media");
1722
+ const showBeatEditor = computed(() => canEditBeats.value && beatPane.value === "edit");
1723
+ const beatPaneTabClass = (active) => [BEAT_TAB_BASE, active ? "bg-gray-700 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"];
1605
1724
  const unsubscribeForeignWrites = watchForeignWrites(() => {
1606
1725
  refreshScriptFromDisk();
1607
1726
  });
@@ -1976,14 +2095,18 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1976
2095
  }
1977
2096
  onMounted(initializeScript);
1978
2097
  watch(() => props.selectedResult, initializeScript);
1979
- const unsubscribeGenerationEvents = api.onGenerationEvent(() => filePath.value, (event) => {
1980
- if (!event.done) {
1981
- reflectGenerationStart(event);
1982
- return;
2098
+ const unsubscribeGenerationEvents = api.onGenerationEvent({
2099
+ filePath: () => filePath.value,
2100
+ root: () => void 0,
2101
+ handler: (event) => {
2102
+ if (!event.done) {
2103
+ reflectGenerationStart(event);
2104
+ return;
2105
+ }
2106
+ reflectGenerationFinish(event).catch((err) => {
2107
+ console.error("[presentMulmoScript] reload on finish failed:", err);
2108
+ });
1983
2109
  }
1984
- reflectGenerationFinish(event).catch((err) => {
1985
- console.error("[presentMulmoScript] reload on finish failed:", err);
1986
- });
1987
2110
  });
1988
2111
  function reflectGenerationStart(entry) {
1989
2112
  if (entry.kind === "beatImage") {
@@ -2058,7 +2181,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2058
2181
  "onDownloadPdf"
2059
2182
  ])]),
2060
2183
  unref(movieError) ? (openBlock(), createElementBlock("div", _hoisted_9, [
2061
- _cache[3] || (_cache[3] = createElementVNode("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
2184
+ _cache[5] || (_cache[5] = createElementVNode("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
2062
2185
  createElementVNode("div", _hoisted_10, [createElementVNode("div", _hoisted_11, toDisplayString(unref(m).movieGenerationFailed), 1), createElementVNode("div", _hoisted_12, toDisplayString(unref(movieError)), 1)]),
2063
2186
  createElementVNode("button", {
2064
2187
  class: "shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50",
@@ -2098,8 +2221,19 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2098
2221
  "onCharDrop",
2099
2222
  "onRenderCharacter"
2100
2223
  ])) : createCommentVNode("", true),
2101
- unref(isDeck) ? (openBlock(), createElementBlock("div", {
2102
- key: 2,
2224
+ unref(canEditBeats) ? (openBlock(), createElementBlock("div", _hoisted_14, [createElementVNode("button", {
2225
+ type: "button",
2226
+ class: normalizeClass(beatPaneTabClass(beatPane.value === "edit")),
2227
+ "data-testid": "mulmo-script-tab-edit",
2228
+ onClick: _cache[1] || (_cache[1] = ($event) => beatPane.value = "edit")
2229
+ }, toDisplayString(unref(m).editTab), 3), createElementVNode("button", {
2230
+ type: "button",
2231
+ class: normalizeClass(beatPaneTabClass(beatPane.value === "media")),
2232
+ "data-testid": "mulmo-script-tab-media",
2233
+ onClick: _cache[2] || (_cache[2] = ($event) => beatPane.value = "media")
2234
+ }, toDisplayString(unref(m).mediaTab), 3)])) : createCommentVNode("", true),
2235
+ showBeatEditor.value ? (openBlock(), createElementBlock("div", {
2236
+ key: 3,
2103
2237
  class: "flex-1 overflow-hidden",
2104
2238
  "data-testid": "mulmo-script-deck-editor",
2105
2239
  onFocusout: onDeckFocusOut
@@ -2107,7 +2241,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2107
2241
  beats: deckBeats.value,
2108
2242
  "onUpdate:beats": onDeckBeatsUpdate
2109
2243
  }, null, 8, ["beats"])], 32)) : (openBlock(), createElementBlock("div", {
2110
- key: 3,
2244
+ key: 4,
2111
2245
  ref_key: "beatListEl",
2112
2246
  ref: beatListEl,
2113
2247
  class: "flex-1 overflow-y-auto p-2 space-y-1.5"
@@ -2115,7 +2249,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2115
2249
  return openBlock(), createElementBlock("div", {
2116
2250
  key: index,
2117
2251
  class: "rounded-lg border border-gray-200 overflow-hidden"
2118
- }, [createElementVNode("div", _hoisted_14, [createElementVNode("div", {
2252
+ }, [createElementVNode("div", _hoisted_15, [createElementVNode("div", {
2119
2253
  class: normalizeClass(["relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors", beatDragOver[index] ? "bg-blue-50" : ""]),
2120
2254
  onDragover: ($event) => onBeatDragOver($event, index),
2121
2255
  onDragleave: ($event) => onBeatDragLeave(index),
@@ -2124,27 +2258,27 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2124
2258
  createElementVNode("div", {
2125
2259
  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",
2126
2260
  "data-testid": `mulmo-script-beat-number-${index}`
2127
- }, toDisplayString(index + 1), 9, _hoisted_16),
2261
+ }, toDisplayString(index + 1), 9, _hoisted_17),
2128
2262
  unref(beatMovieOpen)[index] && unref(beatMovieUrls)[index] ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("video", {
2129
2263
  src: unref(beatMovieUrls)[index],
2130
2264
  class: "w-full object-contain",
2131
2265
  controls: "",
2132
2266
  autoplay: "",
2133
2267
  "data-testid": `mulmo-script-beat-movie-player-${index}`
2134
- }, null, 8, _hoisted_17), createElementVNode("button", {
2268
+ }, null, 8, _hoisted_18), createElementVNode("button", {
2135
2269
  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",
2136
2270
  title: unref(m).close,
2137
2271
  "aria-label": unref(m).close,
2138
2272
  "data-testid": `mulmo-script-beat-movie-close-${index}`,
2139
2273
  onClick: withModifiers(($event) => unref(closeBeatMovie)(index), ["stop"])
2140
- }, [..._cache[4] || (_cache[4] = [createElementVNode("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_18)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2274
+ }, [..._cache[6] || (_cache[6] = [createElementVNode("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_19)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2141
2275
  renderedImages[index] ? (openBlock(), createElementBlock("img", {
2142
2276
  key: 0,
2143
2277
  src: renderedImages[index],
2144
2278
  class: "w-full object-contain cursor-zoom-in",
2145
2279
  alt: `Beat ${index + 1}`,
2146
2280
  onClick: ($event) => openLightbox(index)
2147
- }, null, 8, _hoisted_19)) : createCommentVNode("", true),
2281
+ }, null, 8, _hoisted_20)) : createCommentVNode("", true),
2148
2282
  renderedImages[index] && unref(beatMovies)[index] && canFetchMedia.value ? (openBlock(), createElementBlock("button", {
2149
2283
  key: 1,
2150
2284
  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",
@@ -2152,7 +2286,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2152
2286
  "aria-label": unref(m).play,
2153
2287
  "data-testid": `mulmo-script-beat-movie-play-${index}`,
2154
2288
  onClick: withModifiers(($event) => unref(playBeatMovie)(index), ["stop"])
2155
- }, [unref(beatMovieLoading)[index] ? (openBlock(), createElementBlock("svg", _hoisted_21, [..._cache[5] || (_cache[5] = [createElementVNode("circle", {
2289
+ }, [unref(beatMovieLoading)[index] ? (openBlock(), createElementBlock("svg", _hoisted_22, [..._cache[7] || (_cache[7] = [createElementVNode("circle", {
2156
2290
  class: "opacity-25",
2157
2291
  cx: "12",
2158
2292
  cy: "12",
@@ -2163,13 +2297,13 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2163
2297
  class: "opacity-75",
2164
2298
  fill: "currentColor",
2165
2299
  d: "M4 12a8 8 0 018-8v8H4z"
2166
- }, null, -1)])])) : (openBlock(), createElementBlock("span", _hoisted_22, "play_arrow"))], 8, _hoisted_20)) : createCommentVNode("", true),
2300
+ }, null, -1)])])) : (openBlock(), createElementBlock("span", _hoisted_23, "play_arrow"))], 8, _hoisted_21)) : createCommentVNode("", true),
2167
2301
  renderedImages[index] && renderState[index] !== "rendering" ? (openBlock(), createElementBlock("button", {
2168
2302
  key: 2,
2169
2303
  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",
2170
2304
  disabled: unref(movieGenerating),
2171
2305
  onClick: withModifiers(($event) => regenerateBeat(index), ["stop"])
2172
- }, " ↺ ", 8, _hoisted_23)) : !renderedImages[index] ? (openBlock(), createElementBlock("div", _hoisted_24, [renderState[index] === "rendering" || unref(movieGenerating) && !renderedImages[index] && effectiveBeat$1(index).imagePrompt ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[6] || (_cache[6] = createElementVNode("svg", {
2306
+ }, " ↺ ", 8, _hoisted_24)) : !renderedImages[index] ? (openBlock(), createElementBlock("div", _hoisted_25, [renderState[index] === "rendering" || unref(movieGenerating) && !renderedImages[index] && effectiveBeat$1(index).imagePrompt ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[8] || (_cache[8] = createElementVNode("svg", {
2173
2307
  class: "animate-spin w-4 h-4 text-green-400",
2174
2308
  viewBox: "0 0 24 24",
2175
2309
  fill: "none"
@@ -2184,15 +2318,15 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2184
2318
  class: "opacity-75",
2185
2319
  fill: "currentColor",
2186
2320
  d: "M4 12a8 8 0 018-8v8H4z"
2187
- })], -1)), createElementVNode("span", _hoisted_25, toDisplayString(unref(m).rendering), 1)], 64)) : renderState[index] === "error" ? (openBlock(), createElementBlock("span", _hoisted_26, toDisplayString(renderErrors[index]), 1)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [effectiveBeat$1(index).imagePrompt ? (openBlock(), createElementBlock("span", _hoisted_27, toDisplayString(effectiveBeat$1(index).imagePrompt), 1)) : (openBlock(), createElementBlock("span", _hoisted_28, toDisplayString(beat.image?.type ?? "—"), 1))], 64))])) : createCommentVNode("", true)
2321
+ })], -1)), createElementVNode("span", _hoisted_26, toDisplayString(unref(m).rendering), 1)], 64)) : renderState[index] === "error" ? (openBlock(), createElementBlock("span", _hoisted_27, toDisplayString(renderErrors[index]), 1)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [effectiveBeat$1(index).imagePrompt ? (openBlock(), createElementBlock("span", _hoisted_28, toDisplayString(effectiveBeat$1(index).imagePrompt), 1)) : (openBlock(), createElementBlock("span", _hoisted_29, toDisplayString(beat.image?.type ?? "—"), 1))], 64))])) : createCommentVNode("", true)
2188
2322
  ], 64)),
2189
- beatDragOver[index] ? (openBlock(), createElementBlock("div", _hoisted_29, [createElementVNode("span", _hoisted_30, toDisplayString(unref(m).drop), 1)])) : !renderedImages[index] && renderState[index] !== "rendering" ? (openBlock(), createElementBlock("div", _hoisted_31, toDisplayString(unref(m).orDropImage), 1)) : createCommentVNode("", true),
2323
+ beatDragOver[index] ? (openBlock(), createElementBlock("div", _hoisted_30, [createElementVNode("span", _hoisted_31, toDisplayString(unref(m).drop), 1)])) : !renderedImages[index] && renderState[index] !== "rendering" ? (openBlock(), createElementBlock("div", _hoisted_32, toDisplayString(unref(m).orDropImage), 1)) : createCommentVNode("", true),
2190
2324
  !renderedImages[index] && renderState[index] !== "rendering" && !unref(movieGenerating) && !unref(isBeatImageReference)(effectiveBeat$1(index)) ? (openBlock(), createElementBlock("button", {
2191
2325
  key: 4,
2192
2326
  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",
2193
2327
  onClick: ($event) => renderBeat(index)
2194
- }, toDisplayString(unref(m).generate), 9, _hoisted_32)) : createCommentVNode("", true)
2195
- ], 42, _hoisted_15), createElementVNode("div", _hoisted_33, [createElementVNode("span", _hoisted_34, toDisplayString(effectiveBeat$1(index).text), 1), createElementVNode("div", _hoisted_35, [createElementVNode("div", _hoisted_36, [audioState[index] === "generating" || unref(movieGenerating) && !beatAudios[index] && effectiveBeat$1(index).text ? (openBlock(), createElementBlock("svg", _hoisted_37, [..._cache[7] || (_cache[7] = [createElementVNode("circle", {
2328
+ }, toDisplayString(unref(m).generate), 9, _hoisted_33)) : createCommentVNode("", true)
2329
+ ], 42, _hoisted_16), createElementVNode("div", _hoisted_34, [createElementVNode("span", _hoisted_35, toDisplayString(effectiveBeat$1(index).text), 1), createElementVNode("div", _hoisted_36, [createElementVNode("div", _hoisted_37, [audioState[index] === "generating" || unref(movieGenerating) && !beatAudios[index] && effectiveBeat$1(index).text ? (openBlock(), createElementBlock("svg", _hoisted_38, [..._cache[9] || (_cache[9] = [createElementVNode("circle", {
2196
2330
  class: "opacity-25",
2197
2331
  cx: "12",
2198
2332
  cy: "12",
@@ -2207,24 +2341,24 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2207
2341
  key: 1,
2208
2342
  class: 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"]),
2209
2343
  onClick: ($event) => playAudio(index)
2210
- }, toDisplayString(playingAudio.value?.index === index ? unref(m).stop : unref(m).play), 11, _hoisted_38)) : audioErrors[index] ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [createElementVNode("span", {
2344
+ }, toDisplayString(playingAudio.value?.index === index ? unref(m).stop : unref(m).play), 11, _hoisted_39)) : audioErrors[index] ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [createElementVNode("span", {
2211
2345
  class: "text-xs text-red-400 truncate min-w-0 max-w-[20rem]",
2212
2346
  title: audioErrors[index]
2213
- }, toDisplayString(unref(m).errPrefix) + " " + toDisplayString(audioErrors[index]), 9, _hoisted_39), effectiveBeat$1(index).text ? (openBlock(), createElementBlock("button", {
2347
+ }, toDisplayString(unref(m).errPrefix) + " " + toDisplayString(audioErrors[index]), 9, _hoisted_40), effectiveBeat$1(index).text ? (openBlock(), createElementBlock("button", {
2214
2348
  key: 0,
2215
2349
  class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
2216
2350
  disabled: unref(movieGenerating),
2217
2351
  onClick: ($event) => generateAudio(index)
2218
- }, " ↺ ", 8, _hoisted_40)) : createCommentVNode("", true)], 64)) : effectiveBeat$1(index).text ? (openBlock(), createElementBlock("button", {
2352
+ }, " ↺ ", 8, _hoisted_41)) : createCommentVNode("", true)], 64)) : effectiveBeat$1(index).text ? (openBlock(), createElementBlock("button", {
2219
2353
  key: 3,
2220
2354
  class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50",
2221
2355
  onClick: ($event) => generateAudio(index)
2222
- }, toDisplayString(unref(m).generateAudio), 9, _hoisted_41)) : createCommentVNode("", true)]), createElementVNode("button", {
2356
+ }, toDisplayString(unref(m).generateAudio), 9, _hoisted_42)) : createCommentVNode("", true)]), createElementVNode("button", {
2223
2357
  class: "text-gray-400 hover:text-gray-600",
2224
2358
  title: sourceOpen[index] ? "Hide source" : "Show source",
2225
2359
  "data-testid": `mulmo-script-beat-source-toggle-${index}`,
2226
2360
  onClick: ($event) => toggleSource(index)
2227
- }, [..._cache[8] || (_cache[8] = [createElementVNode("svg", {
2361
+ }, [..._cache[10] || (_cache[10] = [createElementVNode("svg", {
2228
2362
  xmlns: "http://www.w3.org/2000/svg",
2229
2363
  class: "w-3.5 h-3.5",
2230
2364
  viewBox: "0 0 24 24",
@@ -2233,36 +2367,36 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2233
2367
  "stroke-width": "2",
2234
2368
  "stroke-linecap": "round",
2235
2369
  "stroke-linejoin": "round"
2236
- }, [createElementVNode("polyline", { points: "16 18 22 12 16 6" }), createElementVNode("polyline", { points: "8 6 2 12 8 18" })], -1)])], 8, _hoisted_42)])])]), sourceOpen[index] ? (openBlock(), createElementBlock("div", _hoisted_43, [withDirectives(createElementVNode("textarea", {
2370
+ }, [createElementVNode("polyline", { points: "16 18 22 12 16 6" }), createElementVNode("polyline", { points: "8 6 2 12 8 18" })], -1)])], 8, _hoisted_43)])])]), sourceOpen[index] ? (openBlock(), createElementBlock("div", _hoisted_44, [withDirectives(createElementVNode("textarea", {
2237
2371
  "onUpdate:modelValue": ($event) => sourceText[index] = $event,
2238
2372
  class: 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"]),
2239
2373
  rows: "8",
2240
2374
  spellcheck: "false",
2241
2375
  "data-testid": `mulmo-script-beat-source-textarea-${index}`
2242
- }, null, 10, _hoisted_44), [[vModelText, sourceText[index]]]), createElementVNode("div", _hoisted_45, [beatSaveErrors[index] ? (openBlock(), createElementBlock("span", _hoisted_46, toDisplayString(beatSaveErrors[index].kind === "invalidJson" ? unref(m).saveErrorInvalidJson(beatSaveErrors[index].error) : unref(m).saveErrorSaveFailed(beatSaveErrors[index].error)), 1)) : createCommentVNode("", true), createElementVNode("button", {
2376
+ }, null, 10, _hoisted_45), [[vModelText, sourceText[index]]]), createElementVNode("div", _hoisted_46, [beatSaveErrors[index] ? (openBlock(), createElementBlock("span", _hoisted_47, toDisplayString(beatSaveErrors[index].kind === "invalidJson" ? unref(m).saveErrorInvalidJson(beatSaveErrors[index].error) : unref(m).saveErrorSaveFailed(beatSaveErrors[index].error)), 1)) : createCommentVNode("", true), createElementVNode("button", {
2243
2377
  class: 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"]),
2244
2378
  disabled: !isValidBeat$1(index) || !!beatSaving[index],
2245
2379
  "data-testid": `mulmo-script-beat-update-button-${index}`,
2246
2380
  onClick: ($event) => updateBeat(index)
2247
- }, toDisplayString(beatSaving[index] ? unref(m).saving : unref(m).update), 11, _hoisted_47)])])) : createCommentVNode("", true)]);
2248
- }), 128)), beats.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_48, toDisplayString(unref(m).noBeats), 1)) : createCommentVNode("", true)], 512)),
2249
- createElementVNode("div", _hoisted_49, [createElementVNode("details", {
2381
+ }, toDisplayString(beatSaving[index] ? unref(m).saving : unref(m).update), 11, _hoisted_48)])])) : createCommentVNode("", true)]);
2382
+ }), 128)), beats.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_49, toDisplayString(unref(m).noBeats), 1)) : createCommentVNode("", true)], 512)),
2383
+ createElementVNode("div", _hoisted_50, [createElementVNode("details", {
2250
2384
  ref_key: "sourceDetails",
2251
2385
  ref: sourceDetails,
2252
2386
  class: "script-source",
2253
- onToggle: _cache[2] || (_cache[2] = ($event) => onSourceToggle($event.target.open))
2387
+ onToggle: _cache[4] || (_cache[4] = ($event) => onSourceToggle($event.target.open))
2254
2388
  }, [
2255
2389
  createElementVNode("summary", null, toDisplayString(unref(m).editSource), 1),
2256
2390
  withDirectives(createElementVNode("textarea", {
2257
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => editableSource.value = $event),
2391
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => editableSource.value = $event),
2258
2392
  class: normalizeClass(["script-editor", { "script-editor-invalid": sourceChanged.value && !sourceValid.value }]),
2259
2393
  spellcheck: "false"
2260
2394
  }, null, 2), [[vModelText, editableSource.value]]),
2261
- createElementVNode("div", _hoisted_50, [createElementVNode("button", {
2395
+ createElementVNode("div", _hoisted_51, [createElementVNode("button", {
2262
2396
  class: "apply-btn",
2263
2397
  disabled: !sourceChanged.value || !sourceValid.value,
2264
2398
  onClick: applySource
2265
- }, toDisplayString(unref(m).applyChanges), 9, _hoisted_51), createElementVNode("button", {
2399
+ }, toDisplayString(unref(m).applyChanges), 9, _hoisted_52), createElementVNode("button", {
2266
2400
  class: "cancel-btn",
2267
2401
  onClick: cancelSourceEdit
2268
2402
  }, toDisplayString(unref(m).cancel), 1)])
@@ -2270,9 +2404,9 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2270
2404
  class: "copy-btn",
2271
2405
  title: unref(copied) ? "Copied!" : "Copy",
2272
2406
  onClick: copyText
2273
- }, [createElementVNode("span", _hoisted_53, toDisplayString(unref(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_52), [[vShow, !editing.value]])]),
2407
+ }, [createElementVNode("span", _hoisted_54, toDisplayString(unref(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_53), [[vShow, !editing.value]])]),
2274
2408
  lightbox.value ? (openBlock(), createBlock(BeatLightbox_default, {
2275
- key: 4,
2409
+ key: 5,
2276
2410
  lightbox: lightbox.value,
2277
2411
  "beat-count": beats.value.length,
2278
2412
  "beat-texts": beatTexts.value,
@@ -2308,7 +2442,7 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
2308
2442
  };
2309
2443
  //#endregion
2310
2444
  //#region src/vue/View.vue
2311
- var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-d78be72d"]]);
2445
+ var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-2d5f76ac"]]);
2312
2446
  //#endregion
2313
2447
  //#region src/vue/Preview.vue?vue&type=script&setup=true&lang.ts
2314
2448
  var _hoisted_1 = {