@signalsandsorcery/plugin-sdk 2.35.3 → 2.35.5

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.
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ __export(index_exports, {
54
54
  FxToggleBar: () => FxToggleBar,
55
55
  GUTTER_W: () => GUTTER_W,
56
56
  GeneratorPanelShell: () => GeneratorPanelShell,
57
+ GroupFadeTrackRow: () => GroupFadeTrackRow,
57
58
  ImportTrackModal: () => ImportTrackModal,
58
59
  InstrumentDrawer: () => TrackDrawer,
59
60
  LevelMeter: () => LevelMeter,
@@ -122,6 +123,7 @@ __export(index_exports, {
122
123
  sliderToDb: () => sliderToDb,
123
124
  slotsEqual: () => slotsEqual,
124
125
  soundIdentity: () => soundIdentity,
126
+ splitFadeEntries: () => splitFadeEntries,
125
127
  synthesizeCuePoints: () => synthesizeCuePoints,
126
128
  tokenizePrompt: () => tokenizePrompt,
127
129
  trackDataKey: () => trackDataKey,
@@ -963,12 +965,12 @@ function useTrackExternalFx(host, trackId) {
963
965
  void reload();
964
966
  }, [reload]);
965
967
  const loadFxList = (0, import_react2.useCallback)(
966
- async (force) => {
968
+ async (opts) => {
967
969
  if (!supported || !host.getAvailableFx) return;
968
- if (fxLoadedRef.current && !force) return;
970
+ if (fxLoadedRef.current && !opts.force && !opts.rescan) return;
969
971
  setFxLoading(true);
970
972
  try {
971
- const list = await host.getAvailableFx();
973
+ const list = opts.rescan && host.rescanAvailableFx ? await host.rescanAvailableFx() : await host.getAvailableFx();
972
974
  setAvailableFx(list);
973
975
  fxLoadedRef.current = true;
974
976
  } catch {
@@ -981,7 +983,7 @@ function useTrackExternalFx(host, trackId) {
981
983
  const openPicker = (0, import_react2.useCallback)(
982
984
  (open) => {
983
985
  setPickerOpen(open);
984
- if (open) void loadFxList(false);
986
+ if (open) void loadFxList({});
985
987
  },
986
988
  [loadFxList]
987
989
  );
@@ -1005,7 +1007,7 @@ function useTrackExternalFx(host, trackId) {
1005
1007
  fxLoading,
1006
1008
  pickerOpen,
1007
1009
  setPickerOpen: openPicker,
1008
- refreshFx: () => void loadFxList(true),
1010
+ refreshFx: () => void loadFxList({ rescan: true }),
1009
1011
  reload,
1010
1012
  onAddFx: (pluginId) => mutate(host.loadTrackExternalFx && (async () => {
1011
1013
  await host.loadTrackExternalFx(trackId, pluginId);
@@ -1142,7 +1144,7 @@ function TrackExternalFxSection({
1142
1144
  onClick: () => refreshFx(),
1143
1145
  disabled: fxLoading,
1144
1146
  className: "px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-50",
1145
- title: "Re-scan plugins",
1147
+ title: "Re-scan plugins \u2014 picks up newly installed FX and retries any that failed a previous scan",
1146
1148
  children: fxLoading ? "..." : "Refresh"
1147
1149
  }
1148
1150
  )
@@ -2991,7 +2993,10 @@ function asFadeMeta(val) {
2991
2993
  sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
2992
2994
  sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
2993
2995
  soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
2994
- sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
2996
+ sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5,
2997
+ groupId: typeof m.groupId === "string" && m.groupId ? m.groupId : void 0,
2998
+ memberIndex: typeof m.memberIndex === "number" ? m.memberIndex : void 0,
2999
+ memberLabel: typeof m.memberLabel === "string" && m.memberLabel ? m.memberLabel : void 0
2995
3000
  };
2996
3001
  }
2997
3002
  function parseFades(sceneData) {
@@ -3005,6 +3010,33 @@ function parseFades(sceneData) {
3005
3010
  }
3006
3011
  return out;
3007
3012
  }
3013
+ function splitFadeEntries(entries) {
3014
+ const singles = [];
3015
+ const byGroup = /* @__PURE__ */ new Map();
3016
+ for (const entry of entries) {
3017
+ const gid = entry.meta.groupId;
3018
+ if (!gid) {
3019
+ singles.push(entry);
3020
+ continue;
3021
+ }
3022
+ const list = byGroup.get(gid) ?? [];
3023
+ list.push(entry);
3024
+ byGroup.set(gid, list);
3025
+ }
3026
+ const groups = [];
3027
+ for (const [groupId, members] of byGroup) {
3028
+ members.sort((a, b) => (a.meta.memberIndex ?? 0) - (b.meta.memberIndex ?? 0));
3029
+ const head = members[0].meta;
3030
+ groups.push({
3031
+ groupId,
3032
+ direction: head.direction,
3033
+ gesture: head.gesture,
3034
+ sliderPos: head.sliderPos,
3035
+ members
3036
+ });
3037
+ }
3038
+ return { singles, groups };
3039
+ }
3008
3040
  function buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture, steps = 32) {
3009
3041
  const durationSeconds = bars * 4 * 60 / Math.max(1, bpm);
3010
3042
  if (gesture === "build") {
@@ -3189,9 +3221,191 @@ function FadeTrackRow({
3189
3221
  );
3190
3222
  }
3191
3223
 
3192
- // src/components/FadeModal.tsx
3193
- var import_react14 = require("react");
3224
+ // src/components/GroupFadeTrackRow.tsx
3225
+ var import_react14 = __toESM(require("react"));
3194
3226
  var import_jsx_runtime15 = require("react/jsx-runtime");
3227
+ function MemberCaption({
3228
+ member,
3229
+ direction
3230
+ }) {
3231
+ const tag = direction === "in" ? "Fade in" : "Fade out";
3232
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
3233
+ member.memberLabel && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: member.memberLabel }),
3234
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-[11px] text-sas-text truncate", title: member.sourceName ?? member.name, children: member.sourceName ?? member.name }),
3235
+ member.soundLabel && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: member.soundLabel, children: [
3236
+ "\xB7 ",
3237
+ member.soundLabel
3238
+ ] }),
3239
+ !member.memberLabel && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: [
3240
+ "\xB7 ",
3241
+ tag
3242
+ ] })
3243
+ ] });
3244
+ }
3245
+ function GroupFadeTrackRow({
3246
+ groupLabel,
3247
+ direction,
3248
+ gesture,
3249
+ sliderPos = 0.5,
3250
+ members,
3251
+ onMemberMuteToggle,
3252
+ onMemberSoloToggle,
3253
+ onMemberVolumeChange,
3254
+ onMemberPanChange,
3255
+ onMuteAll,
3256
+ onSoloAll,
3257
+ onDelete,
3258
+ onSliderChange,
3259
+ levels,
3260
+ accentColor = "#9333EA"
3261
+ }) {
3262
+ const [confirmDelete, setConfirmDelete] = import_react14.default.useState(false);
3263
+ const allMuted = members.length > 0 && members.every((m) => m.runtimeState.muted);
3264
+ const allSolo = members.length > 0 && members.every((m) => m.runtimeState.solo);
3265
+ const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
3266
+ const leftLabel = direction === "in" ? "(silent)" : groupLabel;
3267
+ const rightLabel = direction === "in" ? groupLabel : "(silent)";
3268
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3269
+ "div",
3270
+ {
3271
+ "data-testid": "group-fade-track-row",
3272
+ className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
3273
+ style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
3274
+ children: [
3275
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60 gap-2", children: [
3276
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 min-w-0", children: [
3277
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3278
+ "span",
3279
+ {
3280
+ "data-testid": "group-fade-direction-badge",
3281
+ className: "text-[10px] font-bold uppercase tracking-wide flex-shrink-0",
3282
+ style: { color: accentColor },
3283
+ children: badge
3284
+ }
3285
+ ),
3286
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3287
+ "span",
3288
+ {
3289
+ "data-testid": "group-fade-label",
3290
+ className: "text-[11px] text-sas-text truncate",
3291
+ title: `${groupLabel} \xB7 ${gesture}`,
3292
+ children: groupLabel
3293
+ }
3294
+ )
3295
+ ] }),
3296
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-1 flex-shrink-0", children: [
3297
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3298
+ "button",
3299
+ {
3300
+ "data-testid": "group-fade-mute-button",
3301
+ onClick: onMuteAll,
3302
+ className: `px-1.5 py-0.5 text-[9px] font-bold rounded-sm border transition-colors ${allMuted ? "bg-sas-danger/20 border-sas-danger text-sas-danger" : "border-sas-border text-sas-muted hover:text-sas-text"}`,
3303
+ title: allMuted ? "Unmute all members" : "Mute all members",
3304
+ children: "M"
3305
+ }
3306
+ ),
3307
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3308
+ "button",
3309
+ {
3310
+ "data-testid": "group-fade-solo-button",
3311
+ onClick: onSoloAll,
3312
+ className: `px-1.5 py-0.5 text-[9px] font-bold rounded-sm border transition-colors ${allSolo ? "bg-sas-accent/20 border-sas-accent text-sas-accent" : "border-sas-border text-sas-muted hover:text-sas-text"}`,
3313
+ title: allSolo ? "Unsolo all members" : "Solo all members",
3314
+ children: "S"
3315
+ }
3316
+ ),
3317
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3318
+ "button",
3319
+ {
3320
+ "data-testid": "group-fade-delete-button",
3321
+ onClick: () => setConfirmDelete(true),
3322
+ className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
3323
+ title: "Delete group fade",
3324
+ "aria-label": "Delete group fade",
3325
+ children: "x"
3326
+ }
3327
+ )
3328
+ ] })
3329
+ ] }),
3330
+ members.map((member) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3331
+ TrackRow,
3332
+ {
3333
+ track: { id: member.trackId, name: "", role: member.role },
3334
+ runtimeState: member.runtimeState,
3335
+ fxDetailState: EMPTY_FX_DETAIL_STATE,
3336
+ drawerOpen: false,
3337
+ drawerTab: "fx",
3338
+ levels,
3339
+ accentColor,
3340
+ contentSlot: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(MemberCaption, { member, direction }),
3341
+ onMuteToggle: () => onMemberMuteToggle(member.trackId),
3342
+ onSoloToggle: () => onMemberSoloToggle(member.trackId),
3343
+ onVolumeChange: (vol) => onMemberVolumeChange(member.trackId, vol),
3344
+ onPanChange: (pan) => onMemberPanChange(member.trackId, pan)
3345
+ },
3346
+ member.trackId
3347
+ )),
3348
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "group-fade-slider-row", children: [
3349
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3350
+ "span",
3351
+ {
3352
+ className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
3353
+ title: leftLabel,
3354
+ children: leftLabel
3355
+ }
3356
+ ),
3357
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3358
+ "input",
3359
+ {
3360
+ type: "range",
3361
+ "data-testid": "group-fade-slider",
3362
+ min: 0,
3363
+ max: 1,
3364
+ step: 0.01,
3365
+ value: sliderPos,
3366
+ disabled: !onSliderChange,
3367
+ onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
3368
+ style: { accentColor },
3369
+ className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
3370
+ "aria-label": "Group fade position"
3371
+ }
3372
+ ),
3373
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3374
+ "span",
3375
+ {
3376
+ className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
3377
+ title: rightLabel,
3378
+ children: rightLabel
3379
+ }
3380
+ )
3381
+ ] }),
3382
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3383
+ ConfirmDialog,
3384
+ {
3385
+ open: confirmDelete,
3386
+ title: "Delete group fade?",
3387
+ message: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
3388
+ "All ",
3389
+ members.length,
3390
+ " member tracks of this fade will be permanently removed from this scene. This cannot be undone."
3391
+ ] }),
3392
+ confirmLabel: "Delete",
3393
+ onConfirm: () => {
3394
+ setConfirmDelete(false);
3395
+ onDelete();
3396
+ },
3397
+ onCancel: () => setConfirmDelete(false),
3398
+ testIdPrefix: "group-fade-delete-confirm"
3399
+ }
3400
+ )
3401
+ ]
3402
+ }
3403
+ );
3404
+ }
3405
+
3406
+ // src/components/FadeModal.tsx
3407
+ var import_react15 = require("react");
3408
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3195
3409
  function shortId(dbId) {
3196
3410
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3197
3411
  }
@@ -3234,7 +3448,7 @@ function OrphanRow({
3234
3448
  }) {
3235
3449
  const primary = track.prompt?.trim() || track.name;
3236
3450
  const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
3237
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3451
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3238
3452
  "button",
3239
3453
  {
3240
3454
  type: "button",
@@ -3246,8 +3460,8 @@ function OrphanRow({
3246
3460
  disabled,
3247
3461
  className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
3248
3462
  children: [
3249
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3250
- meta && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3463
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3464
+ meta && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3251
3465
  ]
3252
3466
  }
3253
3467
  );
@@ -3264,14 +3478,14 @@ function FadeModal({
3264
3478
  onCreate,
3265
3479
  testIdPrefix = "fade-modal"
3266
3480
  }) {
3267
- const [load, setLoad] = (0, import_react14.useState)({ status: "loading" });
3268
- const [selectedDbId, setSelectedDbId] = (0, import_react14.useState)("");
3269
- const [isCreating, setIsCreating] = (0, import_react14.useState)(false);
3270
- const [error, setError] = (0, import_react14.useState)(null);
3271
- const [fromName, setFromName] = (0, import_react14.useState)(null);
3272
- const [toName, setToName] = (0, import_react14.useState)(null);
3273
- const cancelRef = (0, import_react14.useRef)(null);
3274
- const refresh = (0, import_react14.useCallback)(async () => {
3481
+ const [load, setLoad] = (0, import_react15.useState)({ status: "loading" });
3482
+ const [selectedDbId, setSelectedDbId] = (0, import_react15.useState)("");
3483
+ const [isCreating, setIsCreating] = (0, import_react15.useState)(false);
3484
+ const [error, setError] = (0, import_react15.useState)(null);
3485
+ const [fromName, setFromName] = (0, import_react15.useState)(null);
3486
+ const [toName, setToName] = (0, import_react15.useState)(null);
3487
+ const cancelRef = (0, import_react15.useRef)(null);
3488
+ const refresh = (0, import_react15.useCallback)(async () => {
3275
3489
  if (!host.listSceneFamilyTracks) {
3276
3490
  setLoad({ status: "error", message: "This host does not support fades." });
3277
3491
  return;
@@ -3291,7 +3505,7 @@ function FadeModal({
3291
3505
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
3292
3506
  }
3293
3507
  }, [host, fromSceneId, toSceneId]);
3294
- (0, import_react14.useEffect)(() => {
3508
+ (0, import_react15.useEffect)(() => {
3295
3509
  if (open) {
3296
3510
  setError(null);
3297
3511
  setIsCreating(false);
@@ -3299,29 +3513,29 @@ function FadeModal({
3299
3513
  void refresh();
3300
3514
  }
3301
3515
  }, [open, refresh]);
3302
- const excludeSet = (0, import_react14.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3303
- const { fadeOut, fadeIn } = (0, import_react14.useMemo)(
3516
+ const excludeSet = (0, import_react15.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3517
+ const { fadeOut, fadeIn } = (0, import_react15.useMemo)(
3304
3518
  () => load.status === "ready" ? computeOrphans(load.from, load.to, excludeSet) : { fadeOut: [], fadeIn: [] },
3305
3519
  [load, excludeSet]
3306
3520
  );
3307
- const allOrphans = (0, import_react14.useMemo)(
3521
+ const allOrphans = (0, import_react15.useMemo)(
3308
3522
  () => [
3309
3523
  ...fadeOut.map((t) => ({ track: t, direction: "out" })),
3310
3524
  ...fadeIn.map((t) => ({ track: t, direction: "in" }))
3311
3525
  ],
3312
3526
  [fadeOut, fadeIn]
3313
3527
  );
3314
- (0, import_react14.useEffect)(() => {
3528
+ (0, import_react15.useEffect)(() => {
3315
3529
  if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {
3316
3530
  setSelectedDbId(allOrphans[0]?.track.dbId ?? "");
3317
3531
  }
3318
3532
  }, [allOrphans, selectedDbId]);
3319
3533
  const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;
3320
3534
  const canCreate = !isCreating && !!selected;
3321
- const handleClose = (0, import_react14.useCallback)(() => {
3535
+ const handleClose = (0, import_react15.useCallback)(() => {
3322
3536
  if (!isCreating) onClose();
3323
3537
  }, [isCreating, onClose]);
3324
- const handleCreate = (0, import_react14.useCallback)(async () => {
3538
+ const handleCreate = (0, import_react15.useCallback)(async () => {
3325
3539
  if (!selected) return;
3326
3540
  setIsCreating(true);
3327
3541
  setError(null);
@@ -3342,16 +3556,16 @@ function FadeModal({
3342
3556
  if (!open) return null;
3343
3557
  const renderSection = (heading, list, section) => {
3344
3558
  if (list.length === 0) return null;
3345
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "block", children: [
3346
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
3347
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3559
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "block", children: [
3560
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
3561
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3348
3562
  "div",
3349
3563
  {
3350
3564
  role: "radiogroup",
3351
3565
  "aria-label": heading,
3352
3566
  "data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
3353
3567
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3354
- children: list.map((t) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3568
+ children: list.map((t) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3355
3569
  OrphanRow,
3356
3570
  {
3357
3571
  track: t,
@@ -3367,32 +3581,32 @@ function FadeModal({
3367
3581
  )
3368
3582
  ] });
3369
3583
  };
3370
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3584
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3371
3585
  "div",
3372
3586
  {
3373
3587
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3374
3588
  onClick: (e) => e.stopPropagation(),
3375
3589
  "data-testid": `${testIdPrefix}-box`,
3376
3590
  children: [
3377
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
3378
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3591
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
3592
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3379
3593
  "Tracks with no counterpart between",
3380
3594
  " ",
3381
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3595
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3382
3596
  " and",
3383
3597
  " ",
3384
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3598
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3385
3599
  " can gracefully fade out (leaving) or fade in (entering) across this transition."
3386
3600
  ] }),
3387
- load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3388
- load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3389
- load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", "data-testid": `${testIdPrefix}-empty`, children: "Every track has a counterpart in the other scene \u2014 nothing to fade. Use \u201C+ Crossfade\u201D to bridge matching tracks." }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
3601
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3602
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3603
+ load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", "data-testid": `${testIdPrefix}-empty`, children: "Every track has a counterpart in the other scene \u2014 nothing to fade. Use \u201C+ Crossfade\u201D to bridge matching tracks." }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
3390
3604
  renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
3391
3605
  renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
3392
3606
  ] })),
3393
- error && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3394
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
3395
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3607
+ error && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3608
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
3609
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3396
3610
  "button",
3397
3611
  {
3398
3612
  ref: cancelRef,
@@ -3403,7 +3617,7 @@ function FadeModal({
3403
3617
  children: "Cancel"
3404
3618
  }
3405
3619
  ),
3406
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3620
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3407
3621
  "button",
3408
3622
  {
3409
3623
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3420,8 +3634,8 @@ function FadeModal({
3420
3634
  }
3421
3635
 
3422
3636
  // src/components/ImportTrackModal.tsx
3423
- var import_react15 = require("react");
3424
- var import_jsx_runtime16 = require("react/jsx-runtime");
3637
+ var import_react16 = require("react");
3638
+ var import_jsx_runtime17 = require("react/jsx-runtime");
3425
3639
  function ImportTrackModal({
3426
3640
  host,
3427
3641
  open,
@@ -3433,10 +3647,10 @@ function ImportTrackModal({
3433
3647
  onPick,
3434
3648
  onPortTrack
3435
3649
  }) {
3436
- const [load, setLoad] = (0, import_react15.useState)({ status: "loading" });
3437
- const [selectedSceneId, setSelectedSceneId] = (0, import_react15.useState)(null);
3438
- const [importingTrackId, setImportingTrackId] = (0, import_react15.useState)(null);
3439
- const refresh = (0, import_react15.useCallback)(async () => {
3650
+ const [load, setLoad] = (0, import_react16.useState)({ status: "loading" });
3651
+ const [selectedSceneId, setSelectedSceneId] = (0, import_react16.useState)(null);
3652
+ const [importingTrackId, setImportingTrackId] = (0, import_react16.useState)(null);
3653
+ const refresh = (0, import_react16.useCallback)(async () => {
3440
3654
  if (!host.listImportableTracks) {
3441
3655
  setLoad({ status: "error", message: "This host does not support importing tracks." });
3442
3656
  return;
@@ -3452,14 +3666,14 @@ function ImportTrackModal({
3452
3666
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
3453
3667
  }
3454
3668
  }, [host, mode, onPortTrack]);
3455
- (0, import_react15.useEffect)(() => {
3669
+ (0, import_react16.useEffect)(() => {
3456
3670
  if (open) {
3457
3671
  setSelectedSceneId(null);
3458
3672
  setImportingTrackId(null);
3459
3673
  void refresh();
3460
3674
  }
3461
3675
  }, [open, refresh]);
3462
- const handleImport = (0, import_react15.useCallback)(
3676
+ const handleImport = (0, import_react16.useCallback)(
3463
3677
  async (track, sourceSceneId, sceneName, isSameScene) => {
3464
3678
  if (isSameScene && onPortTrack) {
3465
3679
  if (!track.importable) return;
@@ -3500,16 +3714,16 @@ function ImportTrackModal({
3500
3714
  if (!open) return null;
3501
3715
  const scenes = load.status === "ready" ? load.scenes : [];
3502
3716
  const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
3503
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3717
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3504
3718
  "div",
3505
3719
  {
3506
3720
  className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
3507
3721
  onClick: (e) => e.stopPropagation(),
3508
3722
  "data-testid": `${testIdPrefix}-modal`,
3509
3723
  children: [
3510
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3511
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
3512
- selectedScene && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3724
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3725
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-2", children: [
3726
+ selectedScene && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3513
3727
  "button",
3514
3728
  {
3515
3729
  className: "text-sas-muted hover:text-sas-accent text-xs",
@@ -3518,9 +3732,9 @@ function ImportTrackModal({
3518
3732
  children: "\u2190"
3519
3733
  }
3520
3734
  ),
3521
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3735
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3522
3736
  ] }),
3523
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3737
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3524
3738
  "button",
3525
3739
  {
3526
3740
  className: "text-sas-muted hover:text-sas-accent text-sm",
@@ -3530,30 +3744,30 @@ function ImportTrackModal({
3530
3744
  }
3531
3745
  )
3532
3746
  ] }),
3533
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "overflow-y-auto p-2 flex-1", children: [
3534
- load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3535
- load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3536
- load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
3537
- load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3747
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "overflow-y-auto p-2 flex-1", children: [
3748
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3749
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3750
+ load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
3751
+ load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3538
3752
  "button",
3539
3753
  {
3540
3754
  className: "w-full flex items-center justify-between px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt text-left text-xs text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors",
3541
3755
  onClick: () => setSelectedSceneId(scene.sceneId),
3542
3756
  "data-testid": `${testIdPrefix}-scene`,
3543
3757
  children: [
3544
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate", children: scene.sceneName }),
3545
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-sas-muted", children: [
3758
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "truncate", children: scene.sceneName }),
3759
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-sas-muted", children: [
3546
3760
  scene.tracks.length,
3547
3761
  " \u2192"
3548
3762
  ] })
3549
3763
  ]
3550
3764
  }
3551
3765
  ) }, scene.sceneId)) }),
3552
- selectedScene && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3766
+ selectedScene && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3553
3767
  const busy = importingTrackId === track.trackId;
3554
3768
  const gated = mode === "track" && !track.importable;
3555
3769
  const disabled = gated || busy;
3556
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3770
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3557
3771
  "button",
3558
3772
  {
3559
3773
  className: `w-full flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${disabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-text hover:border-sas-accent hover:text-sas-accent"}`,
@@ -3563,14 +3777,14 @@ function ImportTrackModal({
3563
3777
  "data-testid": `${testIdPrefix}-track`,
3564
3778
  "data-importable": mode === "sound" || track.importable ? "true" : "false",
3565
3779
  children: [
3566
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "truncate", children: [
3780
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "truncate", children: [
3567
3781
  track.name,
3568
- track.role ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-sas-muted", children: [
3782
+ track.role ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-sas-muted", children: [
3569
3783
  " \xB7 ",
3570
3784
  track.role
3571
3785
  ] }) : null
3572
3786
  ] }),
3573
- busy ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-muted", children: "\u2298" }) : null
3787
+ busy ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-muted", children: "\u2298" }) : null
3574
3788
  ]
3575
3789
  }
3576
3790
  ) }, track.dbId);
@@ -3582,8 +3796,8 @@ function ImportTrackModal({
3582
3796
  }
3583
3797
 
3584
3798
  // src/components/CrossfadeModal.tsx
3585
- var import_react16 = require("react");
3586
- var import_jsx_runtime17 = require("react/jsx-runtime");
3799
+ var import_react17 = require("react");
3800
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3587
3801
  function shortId2(dbId) {
3588
3802
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3589
3803
  }
@@ -3596,7 +3810,7 @@ function CandidateRow({
3596
3810
  }) {
3597
3811
  const primary = track.prompt?.trim() || track.name;
3598
3812
  const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
3599
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3813
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3600
3814
  "button",
3601
3815
  {
3602
3816
  type: "button",
@@ -3608,8 +3822,8 @@ function CandidateRow({
3608
3822
  disabled,
3609
3823
  className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
3610
3824
  children: [
3611
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3612
- meta && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3825
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3826
+ meta && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3613
3827
  ]
3614
3828
  }
3615
3829
  );
@@ -3626,15 +3840,15 @@ function CrossfadeModal({
3626
3840
  onCreate,
3627
3841
  testIdPrefix = "crossfade-modal"
3628
3842
  }) {
3629
- const [load, setLoad] = (0, import_react16.useState)({ status: "loading" });
3630
- const [originDbId, setOriginDbId] = (0, import_react16.useState)("");
3631
- const [targetDbId, setTargetDbId] = (0, import_react16.useState)("");
3632
- const [isCreating, setIsCreating] = (0, import_react16.useState)(false);
3633
- const [error, setError] = (0, import_react16.useState)(null);
3634
- const [fromName, setFromName] = (0, import_react16.useState)(null);
3635
- const [toName, setToName] = (0, import_react16.useState)(null);
3636
- const cancelRef = (0, import_react16.useRef)(null);
3637
- const refresh = (0, import_react16.useCallback)(async () => {
3843
+ const [load, setLoad] = (0, import_react17.useState)({ status: "loading" });
3844
+ const [originDbId, setOriginDbId] = (0, import_react17.useState)("");
3845
+ const [targetDbId, setTargetDbId] = (0, import_react17.useState)("");
3846
+ const [isCreating, setIsCreating] = (0, import_react17.useState)(false);
3847
+ const [error, setError] = (0, import_react17.useState)(null);
3848
+ const [fromName, setFromName] = (0, import_react17.useState)(null);
3849
+ const [toName, setToName] = (0, import_react17.useState)(null);
3850
+ const cancelRef = (0, import_react17.useRef)(null);
3851
+ const refresh = (0, import_react17.useCallback)(async () => {
3638
3852
  if (!host.listSceneFamilyTracks) {
3639
3853
  setLoad({ status: "error", message: "This host does not support crossfade tracks." });
3640
3854
  return;
@@ -3654,7 +3868,7 @@ function CrossfadeModal({
3654
3868
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
3655
3869
  }
3656
3870
  }, [host, fromSceneId, toSceneId]);
3657
- (0, import_react16.useEffect)(() => {
3871
+ (0, import_react17.useEffect)(() => {
3658
3872
  if (open) {
3659
3873
  setError(null);
3660
3874
  setIsCreating(false);
@@ -3663,21 +3877,21 @@ function CrossfadeModal({
3663
3877
  void refresh();
3664
3878
  }
3665
3879
  }, [open, refresh]);
3666
- const excludeSet = (0, import_react16.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3667
- const originCandidates = (0, import_react16.useMemo)(
3880
+ const excludeSet = (0, import_react17.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3881
+ const originCandidates = (0, import_react17.useMemo)(
3668
3882
  () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
3669
3883
  [load, excludeSet]
3670
3884
  );
3671
- const targetCandidates = (0, import_react16.useMemo)(
3885
+ const targetCandidates = (0, import_react17.useMemo)(
3672
3886
  () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
3673
3887
  [load, excludeSet]
3674
3888
  );
3675
- (0, import_react16.useEffect)(() => {
3889
+ (0, import_react17.useEffect)(() => {
3676
3890
  if (!originCandidates.some((t) => t.dbId === originDbId)) {
3677
3891
  setOriginDbId(originCandidates[0]?.dbId ?? "");
3678
3892
  }
3679
3893
  }, [originCandidates, originDbId]);
3680
- (0, import_react16.useEffect)(() => {
3894
+ (0, import_react17.useEffect)(() => {
3681
3895
  if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
3682
3896
  setTargetDbId(targetCandidates[0]?.dbId ?? "");
3683
3897
  }
@@ -3685,10 +3899,10 @@ function CrossfadeModal({
3685
3899
  const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
3686
3900
  const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
3687
3901
  const canCreate = !isCreating && !!originTrack && !!targetTrack;
3688
- const handleClose = (0, import_react16.useCallback)(() => {
3902
+ const handleClose = (0, import_react17.useCallback)(() => {
3689
3903
  if (!isCreating) onClose();
3690
3904
  }, [isCreating, onClose]);
3691
- const handleCreate = (0, import_react16.useCallback)(async () => {
3905
+ const handleCreate = (0, import_react17.useCallback)(async () => {
3692
3906
  if (!originTrack || !targetTrack) return;
3693
3907
  setIsCreating(true);
3694
3908
  setError(null);
@@ -3706,26 +3920,26 @@ function CrossfadeModal({
3706
3920
  const fromLabel = fromName ?? fromSceneName ?? null;
3707
3921
  const toLabel = toName ?? toSceneName ?? null;
3708
3922
  if (!open) return null;
3709
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3923
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3710
3924
  "div",
3711
3925
  {
3712
3926
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3713
3927
  onClick: (e) => e.stopPropagation(),
3714
3928
  "data-testid": `${testIdPrefix}-box`,
3715
3929
  children: [
3716
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
3717
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3930
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
3931
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3718
3932
  "Bridge a track from",
3719
3933
  " ",
3720
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3934
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3721
3935
  " into one from",
3722
3936
  " ",
3723
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3937
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3724
3938
  ". Both layers share one generated part; each keeps its own preset."
3725
3939
  ] }),
3726
- load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3727
- load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3728
- load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3940
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3941
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3942
+ load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3729
3943
  "div",
3730
3944
  {
3731
3945
  className: "text-xs text-sas-muted py-4 text-center",
@@ -3736,20 +3950,20 @@ function CrossfadeModal({
3736
3950
  ". Add one (or free one from another crossfade) first."
3737
3951
  ]
3738
3952
  }
3739
- ) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
3740
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "block", children: [
3741
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3953
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
3954
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "block", children: [
3955
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3742
3956
  "Origin ",
3743
3957
  fromLabel ? `(${fromLabel})` : "(top)"
3744
3958
  ] }),
3745
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3959
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3746
3960
  "div",
3747
3961
  {
3748
3962
  role: "radiogroup",
3749
3963
  "aria-label": "Origin track",
3750
3964
  "data-testid": `${testIdPrefix}-origin-list`,
3751
3965
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3752
- children: originCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3966
+ children: originCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3753
3967
  CandidateRow,
3754
3968
  {
3755
3969
  track: t,
@@ -3763,23 +3977,23 @@ function CrossfadeModal({
3763
3977
  }
3764
3978
  )
3765
3979
  ] }),
3766
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "block", children: [
3767
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3980
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "block", children: [
3981
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3768
3982
  "Target ",
3769
3983
  toLabel ? `(${toLabel})` : "(bottom)"
3770
3984
  ] }),
3771
- targetCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3985
+ targetCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3772
3986
  "No available tracks in ",
3773
3987
  toLabel ?? "the target scene",
3774
3988
  " to crossfade into."
3775
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3989
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3776
3990
  "div",
3777
3991
  {
3778
3992
  role: "radiogroup",
3779
3993
  "aria-label": "Target track",
3780
3994
  "data-testid": `${testIdPrefix}-target-list`,
3781
3995
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3782
- children: targetCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3996
+ children: targetCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3783
3997
  CandidateRow,
3784
3998
  {
3785
3999
  track: t,
@@ -3794,9 +4008,9 @@ function CrossfadeModal({
3794
4008
  )
3795
4009
  ] })
3796
4010
  ] })),
3797
- error && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3798
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
3799
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4011
+ error && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
4012
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
4013
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3800
4014
  "button",
3801
4015
  {
3802
4016
  ref: cancelRef,
@@ -3807,7 +4021,7 @@ function CrossfadeModal({
3807
4021
  children: "Cancel"
3808
4022
  }
3809
4023
  ),
3810
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4024
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3811
4025
  "button",
3812
4026
  {
3813
4027
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3824,10 +4038,10 @@ function CrossfadeModal({
3824
4038
  }
3825
4039
 
3826
4040
  // src/components/TransitionDesigner.tsx
3827
- var import_react18 = require("react");
4041
+ var import_react19 = require("react");
3828
4042
 
3829
4043
  // src/hooks/useTrackReorder.ts
3830
- var import_react17 = require("react");
4044
+ var import_react18 = require("react");
3831
4045
  function moveItem(arr, from, to) {
3832
4046
  const next = arr.slice();
3833
4047
  if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
@@ -3844,12 +4058,12 @@ function useTrackReorder({
3844
4058
  getId,
3845
4059
  onError
3846
4060
  }) {
3847
- const [draggingIndex, setDraggingIndex] = (0, import_react17.useState)(null);
3848
- const [dragOverIndex, setDragOverIndex] = (0, import_react17.useState)(null);
3849
- const fromRef = (0, import_react17.useRef)(null);
3850
- const itemsRef = (0, import_react17.useRef)(items);
4061
+ const [draggingIndex, setDraggingIndex] = (0, import_react18.useState)(null);
4062
+ const [dragOverIndex, setDragOverIndex] = (0, import_react18.useState)(null);
4063
+ const fromRef = (0, import_react18.useRef)(null);
4064
+ const itemsRef = (0, import_react18.useRef)(items);
3851
4065
  itemsRef.current = items;
3852
- const dragPropsFor = (0, import_react17.useCallback)(
4066
+ const dragPropsFor = (0, import_react18.useCallback)(
3853
4067
  (index) => ({
3854
4068
  handleProps: {
3855
4069
  draggable: true,
@@ -4031,7 +4245,7 @@ function dbIdsFromKeys(keys) {
4031
4245
  }
4032
4246
 
4033
4247
  // src/components/TransitionDesigner.tsx
4034
- var import_jsx_runtime18 = require("react/jsx-runtime");
4248
+ var import_jsx_runtime19 = require("react/jsx-runtime");
4035
4249
  var CROSSFADE_ESTIMATE_MS = 15e3;
4036
4250
  var FADE_ESTIMATE_MS = 11e3;
4037
4251
  var CREATE_ALL_CONCURRENCY = 5;
@@ -4052,56 +4266,67 @@ function TransitionDesigner({
4052
4266
  onCreateCrossfade,
4053
4267
  onCreateFade,
4054
4268
  onCreateAudioTransition,
4269
+ mapColumnSubjects,
4270
+ fadeEstimateMs,
4055
4271
  familyLabel,
4056
4272
  testIdPrefix = "transition-designer"
4057
4273
  }) {
4058
- const [load, setLoad] = (0, import_react18.useState)({ status: "loading" });
4059
- const [fromName, setFromName] = (0, import_react18.useState)(null);
4060
- const [toName, setToName] = (0, import_react18.useState)(null);
4061
- const [originSlots, setOriginSlots] = (0, import_react18.useState)([]);
4062
- const [targetSlots, setTargetSlots] = (0, import_react18.useState)([]);
4063
- const [creatingKeys, setCreatingKeys] = (0, import_react18.useState)(() => /* @__PURE__ */ new Set());
4064
- const [rowErrors, setRowErrors] = (0, import_react18.useState)({});
4065
- const [rowEffects, setRowEffects] = (0, import_react18.useState)({});
4066
- const rowEffectsRef = (0, import_react18.useRef)(rowEffects);
4274
+ const fadeOnly = !onCreateCrossfade;
4275
+ const [load, setLoad] = (0, import_react19.useState)({ status: "loading" });
4276
+ const [fromName, setFromName] = (0, import_react19.useState)(null);
4277
+ const [toName, setToName] = (0, import_react19.useState)(null);
4278
+ const [originSlots, setOriginSlots] = (0, import_react19.useState)([]);
4279
+ const [targetSlots, setTargetSlots] = (0, import_react19.useState)([]);
4280
+ const [creatingKeys, setCreatingKeys] = (0, import_react19.useState)(() => /* @__PURE__ */ new Set());
4281
+ const [rowErrors, setRowErrors] = (0, import_react19.useState)({});
4282
+ const [rowEffects, setRowEffects] = (0, import_react19.useState)({});
4283
+ const rowEffectsRef = (0, import_react19.useRef)(rowEffects);
4067
4284
  rowEffectsRef.current = rowEffects;
4068
4285
  const audioEffectsEnabled = !!onCreateAudioTransition;
4069
- const excludeRef = (0, import_react18.useRef)(excludeSourceDbIds);
4286
+ const excludeRef = (0, import_react19.useRef)(excludeSourceDbIds);
4070
4287
  excludeRef.current = excludeSourceDbIds;
4071
- const originSlotsRef = (0, import_react18.useRef)(originSlots);
4288
+ const originSlotsRef = (0, import_react19.useRef)(originSlots);
4072
4289
  originSlotsRef.current = originSlots;
4073
- const targetSlotsRef = (0, import_react18.useRef)(targetSlots);
4290
+ const targetSlotsRef = (0, import_react19.useRef)(targetSlots);
4074
4291
  targetSlotsRef.current = targetSlots;
4075
- const creatingKeysRef = (0, import_react18.useRef)(creatingKeys);
4292
+ const creatingKeysRef = (0, import_react19.useRef)(creatingKeys);
4076
4293
  creatingKeysRef.current = creatingKeys;
4077
- const dragRef = (0, import_react18.useRef)(null);
4078
- const [dragging, setDragging] = (0, import_react18.useState)(null);
4079
- const [dragOver, setDragOver] = (0, import_react18.useState)(null);
4080
- const excludeSet = (0, import_react18.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
4081
- const originPool = (0, import_react18.useMemo)(
4294
+ const dragRef = (0, import_react19.useRef)(null);
4295
+ const [dragging, setDragging] = (0, import_react19.useState)(null);
4296
+ const [dragOver, setDragOver] = (0, import_react19.useState)(null);
4297
+ const excludeSet = (0, import_react19.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
4298
+ const originPool = (0, import_react19.useMemo)(
4082
4299
  () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
4083
4300
  [load, excludeSet]
4084
4301
  );
4085
- const targetPool = (0, import_react18.useMemo)(
4302
+ const targetPool = (0, import_react19.useMemo)(
4086
4303
  () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
4087
4304
  [load, excludeSet]
4088
4305
  );
4089
- const originById = (0, import_react18.useMemo)(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
4090
- const targetById = (0, import_react18.useMemo)(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
4091
- const originByIdRef = (0, import_react18.useRef)(originById);
4306
+ const originById = (0, import_react19.useMemo)(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
4307
+ const targetById = (0, import_react19.useMemo)(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
4308
+ const originByIdRef = (0, import_react19.useRef)(originById);
4092
4309
  originByIdRef.current = originById;
4093
- const targetByIdRef = (0, import_react18.useRef)(targetById);
4310
+ const targetByIdRef = (0, import_react19.useRef)(targetById);
4094
4311
  targetByIdRef.current = targetById;
4095
- const refresh = (0, import_react18.useCallback)(async () => {
4312
+ const refresh = (0, import_react19.useCallback)(async () => {
4096
4313
  if (!host.listSceneFamilyTracks) {
4097
4314
  setLoad({ status: "error", message: "This host does not support transition tracks." });
4098
4315
  return;
4099
4316
  }
4100
4317
  setLoad({ status: "loading" });
4101
4318
  try {
4102
- const [origin, target, fName, tName, draftRaw] = await Promise.all([
4319
+ let [origin, target] = await Promise.all([
4103
4320
  host.listSceneFamilyTracks(fromSceneId),
4104
- host.listSceneFamilyTracks(toSceneId),
4321
+ host.listSceneFamilyTracks(toSceneId)
4322
+ ]);
4323
+ if (mapColumnSubjects) {
4324
+ [origin, target] = await Promise.all([
4325
+ mapColumnSubjects(fromSceneId, origin),
4326
+ mapColumnSubjects(toSceneId, target)
4327
+ ]);
4328
+ }
4329
+ const [fName, tName, draftRaw] = await Promise.all([
4105
4330
  host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
4106
4331
  host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
4107
4332
  host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
@@ -4126,11 +4351,11 @@ function TransitionDesigner({
4126
4351
  message: err instanceof Error ? err.message : "Failed to load tracks."
4127
4352
  });
4128
4353
  }
4129
- }, [host, fromSceneId, toSceneId, transitionSceneId]);
4130
- (0, import_react18.useEffect)(() => {
4354
+ }, [host, fromSceneId, toSceneId, transitionSceneId, mapColumnSubjects]);
4355
+ (0, import_react19.useEffect)(() => {
4131
4356
  void refresh();
4132
4357
  }, [refresh]);
4133
- (0, import_react18.useEffect)(() => {
4358
+ (0, import_react19.useEffect)(() => {
4134
4359
  if (load.status !== "ready") return;
4135
4360
  const [po, pt] = padPair(
4136
4361
  reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
@@ -4139,7 +4364,7 @@ function TransitionDesigner({
4139
4364
  if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
4140
4365
  if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
4141
4366
  }, [originPool, targetPool, load.status]);
4142
- const mutate = (0, import_react18.useCallback)(
4367
+ const mutate = (0, import_react19.useCallback)(
4143
4368
  (nextOrigin, nextTarget) => {
4144
4369
  const norm = normalizeSlots(nextOrigin, nextTarget);
4145
4370
  const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
@@ -4152,7 +4377,7 @@ function TransitionDesigner({
4152
4377
  },
4153
4378
  [host, transitionSceneId]
4154
4379
  );
4155
- const setRowEffect = (0, import_react18.useCallback)(
4380
+ const setRowEffect = (0, import_react19.useCallback)(
4156
4381
  (sourceDbId, effect) => {
4157
4382
  setRowEffects((prev) => {
4158
4383
  const next = { ...prev, [sourceDbId]: effect };
@@ -4166,7 +4391,7 @@ function TransitionDesigner({
4166
4391
  },
4167
4392
  [host, transitionSceneId]
4168
4393
  );
4169
- const insertGapAbove = (0, import_react18.useCallback)(
4394
+ const insertGapAbove = (0, import_react19.useCallback)(
4170
4395
  (col, index) => {
4171
4396
  const slots = col === "origin" ? originSlots : targetSlots;
4172
4397
  const next = [...slots.slice(0, index), null, ...slots.slice(index)];
@@ -4175,7 +4400,7 @@ function TransitionDesigner({
4175
4400
  },
4176
4401
  [originSlots, targetSlots, mutate]
4177
4402
  );
4178
- const removeGap = (0, import_react18.useCallback)(
4403
+ const removeGap = (0, import_react19.useCallback)(
4179
4404
  (col, index) => {
4180
4405
  const slots = col === "origin" ? originSlots : targetSlots;
4181
4406
  const next = slots.filter((_, i) => i !== index);
@@ -4184,7 +4409,7 @@ function TransitionDesigner({
4184
4409
  },
4185
4410
  [originSlots, targetSlots, mutate]
4186
4411
  );
4187
- const handleDrop = (0, import_react18.useCallback)(
4412
+ const handleDrop = (0, import_react19.useCallback)(
4188
4413
  (col, to) => {
4189
4414
  const from = dragRef.current;
4190
4415
  dragRef.current = null;
@@ -4196,16 +4421,34 @@ function TransitionDesigner({
4196
4421
  },
4197
4422
  [originSlots, targetSlots, mutate]
4198
4423
  );
4199
- const rows = (0, import_react18.useMemo)(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
4200
- const creatingDbIds = (0, import_react18.useMemo)(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
4201
- const eligibleCount = (0, import_react18.useMemo)(
4202
- () => rows.filter((r) => {
4424
+ const rows = (0, import_react19.useMemo)(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
4425
+ const fadeOnlyRows = (0, import_react19.useMemo)(
4426
+ () => fadeOnly ? [
4427
+ ...originPool.map((t) => ({
4428
+ originId: t.dbId,
4429
+ targetId: null,
4430
+ type: "fade-out"
4431
+ })),
4432
+ ...targetPool.map((t) => ({
4433
+ originId: null,
4434
+ targetId: t.dbId,
4435
+ type: "fade-in"
4436
+ }))
4437
+ ] : [],
4438
+ [fadeOnly, originPool, targetPool]
4439
+ );
4440
+ const fadeOnlyRowsRef = (0, import_react19.useRef)(fadeOnlyRows);
4441
+ fadeOnlyRowsRef.current = fadeOnlyRows;
4442
+ const effectiveRows = fadeOnly ? fadeOnlyRows : rows;
4443
+ const creatingDbIds = (0, import_react19.useMemo)(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
4444
+ const eligibleCount = (0, import_react19.useMemo)(
4445
+ () => effectiveRows.filter((r) => {
4203
4446
  const k = rowKey(r);
4204
4447
  return k !== null && !creatingKeys.has(k);
4205
4448
  }).length,
4206
- [rows, creatingKeys]
4449
+ [effectiveRows, creatingKeys]
4207
4450
  );
4208
- const createRow = (0, import_react18.useCallback)(
4451
+ const createRow = (0, import_react19.useCallback)(
4209
4452
  async (row) => {
4210
4453
  const key = rowKey(row);
4211
4454
  if (!key || !row.type || creatingKeysRef.current.has(key)) return;
@@ -4221,6 +4464,7 @@ function TransitionDesigner({
4221
4464
  const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
4222
4465
  const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
4223
4466
  if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
4467
+ if (!onCreateCrossfade) throw new Error("This panel does not support crossfades.");
4224
4468
  await onCreateCrossfade(
4225
4469
  { dbId: o.dbId, name: o.name, role: o.role },
4226
4470
  { dbId: t.dbId, name: t.name, role: t.role }
@@ -4259,8 +4503,9 @@ function TransitionDesigner({
4259
4503
  },
4260
4504
  [onCreateCrossfade, onCreateFade, onCreateAudioTransition]
4261
4505
  );
4262
- const createAll = (0, import_react18.useCallback)(async () => {
4263
- const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
4506
+ const createAll = (0, import_react19.useCallback)(async () => {
4507
+ const source = fadeOnly ? fadeOnlyRowsRef.current : buildRowSlots(originSlotsRef.current, targetSlotsRef.current);
4508
+ const eligible = source.filter((r) => {
4264
4509
  const k = rowKey(r);
4265
4510
  return k !== null && !creatingKeysRef.current.has(k);
4266
4511
  });
@@ -4276,7 +4521,7 @@ function TransitionDesigner({
4276
4521
  await Promise.all(
4277
4522
  Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
4278
4523
  );
4279
- }, [createRow]);
4524
+ }, [createRow, fadeOnly]);
4280
4525
  const fromLabel = fromName ?? "origin";
4281
4526
  const toLabel = toName ?? "target";
4282
4527
  const cellDragProps = (col, index, locked) => ({
@@ -4327,15 +4572,15 @@ function TransitionDesigner({
4327
4572
  const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
4328
4573
  const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
4329
4574
  if (slotId === null) {
4330
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4575
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4331
4576
  "div",
4332
4577
  {
4333
4578
  ...cellDragProps(col, index, false),
4334
4579
  "data-testid": `${testIdPrefix}-${col}-gap-${index}`,
4335
4580
  className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
4336
4581
  children: [
4337
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4338
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4582
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4583
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4339
4584
  "button",
4340
4585
  {
4341
4586
  type: "button",
@@ -4352,7 +4597,7 @@ function TransitionDesigner({
4352
4597
  }
4353
4598
  const primary = track ? track.prompt?.trim() || track.name : slotId;
4354
4599
  const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
4355
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4600
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4356
4601
  "div",
4357
4602
  {
4358
4603
  ...cellDragProps(col, index, locked),
@@ -4360,13 +4605,13 @@ function TransitionDesigner({
4360
4605
  "data-value": slotId,
4361
4606
  className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
4362
4607
  title: track ? track.dbId : "Track no longer available",
4363
- children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-start gap-1", children: [
4364
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4365
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "min-w-0 flex-1", children: [
4366
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-text truncate", children: primary }),
4367
- meta && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4608
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-start gap-1", children: [
4609
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4610
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "min-w-0 flex-1", children: [
4611
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-text truncate", children: primary }),
4612
+ meta && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4368
4613
  ] }),
4369
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4614
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4370
4615
  "button",
4371
4616
  {
4372
4617
  type: "button",
@@ -4382,22 +4627,159 @@ function TransitionDesigner({
4382
4627
  }
4383
4628
  );
4384
4629
  };
4385
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4386
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4387
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4388
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-text", children: fromLabel }),
4630
+ const renderFadeOnlyRow = (row, i) => {
4631
+ const slotId = row.originId ?? row.targetId;
4632
+ const byId = row.type === "fade-out" ? originById : targetById;
4633
+ const track = byId.get(slotId);
4634
+ const key = rowKey(row);
4635
+ const isCreatingThis = key !== null && creatingKeys.has(key);
4636
+ const errMsg = key !== null ? rowErrors[key] : void 0;
4637
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4638
+ "div",
4639
+ {
4640
+ "data-testid": `${testIdPrefix}-row-${i}`,
4641
+ className: "grid grid-cols-[1fr_auto] gap-2 items-center",
4642
+ children: [
4643
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4644
+ "div",
4645
+ {
4646
+ "data-testid": `${testIdPrefix}-subject-${slotId}`,
4647
+ className: `rounded-sm border border-sas-border bg-sas-panel px-2 py-1.5 ${isCreatingThis ? "opacity-60" : ""}`,
4648
+ title: track ? track.dbId : "Track no longer available",
4649
+ children: [
4650
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-text truncate", children: track ? track.prompt?.trim() || track.name : slotId }),
4651
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing" })
4652
+ ]
4653
+ }
4654
+ ),
4655
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4656
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4657
+ "span",
4658
+ {
4659
+ "data-testid": `${testIdPrefix}-type-${i}`,
4660
+ className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
4661
+ children: row.type ? TYPE_LABEL[row.type] : "\u2014"
4662
+ }
4663
+ ),
4664
+ isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4665
+ SorceryProgressBar,
4666
+ {
4667
+ isLoading: true,
4668
+ heightClass: "h-5",
4669
+ statusText: "CREATING",
4670
+ estimatedDurationMs: fadeEstimateMs ?? FADE_ESTIMATE_MS
4671
+ }
4672
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4673
+ "button",
4674
+ {
4675
+ type: "button",
4676
+ "data-testid": `${testIdPrefix}-create-${i}`,
4677
+ onClick: () => createRow(row),
4678
+ className: "w-full px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg",
4679
+ children: "Create"
4680
+ }
4681
+ ),
4682
+ errMsg && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4683
+ "span",
4684
+ {
4685
+ "data-testid": `${testIdPrefix}-row-error-${i}`,
4686
+ className: "text-[10px] text-sas-danger text-center leading-tight",
4687
+ children: errMsg
4688
+ }
4689
+ )
4690
+ ] })
4691
+ ]
4692
+ },
4693
+ slotId
4694
+ );
4695
+ };
4696
+ if (fadeOnly) {
4697
+ const outRows = fadeOnlyRows.filter((r) => r.type === "fade-out");
4698
+ const inRows = fadeOnlyRows.filter((r) => r.type === "fade-in");
4699
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4700
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4701
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4702
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-text", children: fromLabel }),
4703
+ " \u2192",
4704
+ " ",
4705
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-text", children: toLabel }),
4706
+ familyLabel ? ` \xB7 ${familyLabel}` : "",
4707
+ " \xB7 each entry fades on its own \u2014 origin fades out, target fades in."
4708
+ ] }),
4709
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
4710
+ creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4711
+ "span",
4712
+ {
4713
+ className: "text-[10px] text-sas-accent whitespace-nowrap",
4714
+ "data-testid": `${testIdPrefix}-creating-count`,
4715
+ children: [
4716
+ creatingKeys.size,
4717
+ " creating\u2026"
4718
+ ]
4719
+ }
4720
+ ),
4721
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4722
+ "button",
4723
+ {
4724
+ type: "button",
4725
+ "data-testid": `${testIdPrefix}-create-all`,
4726
+ onClick: createAll,
4727
+ disabled: eligibleCount === 0,
4728
+ title: "Create every fade at once (runs several concurrently)",
4729
+ className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors whitespace-nowrap ${eligibleCount > 0 ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
4730
+ children: [
4731
+ "Create all",
4732
+ eligibleCount > 0 ? ` (${eligibleCount})` : ""
4733
+ ]
4734
+ }
4735
+ )
4736
+ ] })
4737
+ ] }),
4738
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4739
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4740
+ load.status === "ready" && (fadeOnlyRows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4741
+ "Nothing to fade in this panel for either scene. Add tracks to ",
4742
+ fromLabel,
4743
+ " or ",
4744
+ toLabel,
4745
+ " ",
4746
+ "first (or free one by deleting an existing fade)."
4747
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-3", children: [
4748
+ outRows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-out-section`, children: [
4749
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4750
+ "Origin (",
4751
+ fromLabel,
4752
+ ") \u2014 fades out"
4753
+ ] }),
4754
+ outRows.map((row, i) => renderFadeOnlyRow(row, i))
4755
+ ] }),
4756
+ inRows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-in-section`, children: [
4757
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4758
+ "Target (",
4759
+ toLabel,
4760
+ ") \u2014 fades in"
4761
+ ] }),
4762
+ inRows.map((row, j) => renderFadeOnlyRow(row, outRows.length + j))
4763
+ ] })
4764
+ ] }))
4765
+ ] });
4766
+ }
4767
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4768
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4769
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4770
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-text", children: fromLabel }),
4389
4771
  " \u2192",
4390
4772
  " ",
4391
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-text", children: toLabel }),
4773
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-text", children: toLabel }),
4392
4774
  familyLabel ? ` \xB7 ${familyLabel}` : "",
4393
4775
  " \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
4394
4776
  ] }),
4395
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
4396
- creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4777
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
4778
+ creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4397
4779
  creatingKeys.size,
4398
4780
  " creating\u2026"
4399
4781
  ] }),
4400
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4782
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4401
4783
  "button",
4402
4784
  {
4403
4785
  type: "button",
@@ -4414,49 +4796,49 @@ function TransitionDesigner({
4414
4796
  )
4415
4797
  ] })
4416
4798
  ] }),
4417
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4418
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4799
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4800
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4419
4801
  "Origin (",
4420
4802
  fromLabel,
4421
4803
  ")"
4422
4804
  ] }),
4423
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4424
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4805
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4806
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4425
4807
  "Target (",
4426
4808
  toLabel,
4427
4809
  ")"
4428
4810
  ] })
4429
4811
  ] }),
4430
- load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4431
- load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4432
- load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4812
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4813
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4814
+ load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4433
4815
  "No tracks to arrange in this panel for either scene. Add tracks to ",
4434
4816
  fromLabel,
4435
4817
  " or ",
4436
4818
  toLabel,
4437
4819
  " ",
4438
4820
  "first (or free one by deleting an existing crossfade/fade)."
4439
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "space-y-2", children: rows.map((row, i) => {
4821
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "space-y-2", children: rows.map((row, i) => {
4440
4822
  const key = rowKey(row);
4441
4823
  const isCreatingThis = key !== null && creatingKeys.has(key);
4442
4824
  const errMsg = key !== null ? rowErrors[key] : void 0;
4443
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4825
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4444
4826
  "div",
4445
4827
  {
4446
4828
  "data-testid": `${testIdPrefix}-row-${i}`,
4447
4829
  className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
4448
4830
  children: [
4449
4831
  renderCell("origin", i, row.originId),
4450
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4451
- !row.type ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4832
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4833
+ !row.type ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4452
4834
  "span",
4453
4835
  {
4454
4836
  "data-testid": `${testIdPrefix}-type-${i}`,
4455
4837
  className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
4456
4838
  children: TYPE_LABEL[row.type]
4457
4839
  }
4458
- ) : audioEffectsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4459
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4840
+ ) : audioEffectsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4841
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4460
4842
  "select",
4461
4843
  {
4462
4844
  "data-testid": `${testIdPrefix}-effect-${i}`,
@@ -4466,11 +4848,11 @@ function TransitionDesigner({
4466
4848
  if (id) setRowEffect(id, e.target.value);
4467
4849
  },
4468
4850
  className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
4469
- children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4851
+ children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4470
4852
  }
4471
4853
  ),
4472
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4473
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4854
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4855
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4474
4856
  "span",
4475
4857
  {
4476
4858
  "data-testid": `${testIdPrefix}-type-${i}`,
@@ -4478,7 +4860,7 @@ function TransitionDesigner({
4478
4860
  children: TYPE_LABEL[row.type]
4479
4861
  }
4480
4862
  ),
4481
- isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4863
+ isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4482
4864
  SorceryProgressBar,
4483
4865
  {
4484
4866
  isLoading: true,
@@ -4486,7 +4868,7 @@ function TransitionDesigner({
4486
4868
  statusText: "CREATING",
4487
4869
  estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
4488
4870
  }
4489
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4871
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4490
4872
  "button",
4491
4873
  {
4492
4874
  type: "button",
@@ -4497,7 +4879,7 @@ function TransitionDesigner({
4497
4879
  children: "Create"
4498
4880
  }
4499
4881
  ),
4500
- errMsg && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4882
+ errMsg && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4501
4883
  "span",
4502
4884
  {
4503
4885
  "data-testid": `${testIdPrefix}-row-error-${i}`,
@@ -4516,8 +4898,8 @@ function TransitionDesigner({
4516
4898
  }
4517
4899
 
4518
4900
  // src/components/PanelMasterStrip.tsx
4519
- var import_react19 = require("react");
4520
- var import_jsx_runtime19 = require("react/jsx-runtime");
4901
+ var import_react20 = require("react");
4902
+ var import_jsx_runtime20 = require("react/jsx-runtime");
4521
4903
  function PanelMasterStrip({
4522
4904
  bus,
4523
4905
  levels = null,
@@ -4536,22 +4918,22 @@ function PanelMasterStrip({
4536
4918
  onToggleFxEnabled,
4537
4919
  onShowFxEditor
4538
4920
  }) {
4539
- const [search, setSearch] = (0, import_react19.useState)("");
4540
- const filtered = (0, import_react19.useMemo)(() => {
4921
+ const [search, setSearch] = (0, import_react20.useState)("");
4922
+ const filtered = (0, import_react20.useMemo)(() => {
4541
4923
  const q = search.trim().toLowerCase();
4542
4924
  if (!q) return availableFx;
4543
4925
  return availableFx.filter(
4544
4926
  (fx) => fx.name.toLowerCase().includes(q) || fx.manufacturer.toLowerCase().includes(q)
4545
4927
  );
4546
4928
  }, [availableFx, search]);
4547
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4929
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
4548
4930
  "div",
4549
4931
  {
4550
4932
  "data-testid": "panel-master-strip",
4551
4933
  className: `flex flex-col gap-1 px-2 py-1.5 rounded-sm border border-sas-border border-l-2 border-l-sas-accent/50 bg-sas-accent/5 transition-opacity ${soloedOut ? "opacity-40" : ""}`,
4552
4934
  children: [
4553
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2", children: [
4554
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4935
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-2", children: [
4936
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4555
4937
  "span",
4556
4938
  {
4557
4939
  className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
@@ -4559,8 +4941,8 @@ function PanelMasterStrip({
4559
4941
  children: "BUS"
4560
4942
  }
4561
4943
  ),
4562
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
4563
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4944
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
4945
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4564
4946
  VolumeSlider,
4565
4947
  {
4566
4948
  value: dbToSlider(bus.volume),
@@ -4568,8 +4950,8 @@ function PanelMasterStrip({
4568
4950
  disabled
4569
4951
  }
4570
4952
  ),
4571
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
4572
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4953
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
4954
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4573
4955
  LevelMeter,
4574
4956
  {
4575
4957
  peakDb: levels?.leftDb ?? -120,
@@ -4579,7 +4961,7 @@ function PanelMasterStrip({
4579
4961
  "data-testid": "bus-meter-left"
4580
4962
  }
4581
4963
  ),
4582
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4964
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4583
4965
  LevelMeter,
4584
4966
  {
4585
4967
  peakDb: levels?.rightDb ?? -120,
@@ -4590,7 +4972,7 @@ function PanelMasterStrip({
4590
4972
  )
4591
4973
  ] })
4592
4974
  ] }),
4593
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4975
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4594
4976
  "button",
4595
4977
  {
4596
4978
  "data-testid": "bus-mute-button",
@@ -4601,7 +4983,7 @@ function PanelMasterStrip({
4601
4983
  children: "M"
4602
4984
  }
4603
4985
  ),
4604
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4986
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4605
4987
  "button",
4606
4988
  {
4607
4989
  "data-testid": "bus-solo-button",
@@ -4612,14 +4994,14 @@ function PanelMasterStrip({
4612
4994
  children: "S"
4613
4995
  }
4614
4996
  ),
4615
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4997
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
4616
4998
  "span",
4617
4999
  {
4618
5000
  "data-testid": `bus-fx-chip-${fx.index}`,
4619
5001
  className: `flex items-center gap-1 px-1.5 py-0.5 rounded-sm border text-[10px] whitespace-nowrap ${fx.enabled ? "border-sas-accent/60 text-sas-accent bg-sas-accent/10" : "border-sas-border text-sas-muted/50 bg-sas-panel"}`,
4620
5002
  title: `${fx.name}${fx.enabled ? "" : " (bypassed)"}`,
4621
5003
  children: [
4622
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5004
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4623
5005
  "button",
4624
5006
  {
4625
5007
  "data-testid": `bus-fx-toggle-${fx.index}`,
@@ -4630,7 +5012,7 @@ function PanelMasterStrip({
4630
5012
  children: fx.enabled ? "\u25CF" : "\u25CB"
4631
5013
  }
4632
5014
  ),
4633
- onShowFxEditor ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5015
+ onShowFxEditor ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4634
5016
  "button",
4635
5017
  {
4636
5018
  "data-testid": `bus-fx-edit-${fx.index}`,
@@ -4640,8 +5022,8 @@ function PanelMasterStrip({
4640
5022
  title: `Open ${fx.name} editor`,
4641
5023
  children: fx.name
4642
5024
  }
4643
- ) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "max-w-[80px] truncate", children: fx.name }),
4644
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5025
+ ) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "max-w-[80px] truncate", children: fx.name }),
5026
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4645
5027
  "button",
4646
5028
  {
4647
5029
  "data-testid": `bus-fx-remove-${fx.index}`,
@@ -4656,7 +5038,7 @@ function PanelMasterStrip({
4656
5038
  },
4657
5039
  `${fx.index}:${fx.pluginId}`
4658
5040
  )) }),
4659
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5041
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4660
5042
  "button",
4661
5043
  {
4662
5044
  "data-testid": "bus-fx-add-button",
@@ -4668,9 +5050,9 @@ function PanelMasterStrip({
4668
5050
  }
4669
5051
  )
4670
5052
  ] }),
4671
- fxPickerOpen && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
4672
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2", children: [
4673
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5053
+ fxPickerOpen && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
5054
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-2", children: [
5055
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4674
5056
  "input",
4675
5057
  {
4676
5058
  type: "text",
@@ -4680,19 +5062,19 @@ function PanelMasterStrip({
4680
5062
  className: "sas-input flex-1 px-2 py-1 text-xs"
4681
5063
  }
4682
5064
  ),
4683
- onRefreshFx && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5065
+ onRefreshFx && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4684
5066
  "button",
4685
5067
  {
4686
5068
  onClick: () => onRefreshFx(),
4687
5069
  disabled: fxLoading,
4688
5070
  className: "px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-50",
4689
- title: "Re-scan plugins",
5071
+ title: "Re-scan plugins \u2014 picks up newly installed FX and retries any that failed a previous scan",
4690
5072
  children: fxLoading ? "..." : "Refresh"
4691
5073
  }
4692
5074
  )
4693
5075
  ] }),
4694
- fxLoading && availableFx.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
4695
- filtered.map((fx) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5076
+ fxLoading && availableFx.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
5077
+ filtered.map((fx) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
4696
5078
  "button",
4697
5079
  {
4698
5080
  "data-testid": `bus-fx-pick-${fx.pluginId}`,
@@ -4700,13 +5082,13 @@ function PanelMasterStrip({
4700
5082
  className: "flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent",
4701
5083
  title: `${fx.name} by ${fx.manufacturer} (${fx.type.toUpperCase()})`,
4702
5084
  children: [
4703
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
4704
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
5085
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
5086
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
4705
5087
  ]
4706
5088
  },
4707
5089
  fx.pluginId
4708
5090
  )),
4709
- filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
5091
+ filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
4710
5092
  ] })
4711
5093
  ] })
4712
5094
  ]
@@ -4715,18 +5097,18 @@ function PanelMasterStrip({
4715
5097
  }
4716
5098
 
4717
5099
  // src/hooks/usePanelBus.ts
4718
- var import_react20 = require("react");
5100
+ var import_react21 = require("react");
4719
5101
  var LEVELS_POLL_MS = 66;
4720
5102
  function usePanelBus(host, activeSceneId) {
4721
5103
  const supported = typeof host.getPanelBusState === "function";
4722
- const [bus, setBus] = (0, import_react20.useState)(null);
4723
- const [levels, setLevels] = (0, import_react20.useState)(null);
4724
- const [availableFx, setAvailableFx] = (0, import_react20.useState)([]);
4725
- const [fxLoading, setFxLoading] = (0, import_react20.useState)(false);
4726
- const [fxPickerOpen, setFxPickerOpen] = (0, import_react20.useState)(false);
4727
- const fxLoadedRef = (0, import_react20.useRef)(false);
4728
- const loadSeqRef = (0, import_react20.useRef)(0);
4729
- const reload = (0, import_react20.useCallback)(async () => {
5104
+ const [bus, setBus] = (0, import_react21.useState)(null);
5105
+ const [levels, setLevels] = (0, import_react21.useState)(null);
5106
+ const [availableFx, setAvailableFx] = (0, import_react21.useState)([]);
5107
+ const [fxLoading, setFxLoading] = (0, import_react21.useState)(false);
5108
+ const [fxPickerOpen, setFxPickerOpen] = (0, import_react21.useState)(false);
5109
+ const fxLoadedRef = (0, import_react21.useRef)(false);
5110
+ const loadSeqRef = (0, import_react21.useRef)(0);
5111
+ const reload = (0, import_react21.useCallback)(async () => {
4730
5112
  if (!supported || !activeSceneId || !host.getPanelBusState) {
4731
5113
  setBus(null);
4732
5114
  return;
@@ -4738,12 +5120,12 @@ function usePanelBus(host, activeSceneId) {
4738
5120
  } catch {
4739
5121
  }
4740
5122
  }, [host, activeSceneId, supported]);
4741
- (0, import_react20.useEffect)(() => {
5123
+ (0, import_react21.useEffect)(() => {
4742
5124
  setBus(null);
4743
5125
  setFxPickerOpen(false);
4744
5126
  void reload();
4745
5127
  }, [reload]);
4746
- (0, import_react20.useEffect)(() => {
5128
+ (0, import_react21.useEffect)(() => {
4747
5129
  if (!supported || !activeSceneId || !bus?.engaged || !host.getPanelBusLevels) {
4748
5130
  setLevels(null);
4749
5131
  return;
@@ -4765,13 +5147,13 @@ function usePanelBus(host, activeSceneId) {
4765
5147
  clearInterval(id);
4766
5148
  };
4767
5149
  }, [supported, activeSceneId, bus?.engaged, host]);
4768
- const loadFxList = (0, import_react20.useCallback)(
4769
- async (force) => {
5150
+ const loadFxList = (0, import_react21.useCallback)(
5151
+ async (opts) => {
4770
5152
  if (!supported || !host.getAvailableFx) return;
4771
- if (fxLoadedRef.current && !force) return;
5153
+ if (fxLoadedRef.current && !opts.force && !opts.rescan) return;
4772
5154
  setFxLoading(true);
4773
5155
  try {
4774
- const list = await host.getAvailableFx();
5156
+ const list = opts.rescan && host.rescanAvailableFx ? await host.rescanAvailableFx() : await host.getAvailableFx();
4775
5157
  setAvailableFx(list);
4776
5158
  fxLoadedRef.current = true;
4777
5159
  } catch {
@@ -4781,14 +5163,14 @@ function usePanelBus(host, activeSceneId) {
4781
5163
  },
4782
5164
  [host, supported]
4783
5165
  );
4784
- const openPicker = (0, import_react20.useCallback)(
5166
+ const openPicker = (0, import_react21.useCallback)(
4785
5167
  (open) => {
4786
5168
  setFxPickerOpen(open);
4787
- if (open) void loadFxList(false);
5169
+ if (open) void loadFxList({});
4788
5170
  },
4789
5171
  [loadFxList]
4790
5172
  );
4791
- const mutate = (0, import_react20.useCallback)(
5173
+ const mutate = (0, import_react21.useCallback)(
4792
5174
  (fn) => {
4793
5175
  if (!fn || !activeSceneId) return;
4794
5176
  void (async () => {
@@ -4809,7 +5191,7 @@ function usePanelBus(host, activeSceneId) {
4809
5191
  fxLoading,
4810
5192
  fxPickerOpen,
4811
5193
  setFxPickerOpen: openPicker,
4812
- refreshFx: () => void loadFxList(true),
5194
+ refreshFx: () => void loadFxList({ rescan: true }),
4813
5195
  reload,
4814
5196
  onVolumeChange: (volumeDb) => mutate(host.setPanelBusVolume && (() => host.setPanelBusVolume(activeSceneId, volumeDb))),
4815
5197
  onMuteToggle: () => mutate(
@@ -4832,8 +5214,8 @@ function usePanelBus(host, activeSceneId) {
4832
5214
  }
4833
5215
 
4834
5216
  // src/components/DownloadPackButton.tsx
4835
- var import_react21 = require("react");
4836
- var import_jsx_runtime20 = require("react/jsx-runtime");
5217
+ var import_react22 = require("react");
5218
+ var import_jsx_runtime21 = require("react/jsx-runtime");
4837
5219
  function formatSize(bytes) {
4838
5220
  if (!bytes || bytes <= 0) return "";
4839
5221
  const gb = bytes / 1024 ** 3;
@@ -4849,10 +5231,10 @@ var DownloadPackButton = ({
4849
5231
  variant = "compact",
4850
5232
  onDownloadComplete
4851
5233
  }) => {
4852
- const [status, setStatus] = (0, import_react21.useState)("idle");
4853
- const [progress, setProgress] = (0, import_react21.useState)(0);
4854
- const [errorMessage, setErrorMessage] = (0, import_react21.useState)(null);
4855
- (0, import_react21.useEffect)(() => {
5234
+ const [status, setStatus] = (0, import_react22.useState)("idle");
5235
+ const [progress, setProgress] = (0, import_react22.useState)(0);
5236
+ const [errorMessage, setErrorMessage] = (0, import_react22.useState)(null);
5237
+ (0, import_react22.useEffect)(() => {
4856
5238
  const unsub = host.onSamplePackProgress(packId, (p) => {
4857
5239
  setStatus(p.status);
4858
5240
  setProgress(p.progress);
@@ -4867,7 +5249,7 @@ var DownloadPackButton = ({
4867
5249
  });
4868
5250
  return unsub;
4869
5251
  }, [host, packId, onDownloadComplete]);
4870
- const handleClick = (0, import_react21.useCallback)(async () => {
5252
+ const handleClick = (0, import_react22.useCallback)(async () => {
4871
5253
  if (status !== "idle" && status !== "error") return;
4872
5254
  try {
4873
5255
  setStatus("downloading");
@@ -4921,8 +5303,8 @@ var DownloadPackButton = ({
4921
5303
  } else {
4922
5304
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
4923
5305
  }
4924
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { children: [
4925
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
5306
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { children: [
5307
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4926
5308
  "button",
4927
5309
  {
4928
5310
  "data-testid": `download-pack-button-${packId}`,
@@ -4933,12 +5315,12 @@ var DownloadPackButton = ({
4933
5315
  children: buttonLabel
4934
5316
  }
4935
5317
  ),
4936
- variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
5318
+ variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
4937
5319
  ] });
4938
5320
  };
4939
5321
 
4940
5322
  // src/components/SamplePackCTACard.tsx
4941
- var import_jsx_runtime21 = require("react/jsx-runtime");
5323
+ var import_jsx_runtime22 = require("react/jsx-runtime");
4942
5324
  var SamplePackCTACard = ({
4943
5325
  host,
4944
5326
  pack,
@@ -4946,7 +5328,7 @@ var SamplePackCTACard = ({
4946
5328
  onDownloadComplete
4947
5329
  }) => {
4948
5330
  if (status === "checking") {
4949
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
5331
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4950
5332
  "div",
4951
5333
  {
4952
5334
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -4957,16 +5339,16 @@ var SamplePackCTACard = ({
4957
5339
  }
4958
5340
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
4959
5341
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
4960
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
5342
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
4961
5343
  "div",
4962
5344
  {
4963
5345
  "data-testid": `sample-pack-cta-${pack.packId}`,
4964
5346
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
4965
5347
  children: [
4966
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
4967
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
4968
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
4969
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
5348
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
5349
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
5350
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
5351
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4970
5352
  DownloadPackButton,
4971
5353
  {
4972
5354
  host,
@@ -4983,7 +5365,7 @@ var SamplePackCTACard = ({
4983
5365
  };
4984
5366
 
4985
5367
  // src/components/WaveformView.tsx
4986
- var import_react22 = require("react");
5368
+ var import_react23 = require("react");
4987
5369
 
4988
5370
  // src/components/waveform.ts
4989
5371
  function computePeaks(audioBuffer, bins, targetSamples) {
@@ -5046,7 +5428,7 @@ function drawWaveform(canvas, peaks, options = {}) {
5046
5428
  }
5047
5429
 
5048
5430
  // src/components/WaveformView.tsx
5049
- var import_jsx_runtime22 = require("react/jsx-runtime");
5431
+ var import_jsx_runtime23 = require("react/jsx-runtime");
5050
5432
  var WaveformView = ({
5051
5433
  host,
5052
5434
  filePath,
@@ -5055,9 +5437,9 @@ var WaveformView = ({
5055
5437
  fillStyle,
5056
5438
  targetSamples
5057
5439
  }) => {
5058
- const canvasRef = (0, import_react22.useRef)(null);
5059
- const [peaks, setPeaks] = (0, import_react22.useState)(null);
5060
- (0, import_react22.useEffect)(() => {
5440
+ const canvasRef = (0, import_react23.useRef)(null);
5441
+ const [peaks, setPeaks] = (0, import_react23.useState)(null);
5442
+ (0, import_react23.useEffect)(() => {
5061
5443
  let cancelled = false;
5062
5444
  let audioContext = null;
5063
5445
  (async () => {
@@ -5083,7 +5465,7 @@ var WaveformView = ({
5083
5465
  cancelled = true;
5084
5466
  };
5085
5467
  }, [host, filePath, bins, targetSamples]);
5086
- (0, import_react22.useEffect)(() => {
5468
+ (0, import_react23.useEffect)(() => {
5087
5469
  if (!peaks) return;
5088
5470
  const canvas = canvasRef.current;
5089
5471
  if (!canvas) return;
@@ -5094,7 +5476,7 @@ var WaveformView = ({
5094
5476
  observer.observe(canvas);
5095
5477
  return () => observer.disconnect();
5096
5478
  }, [peaks, fillStyle]);
5097
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5479
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5098
5480
  "canvas",
5099
5481
  {
5100
5482
  ref: canvasRef,
@@ -5105,8 +5487,8 @@ var WaveformView = ({
5105
5487
  };
5106
5488
 
5107
5489
  // src/components/ScrollingWaveform.tsx
5108
- var import_react23 = require("react");
5109
- var import_jsx_runtime23 = require("react/jsx-runtime");
5490
+ var import_react24 = require("react");
5491
+ var import_jsx_runtime24 = require("react/jsx-runtime");
5110
5492
  var ScrollingWaveform = ({
5111
5493
  getPeakDb,
5112
5494
  active,
@@ -5114,11 +5496,11 @@ var ScrollingWaveform = ({
5114
5496
  className,
5115
5497
  fillStyle
5116
5498
  }) => {
5117
- const canvasRef = (0, import_react23.useRef)(null);
5118
- const ringRef = (0, import_react23.useRef)(new Float32Array(columns));
5119
- const writeIdxRef = (0, import_react23.useRef)(0);
5120
- const rafRef = (0, import_react23.useRef)(null);
5121
- (0, import_react23.useEffect)(() => {
5499
+ const canvasRef = (0, import_react24.useRef)(null);
5500
+ const ringRef = (0, import_react24.useRef)(new Float32Array(columns));
5501
+ const writeIdxRef = (0, import_react24.useRef)(0);
5502
+ const rafRef = (0, import_react24.useRef)(null);
5503
+ (0, import_react24.useEffect)(() => {
5122
5504
  if (ringRef.current.length !== columns) {
5123
5505
  const next = new Float32Array(columns);
5124
5506
  const prev = ringRef.current;
@@ -5130,7 +5512,7 @@ var ScrollingWaveform = ({
5130
5512
  writeIdxRef.current = writeIdxRef.current % columns;
5131
5513
  }
5132
5514
  }, [columns]);
5133
- (0, import_react23.useEffect)(() => {
5515
+ (0, import_react24.useEffect)(() => {
5134
5516
  if (!active) {
5135
5517
  if (rafRef.current !== null) {
5136
5518
  cancelAnimationFrame(rafRef.current);
@@ -5182,7 +5564,7 @@ var ScrollingWaveform = ({
5182
5564
  }
5183
5565
  };
5184
5566
  }, [active, getPeakDb, fillStyle]);
5185
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5567
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5186
5568
  "canvas",
5187
5569
  {
5188
5570
  ref: canvasRef,
@@ -5193,8 +5575,8 @@ var ScrollingWaveform = ({
5193
5575
  };
5194
5576
 
5195
5577
  // src/components/OffsetScrubber.tsx
5196
- var import_react24 = require("react");
5197
- var import_jsx_runtime24 = require("react/jsx-runtime");
5578
+ var import_react25 = require("react");
5579
+ var import_jsx_runtime25 = require("react/jsx-runtime");
5198
5580
  var SLIDER_HEIGHT_PX = 28;
5199
5581
  var TICK_HEIGHT_PX = 14;
5200
5582
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -5207,40 +5589,40 @@ function OffsetScrubber({
5207
5589
  onChange,
5208
5590
  disabled = false
5209
5591
  }) {
5210
- const trackRef = (0, import_react24.useRef)(null);
5211
- const [draftOffset, setDraftOffset] = (0, import_react24.useState)(offsetSamples);
5212
- const [isDragging, setIsDragging] = (0, import_react24.useState)(false);
5213
- (0, import_react24.useEffect)(() => {
5592
+ const trackRef = (0, import_react25.useRef)(null);
5593
+ const [draftOffset, setDraftOffset] = (0, import_react25.useState)(offsetSamples);
5594
+ const [isDragging, setIsDragging] = (0, import_react25.useState)(false);
5595
+ (0, import_react25.useEffect)(() => {
5214
5596
  if (!isDragging) setDraftOffset(offsetSamples);
5215
5597
  }, [offsetSamples, isDragging]);
5216
5598
  const sampleRate = cuePoints?.sample_rate ?? 44100;
5217
5599
  const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
5218
- const beatsForRange = (0, import_react24.useMemo)(() => {
5600
+ const beatsForRange = (0, import_react25.useMemo)(() => {
5219
5601
  return Math.round(60 / projectBpm * sampleRate);
5220
5602
  }, [projectBpm, sampleRate]);
5221
5603
  const rangeSamples = beatsForRange * meter;
5222
- const sampleToFraction = (0, import_react24.useCallback)(
5604
+ const sampleToFraction = (0, import_react25.useCallback)(
5223
5605
  (sample) => {
5224
5606
  const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
5225
5607
  return (clamped + rangeSamples) / (2 * rangeSamples);
5226
5608
  },
5227
5609
  [rangeSamples]
5228
5610
  );
5229
- const fractionToSample = (0, import_react24.useCallback)(
5611
+ const fractionToSample = (0, import_react25.useCallback)(
5230
5612
  (fraction) => {
5231
5613
  const clamped = Math.max(0, Math.min(1, fraction));
5232
5614
  return Math.round(clamped * 2 * rangeSamples - rangeSamples);
5233
5615
  },
5234
5616
  [rangeSamples]
5235
5617
  );
5236
- const snapTargets = (0, import_react24.useMemo)(() => {
5618
+ const snapTargets = (0, import_react25.useMemo)(() => {
5237
5619
  if (!cuePoints || cuePoints.beats.length === 0) return [];
5238
5620
  const downbeat = cuePoints.beats[0];
5239
5621
  const positives = cuePoints.beats.map((b) => b - downbeat);
5240
5622
  const negatives = positives.slice(1).map((p) => -p);
5241
5623
  return [...negatives, ...positives].sort((a, b) => a - b);
5242
5624
  }, [cuePoints]);
5243
- const snapToBeat = (0, import_react24.useCallback)(
5625
+ const snapToBeat = (0, import_react25.useCallback)(
5244
5626
  (sample) => {
5245
5627
  if (snapTargets.length === 0) return sample;
5246
5628
  let best = snapTargets[0];
@@ -5256,7 +5638,7 @@ function OffsetScrubber({
5256
5638
  },
5257
5639
  [snapTargets]
5258
5640
  );
5259
- const handlePointerDown = (0, import_react24.useCallback)(
5641
+ const handlePointerDown = (0, import_react25.useCallback)(
5260
5642
  (e) => {
5261
5643
  if (disabled || !cuePoints) return;
5262
5644
  e.preventDefault();
@@ -5290,7 +5672,7 @@ function OffsetScrubber({
5290
5672
  },
5291
5673
  [disabled, cuePoints, fractionToSample, onChange, snapToBeat]
5292
5674
  );
5293
- const handleResetToZero = (0, import_react24.useCallback)(() => {
5675
+ const handleResetToZero = (0, import_react25.useCallback)(() => {
5294
5676
  if (disabled) return;
5295
5677
  setDraftOffset(0);
5296
5678
  onChange(0);
@@ -5298,7 +5680,7 @@ function OffsetScrubber({
5298
5680
  const thumbFraction = sampleToFraction(draftOffset);
5299
5681
  const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
5300
5682
  const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
5301
- const ticks = (0, import_react24.useMemo)(() => {
5683
+ const ticks = (0, import_react25.useMemo)(() => {
5302
5684
  if (!cuePoints) return [];
5303
5685
  const downbeat = cuePoints.beats[0] ?? 0;
5304
5686
  return cuePoints.beats.map((b, i) => {
@@ -5309,9 +5691,9 @@ function OffsetScrubber({
5309
5691
  });
5310
5692
  }, [cuePoints, sampleToFraction]);
5311
5693
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
5312
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
5313
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
5314
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
5694
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
5695
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
5696
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
5315
5697
  "div",
5316
5698
  {
5317
5699
  ref: trackRef,
@@ -5327,7 +5709,7 @@ function OffsetScrubber({
5327
5709
  "aria-valuenow": draftOffset,
5328
5710
  "aria-disabled": isDisabled,
5329
5711
  children: [
5330
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5712
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5331
5713
  "div",
5332
5714
  {
5333
5715
  "aria-hidden": "true",
@@ -5335,7 +5717,7 @@ function OffsetScrubber({
5335
5717
  style: { left: "50%" }
5336
5718
  }
5337
5719
  ),
5338
- ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5720
+ ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5339
5721
  "div",
5340
5722
  {
5341
5723
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -5350,7 +5732,7 @@ function OffsetScrubber({
5350
5732
  },
5351
5733
  t.i
5352
5734
  )),
5353
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5735
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5354
5736
  "div",
5355
5737
  {
5356
5738
  "data-testid": "offset-scrubber-thumb",
@@ -5367,7 +5749,7 @@ function OffsetScrubber({
5367
5749
  ]
5368
5750
  }
5369
5751
  ),
5370
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5752
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5371
5753
  "span",
5372
5754
  {
5373
5755
  "data-testid": "offset-scrubber-readout",
@@ -5375,7 +5757,7 @@ function OffsetScrubber({
5375
5757
  children: formatOffset(draftOffset, sampleRate)
5376
5758
  }
5377
5759
  ),
5378
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5760
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5379
5761
  "button",
5380
5762
  {
5381
5763
  type: "button",
@@ -5387,7 +5769,7 @@ function OffsetScrubber({
5387
5769
  children: "\u2316"
5388
5770
  }
5389
5771
  ),
5390
- bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5772
+ bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5391
5773
  "span",
5392
5774
  {
5393
5775
  "data-testid": "offset-bpm-mismatch",
@@ -5459,16 +5841,16 @@ function synthesizeCuePoints({
5459
5841
  }
5460
5842
 
5461
5843
  // src/panel-core/useGeneratorPanelCore.tsx
5462
- var import_react29 = require("react");
5844
+ var import_react30 = require("react");
5463
5845
 
5464
5846
  // src/hooks/useSceneState.ts
5465
- var import_react25 = require("react");
5847
+ var import_react26 = require("react");
5466
5848
  function useSceneState(activeSceneId, initialValue) {
5467
- const [stateMap, setStateMap] = (0, import_react25.useState)(() => /* @__PURE__ */ new Map());
5468
- const activeSceneIdRef = (0, import_react25.useRef)(activeSceneId);
5849
+ const [stateMap, setStateMap] = (0, import_react26.useState)(() => /* @__PURE__ */ new Map());
5850
+ const activeSceneIdRef = (0, import_react26.useRef)(activeSceneId);
5469
5851
  activeSceneIdRef.current = activeSceneId;
5470
5852
  const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
5471
- const setForCurrentScene = (0, import_react25.useCallback)((value) => {
5853
+ const setForCurrentScene = (0, import_react26.useCallback)((value) => {
5472
5854
  const sid = activeSceneIdRef.current;
5473
5855
  if (sid === null) return;
5474
5856
  setStateMap((prev) => {
@@ -5479,7 +5861,7 @@ function useSceneState(activeSceneId, initialValue) {
5479
5861
  return newMap;
5480
5862
  });
5481
5863
  }, [initialValue]);
5482
- const setForScene = (0, import_react25.useCallback)((sceneId, value) => {
5864
+ const setForScene = (0, import_react26.useCallback)((sceneId, value) => {
5483
5865
  setStateMap((prev) => {
5484
5866
  const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
5485
5867
  const next = typeof value === "function" ? value(current) : value;
@@ -5492,10 +5874,10 @@ function useSceneState(activeSceneId, initialValue) {
5492
5874
  }
5493
5875
 
5494
5876
  // src/hooks/useAnySolo.ts
5495
- var import_react26 = require("react");
5877
+ var import_react27 = require("react");
5496
5878
  function useAnySolo(host) {
5497
- const [anySolo, setAnySolo] = (0, import_react26.useState)(false);
5498
- (0, import_react26.useEffect)(() => {
5879
+ const [anySolo, setAnySolo] = (0, import_react27.useState)(false);
5880
+ (0, import_react27.useEffect)(() => {
5499
5881
  let active = true;
5500
5882
  const refresh = () => {
5501
5883
  host.isAnySoloActive().then((v) => {
@@ -5514,7 +5896,7 @@ function useAnySolo(host) {
5514
5896
  }
5515
5897
 
5516
5898
  // src/hooks/useSoundHistory.ts
5517
- var import_react27 = require("react");
5899
+ var import_react28 = require("react");
5518
5900
  var EMPTY = { entries: [], cursor: -1 };
5519
5901
  function sameDescriptor(a, b) {
5520
5902
  if (a === b) return true;
@@ -5526,14 +5908,14 @@ function sameDescriptor(a, b) {
5526
5908
  }
5527
5909
  function useSoundHistory(applySound, opts = {}) {
5528
5910
  const max = Math.max(2, opts.max ?? 24);
5529
- const applyRef = (0, import_react27.useRef)(applySound);
5911
+ const applyRef = (0, import_react28.useRef)(applySound);
5530
5912
  applyRef.current = applySound;
5531
- const onChangeRef = (0, import_react27.useRef)(opts.onChange);
5913
+ const onChangeRef = (0, import_react28.useRef)(opts.onChange);
5532
5914
  onChangeRef.current = opts.onChange;
5533
- const dataRef = (0, import_react27.useRef)({});
5534
- const [, setVersion] = (0, import_react27.useState)(0);
5535
- const bump = (0, import_react27.useCallback)(() => setVersion((v) => v + 1), []);
5536
- const commit = (0, import_react27.useCallback)(
5915
+ const dataRef = (0, import_react28.useRef)({});
5916
+ const [, setVersion] = (0, import_react28.useState)(0);
5917
+ const bump = (0, import_react28.useCallback)(() => setVersion((v) => v + 1), []);
5918
+ const commit = (0, import_react28.useCallback)(
5537
5919
  (trackId, next, notify) => {
5538
5920
  dataRef.current = { ...dataRef.current, [trackId]: next };
5539
5921
  bump();
@@ -5541,7 +5923,7 @@ function useSoundHistory(applySound, opts = {}) {
5541
5923
  },
5542
5924
  [bump]
5543
5925
  );
5544
- const record = (0, import_react27.useCallback)(
5926
+ const record = (0, import_react28.useCallback)(
5545
5927
  (trackId, descriptor, label) => {
5546
5928
  const h = dataRef.current[trackId];
5547
5929
  const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
@@ -5556,7 +5938,7 @@ function useSoundHistory(applySound, opts = {}) {
5556
5938
  },
5557
5939
  [max, commit]
5558
5940
  );
5559
- const restoreTo = (0, import_react27.useCallback)(
5941
+ const restoreTo = (0, import_react28.useCallback)(
5560
5942
  async (trackId, index) => {
5561
5943
  const h = dataRef.current[trackId];
5562
5944
  if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
@@ -5566,7 +5948,7 @@ function useSoundHistory(applySound, opts = {}) {
5566
5948
  },
5567
5949
  [commit]
5568
5950
  );
5569
- const undo = (0, import_react27.useCallback)(
5951
+ const undo = (0, import_react28.useCallback)(
5570
5952
  (trackId) => {
5571
5953
  const h = dataRef.current[trackId];
5572
5954
  if (!h || h.cursor <= 0) return Promise.resolve(false);
@@ -5574,7 +5956,7 @@ function useSoundHistory(applySound, opts = {}) {
5574
5956
  },
5575
5957
  [restoreTo]
5576
5958
  );
5577
- const toggleFavorite = (0, import_react27.useCallback)(
5959
+ const toggleFavorite = (0, import_react28.useCallback)(
5578
5960
  (trackId, index) => {
5579
5961
  const h = dataRef.current[trackId];
5580
5962
  if (!h || index < 0 || index >= h.entries.length) return;
@@ -5583,7 +5965,7 @@ function useSoundHistory(applySound, opts = {}) {
5583
5965
  },
5584
5966
  [commit]
5585
5967
  );
5586
- const restore = (0, import_react27.useCallback)(
5968
+ const restore = (0, import_react28.useCallback)(
5587
5969
  (trackId, state) => {
5588
5970
  const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
5589
5971
  const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
@@ -5592,15 +5974,15 @@ function useSoundHistory(applySound, opts = {}) {
5592
5974
  },
5593
5975
  [commit]
5594
5976
  );
5595
- const list = (0, import_react27.useCallback)(
5977
+ const list = (0, import_react28.useCallback)(
5596
5978
  (trackId) => dataRef.current[trackId] ?? EMPTY,
5597
5979
  []
5598
5980
  );
5599
- const canUndo = (0, import_react27.useCallback)((trackId) => {
5981
+ const canUndo = (0, import_react28.useCallback)((trackId) => {
5600
5982
  const h = dataRef.current[trackId];
5601
5983
  return !!h && h.cursor > 0;
5602
5984
  }, []);
5603
- const clear = (0, import_react27.useCallback)(
5985
+ const clear = (0, import_react28.useCallback)(
5604
5986
  (trackId) => {
5605
5987
  if (dataRef.current[trackId]) {
5606
5988
  const next = { ...dataRef.current };
@@ -5612,11 +5994,11 @@ function useSoundHistory(applySound, opts = {}) {
5612
5994
  },
5613
5995
  [bump]
5614
5996
  );
5615
- const reset = (0, import_react27.useCallback)(() => {
5997
+ const reset = (0, import_react28.useCallback)(() => {
5616
5998
  dataRef.current = {};
5617
5999
  bump();
5618
6000
  }, [bump]);
5619
- return (0, import_react27.useMemo)(
6001
+ return (0, import_react28.useMemo)(
5620
6002
  () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
5621
6003
  [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
5622
6004
  );
@@ -5749,7 +6131,7 @@ function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
5749
6131
  }
5750
6132
 
5751
6133
  // src/panel-core/useTransitionOps.ts
5752
- var import_react28 = require("react");
6134
+ var import_react29 = require("react");
5753
6135
  function useTransitionOps({
5754
6136
  host,
5755
6137
  adapter,
@@ -5766,8 +6148,8 @@ function useTransitionOps({
5766
6148
  resolvedFades
5767
6149
  }) {
5768
6150
  const { identity } = adapter;
5769
- const appliedFadeAutomationRef = (0, import_react28.useRef)(/* @__PURE__ */ new Set());
5770
- const applyCrossfadeAutomation = (0, import_react28.useCallback)(
6151
+ const appliedFadeAutomationRef = (0, import_react29.useRef)(/* @__PURE__ */ new Set());
6152
+ const applyCrossfadeAutomation = (0, import_react29.useCallback)(
5771
6153
  async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
5772
6154
  if (host.setTrackVolumeAutomation) {
5773
6155
  const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
@@ -5784,7 +6166,7 @@ function useTransitionOps({
5784
6166
  },
5785
6167
  [host]
5786
6168
  );
5787
- const applyFadeAutomation = (0, import_react28.useCallback)(
6169
+ const applyFadeAutomation = (0, import_react29.useCallback)(
5788
6170
  async (trackId, direction, bars, bpm, sliderPos, gesture) => {
5789
6171
  if (!host.setTrackVolumeAutomation) return;
5790
6172
  const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
@@ -5793,8 +6175,25 @@ function useTransitionOps({
5793
6175
  },
5794
6176
  [host]
5795
6177
  );
5796
- const [isCreatingCrossfade, setIsCreatingCrossfade] = (0, import_react28.useState)(false);
5797
- const handleCreateCrossfade = (0, import_react28.useCallback)(
6178
+ const copyTrackFx = (0, import_react29.useCallback)(
6179
+ async (newTrackId, sourceDbId) => {
6180
+ if (typeof host.copyTrackFxFrom !== "function") return;
6181
+ try {
6182
+ const res = await host.copyTrackFxFrom(newTrackId, sourceDbId);
6183
+ if (res.externalMissing.length > 0) {
6184
+ host.showToast(
6185
+ "warning",
6186
+ "Some FX not copied",
6187
+ `Missing plugin(s): ${res.externalMissing.join(", ")}`
6188
+ );
6189
+ }
6190
+ } catch {
6191
+ }
6192
+ },
6193
+ [host]
6194
+ );
6195
+ const [isCreatingCrossfade, setIsCreatingCrossfade] = (0, import_react29.useState)(false);
6196
+ const handleCreateCrossfade = (0, import_react29.useCallback)(
5798
6197
  async (origin, target) => {
5799
6198
  const scene = activeSceneId;
5800
6199
  const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
@@ -5871,6 +6270,8 @@ function useTransitionOps({
5871
6270
  };
5872
6271
  const originLabel = await copySound(top.id, origin.dbId);
5873
6272
  const targetLabel = await copySound(bottom.id, target.dbId);
6273
+ await copyTrackFx(top.id, origin.dbId);
6274
+ await copyTrackFx(bottom.id, target.dbId);
5874
6275
  await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
5875
6276
  const groupId = top.dbId;
5876
6277
  const originMeta = {
@@ -5922,8 +6323,9 @@ function useTransitionOps({
5922
6323
  loadTracks
5923
6324
  ]
5924
6325
  );
5925
- const [isCreatingFade, setIsCreatingFade] = (0, import_react28.useState)(false);
5926
- const handleCreateFade = (0, import_react28.useCallback)(
6326
+ const [isCreatingGroupFade, setIsCreatingGroupFade] = (0, import_react29.useState)(false);
6327
+ const [isCreatingFade, setIsCreatingFade] = (0, import_react29.useState)(false);
6328
+ const handleCreateFade = (0, import_react29.useCallback)(
5927
6329
  async (selection, direction, gesture) => {
5928
6330
  const scene = activeSceneId;
5929
6331
  const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
@@ -5990,6 +6392,7 @@ function useTransitionOps({
5990
6392
  soundLabel = await adapter.sound.copySnapshot(track.id, snap);
5991
6393
  }
5992
6394
  }
6395
+ await copyTrackFx(track.id, selection.dbId);
5993
6396
  await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
5994
6397
  appliedFadeAutomationRef.current.add(track.id);
5995
6398
  const meta = {
@@ -6033,7 +6436,7 @@ function useTransitionOps({
6033
6436
  loadTracks
6034
6437
  ]
6035
6438
  );
6036
- const handleCrossfadeMute = (0, import_react28.useCallback)(
6439
+ const handleCrossfadeMute = (0, import_react29.useCallback)(
6037
6440
  (pair) => {
6038
6441
  const newMuted = !pair.origin.runtimeState.muted;
6039
6442
  for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
@@ -6048,7 +6451,7 @@ function useTransitionOps({
6048
6451
  },
6049
6452
  [host, setTracks]
6050
6453
  );
6051
- const handleCrossfadeSolo = (0, import_react28.useCallback)(
6454
+ const handleCrossfadeSolo = (0, import_react29.useCallback)(
6052
6455
  (pair) => {
6053
6456
  const newSolo = !pair.origin.runtimeState.solo;
6054
6457
  for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
@@ -6063,7 +6466,7 @@ function useTransitionOps({
6063
6466
  },
6064
6467
  [host, setTracks]
6065
6468
  );
6066
- const handleCrossfadeDelete = (0, import_react28.useCallback)(
6469
+ const handleCrossfadeDelete = (0, import_react29.useCallback)(
6067
6470
  async (pair) => {
6068
6471
  try {
6069
6472
  for (const member of [pair.origin, pair.target]) {
@@ -6089,8 +6492,8 @@ function useTransitionOps({
6089
6492
  },
6090
6493
  [host, activeSceneId, setCrossfadePairsMeta, setTracks]
6091
6494
  );
6092
- const crossfadeSliderTimers = (0, import_react28.useRef)({});
6093
- const handleCrossfadeSlider = (0, import_react28.useCallback)(
6495
+ const crossfadeSliderTimers = (0, import_react29.useRef)({});
6496
+ const handleCrossfadeSlider = (0, import_react29.useCallback)(
6094
6497
  (pair, pos) => {
6095
6498
  setCrossfadePairsMeta(
6096
6499
  (prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
@@ -6123,7 +6526,7 @@ function useTransitionOps({
6123
6526
  },
6124
6527
  [host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
6125
6528
  );
6126
- const handleFadeDelete = (0, import_react28.useCallback)(
6529
+ const handleFadeDelete = (0, import_react29.useCallback)(
6127
6530
  async (fade) => {
6128
6531
  try {
6129
6532
  await host.deleteTrack(fade.track.handle.id);
@@ -6143,8 +6546,8 @@ function useTransitionOps({
6143
6546
  },
6144
6547
  [host, activeSceneId, setFadesMeta, setTracks]
6145
6548
  );
6146
- const fadeSliderTimers = (0, import_react28.useRef)({});
6147
- const handleFadeSlider = (0, import_react28.useCallback)(
6549
+ const fadeSliderTimers = (0, import_react29.useRef)({});
6550
+ const handleFadeSlider = (0, import_react29.useCallback)(
6148
6551
  (fade, pos) => {
6149
6552
  setFadesMeta(
6150
6553
  (prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
@@ -6174,8 +6577,190 @@ function useTransitionOps({
6174
6577
  },
6175
6578
  [host, activeSceneId, applyFadeAutomation, setFadesMeta]
6176
6579
  );
6177
- const lastResyncKeyRef = (0, import_react28.useRef)("");
6178
- (0, import_react28.useEffect)(() => {
6580
+ const handleCreateVerbatimGroupFade = (0, import_react29.useCallback)(
6581
+ async (subject, direction) => {
6582
+ const groupAdapter = adapter.transitionGroup;
6583
+ if (!groupAdapter) throw new Error("This panel does not support group fades.");
6584
+ const scene = activeSceneId;
6585
+ const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
6586
+ const toSceneId = sceneContext?.transitionToSceneId ?? "";
6587
+ if (!scene) throw new Error("No active scene.");
6588
+ if (!isConnected) throw new Error("Systems not connected.");
6589
+ const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
6590
+ const members = await groupAdapter.expandSubject(sourceSceneId, subject.dbId);
6591
+ if (members.length === 0) {
6592
+ throw new Error("Nothing to fade \u2014 the source group has no members.");
6593
+ }
6594
+ if (tracks.length + members.length > identity.maxTracks) {
6595
+ throw new Error(`Not enough track slots for this fade (needs ${members.length}).`);
6596
+ }
6597
+ setIsCreatingGroupFade(true);
6598
+ const created = [];
6599
+ try {
6600
+ const mc = await host.getMusicalContext();
6601
+ const sliderPos = groupAdapter.defaultSliderPos?.(direction) ?? 0.5;
6602
+ const gesture = "volume";
6603
+ const clipEnd = mc.bars * 4 * 60 / mc.bpm;
6604
+ const maxBeats = mc.bars * 4;
6605
+ for (const member of members) {
6606
+ const handle = await host.createTrack({
6607
+ name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}-v${member.memberIndex}`,
6608
+ ...adapter.createTrackOptions()
6609
+ });
6610
+ created.push({ handle, member, soundLabel: "default" });
6611
+ const role = member.role ?? subject.role ?? "";
6612
+ if (role) await host.setTrackRole(handle.id, role).catch(() => {
6613
+ });
6614
+ const srcMidi = host.readImportableTrackMidi ? await host.readImportableTrackMidi(member.dbId) : { clips: [] };
6615
+ const srcNotes = srcMidi.clips[0]?.notes ?? [];
6616
+ const notes = srcNotes.filter((n) => n.startBeat < maxBeats).map((n) => ({
6617
+ ...n,
6618
+ durationBeats: Math.min(n.durationBeats, maxBeats - n.startBeat)
6619
+ }));
6620
+ if (notes.length > 0) {
6621
+ const clip = { startTime: 0, endTime: clipEnd, tempo: mc.bpm, notes };
6622
+ await host.writeMidiClip(handle.id, clip);
6623
+ }
6624
+ if (host.getTrackSound) {
6625
+ const snap = await host.getTrackSound(member.dbId);
6626
+ if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
6627
+ created[created.length - 1].soundLabel = await adapter.sound.copySnapshot(
6628
+ handle.id,
6629
+ snap
6630
+ );
6631
+ }
6632
+ }
6633
+ await copyTrackFx(handle.id, member.dbId);
6634
+ await applyFadeAutomation(handle.id, direction, mc.bars, mc.bpm, sliderPos, gesture);
6635
+ appliedFadeAutomationRef.current.add(handle.id);
6636
+ }
6637
+ const groupId = created[0].handle.dbId;
6638
+ for (const { handle, member, soundLabel } of created) {
6639
+ const meta = {
6640
+ direction,
6641
+ gesture,
6642
+ sourceTrackDbId: member.dbId,
6643
+ sourceSceneId,
6644
+ sourceName: member.name,
6645
+ soundLabel,
6646
+ sliderPos,
6647
+ groupId,
6648
+ memberIndex: member.memberIndex,
6649
+ memberLabel: member.memberLabel
6650
+ };
6651
+ await host.setSceneData(scene, `track:${handle.dbId}:fade`, meta);
6652
+ }
6653
+ await groupAdapter.writeGroupMetas(
6654
+ scene,
6655
+ created.map(({ handle, member }) => ({ newDbId: handle.dbId, member })),
6656
+ groupId
6657
+ );
6658
+ await loadTracks(true);
6659
+ host.showToast(
6660
+ "success",
6661
+ direction === "in" ? "Fade in created" : "Fade out created",
6662
+ subject.name
6663
+ );
6664
+ } catch (err) {
6665
+ for (const { handle } of [...created].reverse()) {
6666
+ try {
6667
+ await host.deleteTrack(handle.id);
6668
+ } catch {
6669
+ }
6670
+ await host.deleteSceneData(scene, `track:${handle.dbId}:fade`).catch(() => {
6671
+ });
6672
+ for (const suffix of groupAdapter.cleanupKeySuffixes) {
6673
+ await host.deleteSceneData(scene, `track:${handle.dbId}:${suffix}`).catch(() => {
6674
+ });
6675
+ }
6676
+ }
6677
+ throw err instanceof Error ? err : new Error(String(err));
6678
+ } finally {
6679
+ setIsCreatingGroupFade(false);
6680
+ }
6681
+ },
6682
+ [
6683
+ host,
6684
+ adapter,
6685
+ identity,
6686
+ activeSceneId,
6687
+ isConnected,
6688
+ tracks.length,
6689
+ sceneContext,
6690
+ applyFadeAutomation,
6691
+ copyTrackFx,
6692
+ loadTracks
6693
+ ]
6694
+ );
6695
+ const groupFadeSliderTimers = (0, import_react29.useRef)({});
6696
+ const handleGroupFadeSlider = (0, import_react29.useCallback)(
6697
+ (group, pos) => {
6698
+ setFadesMeta(
6699
+ (prev) => prev.map(
6700
+ (f) => f.meta.groupId === group.groupId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f
6701
+ )
6702
+ );
6703
+ if (groupFadeSliderTimers.current[group.groupId]) {
6704
+ clearTimeout(groupFadeSliderTimers.current[group.groupId]);
6705
+ }
6706
+ groupFadeSliderTimers.current[group.groupId] = setTimeout(() => {
6707
+ void (async () => {
6708
+ const mc = await host.getMusicalContext();
6709
+ const sceneData = activeSceneId ? await host.getAllSceneData(activeSceneId) : null;
6710
+ for (const member of group.members) {
6711
+ await applyFadeAutomation(
6712
+ member.track.handle.id,
6713
+ group.direction,
6714
+ mc.bars,
6715
+ mc.bpm,
6716
+ pos,
6717
+ group.gesture
6718
+ );
6719
+ if (activeSceneId && sceneData) {
6720
+ const meta = asFadeMeta(sceneData[`track:${member.dbId}:fade`]);
6721
+ if (meta) {
6722
+ host.setSceneData(activeSceneId, `track:${member.dbId}:fade`, {
6723
+ ...meta,
6724
+ sliderPos: pos
6725
+ }).catch(() => {
6726
+ });
6727
+ }
6728
+ }
6729
+ }
6730
+ })();
6731
+ }, 200);
6732
+ },
6733
+ [host, activeSceneId, applyFadeAutomation, setFadesMeta]
6734
+ );
6735
+ const handleGroupFadeDelete = (0, import_react29.useCallback)(
6736
+ async (group) => {
6737
+ const suffixes = ["fade", ...adapter.transitionGroup?.cleanupKeySuffixes ?? []];
6738
+ try {
6739
+ for (const member of group.members) {
6740
+ await host.deleteTrack(member.track.handle.id);
6741
+ if (activeSceneId) {
6742
+ for (const suffix of suffixes) {
6743
+ await host.deleteSceneData(activeSceneId, `track:${member.dbId}:${suffix}`).catch(() => {
6744
+ });
6745
+ }
6746
+ }
6747
+ }
6748
+ setFadesMeta((prev) => prev.filter((f) => f.meta.groupId !== group.groupId));
6749
+ const gone = new Set(group.members.map((m) => m.track.handle.id));
6750
+ setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
6751
+ host.showToast("success", "Fade removed");
6752
+ } catch (err) {
6753
+ host.showToast(
6754
+ "error",
6755
+ "Failed to delete fade",
6756
+ err instanceof Error ? err.message : String(err)
6757
+ );
6758
+ }
6759
+ },
6760
+ [host, adapter, activeSceneId, setFadesMeta, setTracks]
6761
+ );
6762
+ const lastResyncKeyRef = (0, import_react29.useRef)("");
6763
+ (0, import_react29.useEffect)(() => {
6179
6764
  if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
6180
6765
  return;
6181
6766
  }
@@ -6216,7 +6801,7 @@ function useTransitionOps({
6216
6801
  cancelled = true;
6217
6802
  };
6218
6803
  }, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
6219
- (0, import_react28.useEffect)(() => {
6804
+ (0, import_react29.useEffect)(() => {
6220
6805
  if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
6221
6806
  void (async () => {
6222
6807
  const mc = await host.getMusicalContext();
@@ -6245,12 +6830,16 @@ function useTransitionOps({
6245
6830
  handleCrossfadeDelete,
6246
6831
  handleCrossfadeSlider,
6247
6832
  handleFadeDelete,
6248
- handleFadeSlider
6833
+ handleFadeSlider,
6834
+ isCreatingGroupFade,
6835
+ handleCreateVerbatimGroupFade,
6836
+ handleGroupFadeSlider,
6837
+ handleGroupFadeDelete
6249
6838
  };
6250
6839
  }
6251
6840
 
6252
6841
  // src/panel-core/useGeneratorPanelCore.tsx
6253
- var import_jsx_runtime25 = require("react/jsx-runtime");
6842
+ var import_jsx_runtime26 = require("react/jsx-runtime");
6254
6843
  var EMPTY_PLACEHOLDERS = [];
6255
6844
  function useGeneratorPanelCore({
6256
6845
  ui,
@@ -6270,8 +6859,8 @@ function useGeneratorPanelCore({
6270
6859
  } = ui;
6271
6860
  const { identity, features } = adapter;
6272
6861
  const logTag = identity.logTag;
6273
- const adapterRef = (0, import_react29.useRef)(adapter);
6274
- (0, import_react29.useEffect)(() => {
6862
+ const adapterRef = (0, import_react30.useRef)(adapter);
6863
+ (0, import_react30.useEffect)(() => {
6275
6864
  if (adapterRef.current !== adapter) {
6276
6865
  adapterRef.current = adapter;
6277
6866
  console.warn(
@@ -6281,27 +6870,27 @@ function useGeneratorPanelCore({
6281
6870
  }, [adapter, logTag]);
6282
6871
  const supportsMeters = typeof host.getTrackLevels === "function";
6283
6872
  const trackLevels = useTrackLevels(host, isExpanded);
6284
- const [tracks, setTracks] = (0, import_react29.useState)([]);
6285
- const [isLoadingTracks, setIsLoadingTracks] = (0, import_react29.useState)(false);
6286
- const [importOpen, setImportOpen] = (0, import_react29.useState)(false);
6287
- const [soundImportTarget, setSoundImportTarget] = (0, import_react29.useState)(null);
6288
- const [designerView, setDesignerView] = (0, import_react29.useState)(false);
6289
- const [transitionSourceTotal, setTransitionSourceTotal] = (0, import_react29.useState)(0);
6290
- const [crossfadePairsMeta, setCrossfadePairsMeta] = (0, import_react29.useState)([]);
6291
- const [fadesMeta, setFadesMeta] = (0, import_react29.useState)([]);
6292
- const [genericGroupMetas, setGenericGroupMetas] = (0, import_react29.useState)({});
6873
+ const [tracks, setTracks] = (0, import_react30.useState)([]);
6874
+ const [isLoadingTracks, setIsLoadingTracks] = (0, import_react30.useState)(false);
6875
+ const [importOpen, setImportOpen] = (0, import_react30.useState)(false);
6876
+ const [soundImportTarget, setSoundImportTarget] = (0, import_react30.useState)(null);
6877
+ const [designerView, setDesignerView] = (0, import_react30.useState)(false);
6878
+ const [transitionSourceTotal, setTransitionSourceTotal] = (0, import_react30.useState)(0);
6879
+ const [crossfadePairsMeta, setCrossfadePairsMeta] = (0, import_react30.useState)([]);
6880
+ const [fadesMeta, setFadesMeta] = (0, import_react30.useState)([]);
6881
+ const [genericGroupMetas, setGenericGroupMetas] = (0, import_react30.useState)({});
6293
6882
  const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
6294
6883
  const [placeholders, , setPlaceholdersForScene] = useSceneState(
6295
6884
  activeSceneId,
6296
6885
  EMPTY_PLACEHOLDERS
6297
6886
  );
6298
- const saveTimeoutRefs = (0, import_react29.useRef)({});
6299
- const editLoadStartedRef = (0, import_react29.useRef)(/* @__PURE__ */ new Set());
6300
- const [availableInstruments, setAvailableInstruments] = (0, import_react29.useState)([]);
6301
- const [instrumentsLoading, setInstrumentsLoading] = (0, import_react29.useState)(false);
6302
- const engineToDbIdRef = (0, import_react29.useRef)(/* @__PURE__ */ new Map());
6303
- const tracksLoadedForSceneRef = (0, import_react29.useRef)(null);
6304
- const persistSoundHistory = (0, import_react29.useCallback)(
6887
+ const saveTimeoutRefs = (0, import_react30.useRef)({});
6888
+ const editLoadStartedRef = (0, import_react30.useRef)(/* @__PURE__ */ new Set());
6889
+ const [availableInstruments, setAvailableInstruments] = (0, import_react30.useState)([]);
6890
+ const [instrumentsLoading, setInstrumentsLoading] = (0, import_react30.useState)(false);
6891
+ const engineToDbIdRef = (0, import_react30.useRef)(/* @__PURE__ */ new Map());
6892
+ const tracksLoadedForSceneRef = (0, import_react30.useRef)(null);
6893
+ const persistSoundHistory = (0, import_react30.useCallback)(
6305
6894
  (trackId, state) => {
6306
6895
  if (!activeSceneId) return;
6307
6896
  const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
@@ -6321,7 +6910,7 @@ function useGeneratorPanelCore({
6321
6910
  setItems: setTracks,
6322
6911
  getId: (t) => t.handle.dbId
6323
6912
  });
6324
- const loadTracks = (0, import_react29.useCallback)(
6913
+ const loadTracks = (0, import_react30.useCallback)(
6325
6914
  async (incremental = false) => {
6326
6915
  const sceneAtStart = activeSceneId;
6327
6916
  if (!sceneAtStart) {
@@ -6446,18 +7035,18 @@ function useGeneratorPanelCore({
6446
7035
  },
6447
7036
  [host, activeSceneId, soundHistory, adapter, logTag]
6448
7037
  );
6449
- (0, import_react29.useEffect)(() => {
7038
+ (0, import_react30.useEffect)(() => {
6450
7039
  loadTracks();
6451
7040
  }, [loadTracks]);
6452
- (0, import_react29.useEffect)(() => {
7041
+ (0, import_react30.useEffect)(() => {
6453
7042
  const map = /* @__PURE__ */ new Map();
6454
7043
  for (const t of tracks) {
6455
7044
  map.set(t.handle.id, t.handle.dbId);
6456
7045
  }
6457
7046
  engineToDbIdRef.current = map;
6458
7047
  }, [tracks]);
6459
- const loadedCompletedIdsRef = (0, import_react29.useRef)(/* @__PURE__ */ new Set());
6460
- (0, import_react29.useEffect)(() => {
7048
+ const loadedCompletedIdsRef = (0, import_react30.useRef)(/* @__PURE__ */ new Set());
7049
+ (0, import_react30.useEffect)(() => {
6461
7050
  if (placeholders.length === 0) {
6462
7051
  loadedCompletedIdsRef.current.clear();
6463
7052
  return;
@@ -6476,16 +7065,16 @@ function useGeneratorPanelCore({
6476
7065
  loadTracks(true);
6477
7066
  }
6478
7067
  }, [placeholders, loadTracks, logTag]);
6479
- const adoptAndLoad = (0, import_react29.useCallback)(() => {
7068
+ const adoptAndLoad = (0, import_react30.useCallback)(() => {
6480
7069
  loadTracks(true);
6481
7070
  }, [loadTracks]);
6482
- (0, import_react29.useEffect)(() => {
7071
+ (0, import_react30.useEffect)(() => {
6483
7072
  const unsub = host.onEngineReady(() => {
6484
7073
  adoptAndLoad();
6485
7074
  });
6486
7075
  return unsub;
6487
7076
  }, [host, adoptAndLoad]);
6488
- (0, import_react29.useEffect)(() => {
7077
+ (0, import_react30.useEffect)(() => {
6489
7078
  if (typeof host.onAfterAgentMutation !== "function") return;
6490
7079
  let timer = null;
6491
7080
  const unsub = host.onAfterAgentMutation(() => {
@@ -6500,13 +7089,13 @@ function useGeneratorPanelCore({
6500
7089
  if (timer) clearTimeout(timer);
6501
7090
  };
6502
7091
  }, [host, loadTracks]);
6503
- (0, import_react29.useEffect)(() => {
7092
+ (0, import_react30.useEffect)(() => {
6504
7093
  const unsub = host.onTrackStateChange((trackId, state) => {
6505
7094
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
6506
7095
  });
6507
7096
  return unsub;
6508
7097
  }, [host]);
6509
- (0, import_react29.useEffect)(() => {
7098
+ (0, import_react30.useEffect)(() => {
6510
7099
  if (!features.bulkComposePlaceholders) return;
6511
7100
  console.log(`[${logTag}] Subscribing to composeProgress`);
6512
7101
  const unsub = host.onComposeProgress((event) => {
@@ -6540,7 +7129,7 @@ function useGeneratorPanelCore({
6540
7129
  });
6541
7130
  return unsub;
6542
7131
  }, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
6543
- (0, import_react29.useEffect)(() => {
7132
+ (0, import_react30.useEffect)(() => {
6544
7133
  const refs = saveTimeoutRefs;
6545
7134
  return () => {
6546
7135
  for (const timeout of Object.values(refs.current)) {
@@ -6548,9 +7137,9 @@ function useGeneratorPanelCore({
6548
7137
  }
6549
7138
  };
6550
7139
  }, []);
6551
- const isAddingTrackRef = (0, import_react29.useRef)(false);
6552
- const [isAddingTrack, setIsAddingTrack] = (0, import_react29.useState)(false);
6553
- const handleAddTrack = (0, import_react29.useCallback)(async () => {
7140
+ const isAddingTrackRef = (0, import_react30.useRef)(false);
7141
+ const [isAddingTrack, setIsAddingTrack] = (0, import_react30.useState)(false);
7142
+ const handleAddTrack = (0, import_react30.useCallback)(async () => {
6554
7143
  if (isAddingTrackRef.current) return;
6555
7144
  if (!activeSceneId) {
6556
7145
  host.showToast("warning", "Select SCENE");
@@ -6590,7 +7179,7 @@ function useGeneratorPanelCore({
6590
7179
  setIsAddingTrack(false);
6591
7180
  }
6592
7181
  }, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
6593
- const handlePortTrack = (0, import_react29.useCallback)(
7182
+ const handlePortTrack = (0, import_react30.useCallback)(
6594
7183
  async (sel) => {
6595
7184
  if (!activeSceneId) {
6596
7185
  host.showToast("warning", "Select SCENE");
@@ -6647,7 +7236,7 @@ function useGeneratorPanelCore({
6647
7236
  },
6648
7237
  [host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
6649
7238
  );
6650
- const handleSoundImportPick = (0, import_react29.useCallback)(
7239
+ const handleSoundImportPick = (0, import_react30.useCallback)(
6651
7240
  async (sel) => {
6652
7241
  const target = soundImportTarget;
6653
7242
  if (!target || !host.getTrackSound) {
@@ -6678,8 +7267,8 @@ function useGeneratorPanelCore({
6678
7267
  },
6679
7268
  [soundImportTarget, host, adapter, identity.familyKey, soundHistory]
6680
7269
  );
6681
- const [isExportingMidi, setIsExportingMidi] = (0, import_react29.useState)(false);
6682
- const handleExportMidi = (0, import_react29.useCallback)(async () => {
7270
+ const [isExportingMidi, setIsExportingMidi] = (0, import_react30.useState)(false);
7271
+ const handleExportMidi = (0, import_react30.useCallback)(async () => {
6683
7272
  if (isExportingMidi) return;
6684
7273
  setIsExportingMidi(true);
6685
7274
  try {
@@ -6710,10 +7299,10 @@ function useGeneratorPanelCore({
6710
7299
  const xfFromId = sceneContext?.transitionFromSceneId ?? null;
6711
7300
  const xfToId = sceneContext?.transitionToSceneId ?? null;
6712
7301
  const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
6713
- (0, import_react29.useEffect)(() => {
7302
+ (0, import_react30.useEffect)(() => {
6714
7303
  if (!canCrossfade) setDesignerView(false);
6715
7304
  }, [canCrossfade]);
6716
- (0, import_react29.useEffect)(() => {
7305
+ (0, import_react30.useEffect)(() => {
6717
7306
  if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
6718
7307
  setTransitionSourceTotal(0);
6719
7308
  return;
@@ -6729,12 +7318,12 @@ function useGeneratorPanelCore({
6729
7318
  };
6730
7319
  }, [canCrossfade, xfFromId, xfToId, host]);
6731
7320
  const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
6732
- (0, import_react29.useEffect)(() => {
7321
+ (0, import_react30.useEffect)(() => {
6733
7322
  if (!onHeaderContent) return;
6734
7323
  const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
6735
7324
  onHeaderContent(
6736
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex gap-1 items-center", children: [
6737
- features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
7325
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex gap-1 items-center", children: [
7326
+ features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
6738
7327
  "button",
6739
7328
  {
6740
7329
  "data-testid": `import-from-scene-${identity.familyKey}-button`,
@@ -6748,7 +7337,7 @@ function useGeneratorPanelCore({
6748
7337
  children: identity.importTrackLabel ?? "Import Track"
6749
7338
  }
6750
7339
  ),
6751
- (!canCrossfade || !designerView) && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
7340
+ (!canCrossfade || !designerView) && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
6752
7341
  "button",
6753
7342
  {
6754
7343
  "data-testid": `add-${identity.familyKey}-track-button`,
@@ -6764,7 +7353,7 @@ function useGeneratorPanelCore({
6764
7353
  children: identity.addTrackLabel ?? "Add Track"
6765
7354
  }
6766
7355
  ),
6767
- canCrossfade && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
7356
+ canCrossfade && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
6768
7357
  "button",
6769
7358
  {
6770
7359
  "data-testid": `${identity.familyKey}-view-toggle`,
@@ -6783,7 +7372,7 @@ function useGeneratorPanelCore({
6783
7372
  title: designerView ? "Back to the track list" : "Open the transition designer",
6784
7373
  className: "relative overflow-hidden px-2 py-0.5 text-[10px] font-medium rounded-sm border border-sas-accent/40 text-sas-accent transition-colors hover:border-sas-accent disabled:opacity-50",
6785
7374
  children: [
6786
- transitionSourceTotal > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
7375
+ transitionSourceTotal > 0 && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
6787
7376
  "span",
6788
7377
  {
6789
7378
  className: "absolute inset-y-0 left-0 bg-sas-accent/25",
@@ -6791,7 +7380,7 @@ function useGeneratorPanelCore({
6791
7380
  "aria-hidden": true
6792
7381
  }
6793
7382
  ),
6794
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "relative", children: [
7383
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "relative", children: [
6795
7384
  "\u21C4 ",
6796
7385
  designerView ? "Transition" : "Tracks",
6797
7386
  transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
@@ -6822,7 +7411,7 @@ function useGeneratorPanelCore({
6822
7411
  identity,
6823
7412
  features.importTracks
6824
7413
  ]);
6825
- (0, import_react29.useEffect)(() => {
7414
+ (0, import_react30.useEffect)(() => {
6826
7415
  if (!onLoading) return;
6827
7416
  const anyGenerating = tracks.some((t) => t.isGenerating);
6828
7417
  onLoading(isLoadingTracks || anyGenerating || isBulkActive);
@@ -6830,7 +7419,7 @@ function useGeneratorPanelCore({
6830
7419
  onLoading(false);
6831
7420
  };
6832
7421
  }, [onLoading, isLoadingTracks, tracks, isBulkActive]);
6833
- const handleDeleteTrack = (0, import_react29.useCallback)(
7422
+ const handleDeleteTrack = (0, import_react30.useCallback)(
6834
7423
  async (trackId) => {
6835
7424
  try {
6836
7425
  await host.deleteTrack(trackId);
@@ -6846,7 +7435,7 @@ function useGeneratorPanelCore({
6846
7435
  },
6847
7436
  [host, activeSceneId]
6848
7437
  );
6849
- const handlePromptChange = (0, import_react29.useCallback)(
7438
+ const handlePromptChange = (0, import_react30.useCallback)(
6850
7439
  (trackId, prompt) => {
6851
7440
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
6852
7441
  const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
@@ -6862,7 +7451,7 @@ function useGeneratorPanelCore({
6862
7451
  },
6863
7452
  [host, activeSceneId]
6864
7453
  );
6865
- const resolvedGenericGroups = (0, import_react29.useMemo)(() => {
7454
+ const resolvedGenericGroups = (0, import_react30.useMemo)(() => {
6866
7455
  const out = {};
6867
7456
  for (const ext of adapter.groupExtensions ?? []) {
6868
7457
  out[ext.metaKey] = resolveTrackGroups(
@@ -6876,18 +7465,18 @@ function useGeneratorPanelCore({
6876
7465
  }
6877
7466
  return out;
6878
7467
  }, [adapter, genericGroupMetas, tracks]);
6879
- const genericGroupMemberDbIds = (0, import_react29.useMemo)(() => {
7468
+ const genericGroupMemberDbIds = (0, import_react30.useMemo)(() => {
6880
7469
  const s = /* @__PURE__ */ new Set();
6881
7470
  for (const r of Object.values(resolvedGenericGroups)) {
6882
7471
  for (const dbId of r.memberDbIds) s.add(dbId);
6883
7472
  }
6884
7473
  return s;
6885
7474
  }, [resolvedGenericGroups]);
6886
- const engineToDbId = (0, import_react29.useCallback)(
7475
+ const engineToDbId = (0, import_react30.useCallback)(
6887
7476
  (trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
6888
7477
  []
6889
7478
  );
6890
- const updateTrack = (0, import_react29.useCallback)(
7479
+ const updateTrack = (0, import_react30.useCallback)(
6891
7480
  (trackId, patch) => {
6892
7481
  setTracks(
6893
7482
  (prev) => prev.map(
@@ -6897,18 +7486,18 @@ function useGeneratorPanelCore({
6897
7486
  },
6898
7487
  []
6899
7488
  );
6900
- const markEditLoaded = (0, import_react29.useCallback)((trackId) => {
7489
+ const markEditLoaded = (0, import_react30.useCallback)((trackId) => {
6901
7490
  editLoadStartedRef.current.add(trackId);
6902
7491
  }, []);
6903
- const tracksRef = (0, import_react29.useRef)(tracks);
6904
- (0, import_react29.useEffect)(() => {
7492
+ const tracksRef = (0, import_react30.useRef)(tracks);
7493
+ (0, import_react30.useEffect)(() => {
6905
7494
  tracksRef.current = tracks;
6906
7495
  }, [tracks]);
6907
- const resolvedGenericGroupsRef = (0, import_react29.useRef)(resolvedGenericGroups);
6908
- (0, import_react29.useEffect)(() => {
7496
+ const resolvedGenericGroupsRef = (0, import_react30.useRef)(resolvedGenericGroups);
7497
+ (0, import_react30.useEffect)(() => {
6909
7498
  resolvedGenericGroupsRef.current = resolvedGenericGroups;
6910
7499
  }, [resolvedGenericGroups]);
6911
- const makeServices = (0, import_react29.useCallback)(() => {
7500
+ const makeServices = (0, import_react30.useCallback)(() => {
6912
7501
  return {
6913
7502
  host,
6914
7503
  activeSceneId,
@@ -6927,7 +7516,7 @@ function useGeneratorPanelCore({
6927
7516
  resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
6928
7517
  };
6929
7518
  }, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
6930
- const handleGenerate = (0, import_react29.useCallback)(
7519
+ const handleGenerate = (0, import_react30.useCallback)(
6931
7520
  async (trackId) => {
6932
7521
  const track = tracks.find((t) => t.handle.id === trackId);
6933
7522
  if (!track || !track.prompt.trim()) return;
@@ -6954,7 +7543,7 @@ function useGeneratorPanelCore({
6954
7543
  },
6955
7544
  [host, adapter, tracks, isAuthenticated, makeServices]
6956
7545
  );
6957
- const handleMuteToggle = (0, import_react29.useCallback)(
7546
+ const handleMuteToggle = (0, import_react30.useCallback)(
6958
7547
  (trackId) => {
6959
7548
  const track = tracks.find((t) => t.handle.id === trackId);
6960
7549
  if (!track) return;
@@ -6974,7 +7563,7 @@ function useGeneratorPanelCore({
6974
7563
  },
6975
7564
  [host, tracks]
6976
7565
  );
6977
- const handleSoloToggle = (0, import_react29.useCallback)(
7566
+ const handleSoloToggle = (0, import_react30.useCallback)(
6978
7567
  (trackId) => {
6979
7568
  const track = tracks.find((t) => t.handle.id === trackId);
6980
7569
  if (!track) return;
@@ -6994,7 +7583,7 @@ function useGeneratorPanelCore({
6994
7583
  },
6995
7584
  [host, tracks]
6996
7585
  );
6997
- const handleVolumeChange = (0, import_react29.useCallback)(
7586
+ const handleVolumeChange = (0, import_react30.useCallback)(
6998
7587
  (trackId, volume) => {
6999
7588
  setTracks(
7000
7589
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
@@ -7004,7 +7593,7 @@ function useGeneratorPanelCore({
7004
7593
  },
7005
7594
  [host]
7006
7595
  );
7007
- const handlePanChange = (0, import_react29.useCallback)(
7596
+ const handlePanChange = (0, import_react30.useCallback)(
7008
7597
  (trackId, pan) => {
7009
7598
  setTracks(
7010
7599
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
@@ -7014,7 +7603,7 @@ function useGeneratorPanelCore({
7014
7603
  },
7015
7604
  [host]
7016
7605
  );
7017
- const handleShuffle = (0, import_react29.useCallback)(
7606
+ const handleShuffle = (0, import_react30.useCallback)(
7018
7607
  async (trackId) => {
7019
7608
  const track = tracks.find((t) => t.handle.id === trackId);
7020
7609
  if (!track) return;
@@ -7056,7 +7645,7 @@ function useGeneratorPanelCore({
7056
7645
  },
7057
7646
  [host, adapter, tracks, soundHistory, logTag]
7058
7647
  );
7059
- const handleCopy = (0, import_react29.useCallback)(
7648
+ const handleCopy = (0, import_react30.useCallback)(
7060
7649
  async (trackId) => {
7061
7650
  try {
7062
7651
  const newHandle = await host.duplicateTrack(trackId);
@@ -7069,7 +7658,7 @@ function useGeneratorPanelCore({
7069
7658
  },
7070
7659
  [host, loadTracks]
7071
7660
  );
7072
- const handleFxToggle = (0, import_react29.useCallback)(
7661
+ const handleFxToggle = (0, import_react30.useCallback)(
7073
7662
  (trackId, category, enabled) => {
7074
7663
  setTracks(
7075
7664
  (prev) => prev.map(
@@ -7092,7 +7681,7 @@ function useGeneratorPanelCore({
7092
7681
  },
7093
7682
  [host]
7094
7683
  );
7095
- const handleFxPresetChange = (0, import_react29.useCallback)(
7684
+ const handleFxPresetChange = (0, import_react30.useCallback)(
7096
7685
  (trackId, category, presetIndex) => {
7097
7686
  setTracks(
7098
7687
  (prev) => prev.map(
@@ -7118,7 +7707,7 @@ function useGeneratorPanelCore({
7118
7707
  },
7119
7708
  [host]
7120
7709
  );
7121
- const handleFxDryWetChange = (0, import_react29.useCallback)(
7710
+ const handleFxDryWetChange = (0, import_react30.useCallback)(
7122
7711
  (trackId, category, value) => {
7123
7712
  setTracks(
7124
7713
  (prev) => prev.map(
@@ -7130,7 +7719,7 @@ function useGeneratorPanelCore({
7130
7719
  },
7131
7720
  [host]
7132
7721
  );
7133
- const toggleFxDrawer = (0, import_react29.useCallback)(
7722
+ const toggleFxDrawer = (0, import_react30.useCallback)(
7134
7723
  (trackId) => {
7135
7724
  setTracks(
7136
7725
  (prev) => prev.map((t) => {
@@ -7152,7 +7741,7 @@ function useGeneratorPanelCore({
7152
7741
  },
7153
7742
  [host, tracks]
7154
7743
  );
7155
- const loadEditNotes = (0, import_react29.useCallback)(
7744
+ const loadEditNotes = (0, import_react30.useCallback)(
7156
7745
  async (trackId) => {
7157
7746
  try {
7158
7747
  const mc = await host.getMusicalContext();
@@ -7170,7 +7759,7 @@ function useGeneratorPanelCore({
7170
7759
  },
7171
7760
  [host, logTag]
7172
7761
  );
7173
- const handleNotesChange = (0, import_react29.useCallback)(
7762
+ const handleNotesChange = (0, import_react30.useCallback)(
7174
7763
  (trackId, notes) => {
7175
7764
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
7176
7765
  const key = `edit:${trackId}`;
@@ -7200,7 +7789,7 @@ function useGeneratorPanelCore({
7200
7789
  },
7201
7790
  [host]
7202
7791
  );
7203
- const handleTabChange = (0, import_react29.useCallback)(
7792
+ const handleTabChange = (0, import_react30.useCallback)(
7204
7793
  (trackId, tab) => {
7205
7794
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
7206
7795
  if (tab === "fx") {
@@ -7225,10 +7814,10 @@ function useGeneratorPanelCore({
7225
7814
  },
7226
7815
  [host, availableInstruments.length, instrumentsLoading, loadEditNotes]
7227
7816
  );
7228
- const handleProgressChange = (0, import_react29.useCallback)((trackId, pct) => {
7817
+ const handleProgressChange = (0, import_react30.useCallback)((trackId, pct) => {
7229
7818
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
7230
7819
  }, []);
7231
- const handleToggleDrawer = (0, import_react29.useCallback)((trackId) => {
7820
+ const handleToggleDrawer = (0, import_react30.useCallback)((trackId) => {
7232
7821
  setTracks(
7233
7822
  (prev) => prev.map((t) => {
7234
7823
  if (t.handle.id !== trackId) return t;
@@ -7237,7 +7826,7 @@ function useGeneratorPanelCore({
7237
7826
  })
7238
7827
  );
7239
7828
  }, []);
7240
- const handleInstrumentSelect = (0, import_react29.useCallback)(
7829
+ const handleInstrumentSelect = (0, import_react30.useCallback)(
7241
7830
  async (trackId, pluginId) => {
7242
7831
  const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
7243
7832
  if (isDefaultInstrument) {
@@ -7290,7 +7879,7 @@ function useGeneratorPanelCore({
7290
7879
  },
7291
7880
  [host, identity.defaultInstrumentPluginId, logTag]
7292
7881
  );
7293
- const handleShowEditor = (0, import_react29.useCallback)(
7882
+ const handleShowEditor = (0, import_react30.useCallback)(
7294
7883
  async (trackId) => {
7295
7884
  try {
7296
7885
  await host.showInstrumentEditor(trackId);
@@ -7301,12 +7890,12 @@ function useGeneratorPanelCore({
7301
7890
  },
7302
7891
  [host]
7303
7892
  );
7304
- const handleBackToInstruments = (0, import_react29.useCallback)((trackId) => {
7893
+ const handleBackToInstruments = (0, import_react30.useCallback)((trackId) => {
7305
7894
  setTracks(
7306
7895
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
7307
7896
  );
7308
7897
  }, []);
7309
- const handleRefreshInstruments = (0, import_react29.useCallback)(() => {
7898
+ const handleRefreshInstruments = (0, import_react30.useCallback)(() => {
7310
7899
  setInstrumentsLoading(true);
7311
7900
  host.getAvailableInstruments().then((instruments) => {
7312
7901
  setAvailableInstruments(instruments);
@@ -7315,13 +7904,13 @@ function useGeneratorPanelCore({
7315
7904
  setInstrumentsLoading(false);
7316
7905
  });
7317
7906
  }, [host]);
7318
- const onAuditionNote = (0, import_react29.useCallback)(
7907
+ const onAuditionNote = (0, import_react30.useCallback)(
7319
7908
  (trackId, pitch, velocity, ms) => {
7320
7909
  void host.auditionNote(trackId, pitch, velocity, ms);
7321
7910
  },
7322
7911
  [host]
7323
7912
  );
7324
- const { resolvedCrossfadePairs, crossfadeMemberDbIds } = (0, import_react29.useMemo)(() => {
7913
+ const { resolvedCrossfadePairs, crossfadeMemberDbIds } = (0, import_react30.useMemo)(() => {
7325
7914
  const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
7326
7915
  const pairs = [];
7327
7916
  const members = /* @__PURE__ */ new Set();
@@ -7336,7 +7925,7 @@ function useGeneratorPanelCore({
7336
7925
  }
7337
7926
  return { resolvedCrossfadePairs: pairs, crossfadeMemberDbIds: members };
7338
7927
  }, [tracks, crossfadePairsMeta]);
7339
- const { resolvedFades, fadeMemberDbIds } = (0, import_react29.useMemo)(() => {
7928
+ const { resolvedFades, fadeMemberDbIds } = (0, import_react30.useMemo)(() => {
7340
7929
  const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
7341
7930
  const list = [];
7342
7931
  const members = /* @__PURE__ */ new Set();
@@ -7349,6 +7938,10 @@ function useGeneratorPanelCore({
7349
7938
  }
7350
7939
  return { resolvedFades: list, fadeMemberDbIds: members };
7351
7940
  }, [tracks, fadesMeta]);
7941
+ const { singles: resolvedSingleFades, groups: resolvedGroupFades } = (0, import_react30.useMemo)(
7942
+ () => splitFadeEntries(resolvedFades),
7943
+ [resolvedFades]
7944
+ );
7352
7945
  const transition = useTransitionOps({
7353
7946
  host,
7354
7947
  adapter,
@@ -7364,7 +7957,7 @@ function useGeneratorPanelCore({
7364
7957
  resolvedCrossfadePairs,
7365
7958
  resolvedFades
7366
7959
  });
7367
- const setGroupMute = (0, import_react29.useCallback)(
7960
+ const setGroupMute = (0, import_react30.useCallback)(
7368
7961
  (trackIds, muted) => {
7369
7962
  for (const id of trackIds) {
7370
7963
  setTracks(
@@ -7376,7 +7969,7 @@ function useGeneratorPanelCore({
7376
7969
  },
7377
7970
  [host]
7378
7971
  );
7379
- const setGroupSolo = (0, import_react29.useCallback)(
7972
+ const setGroupSolo = (0, import_react30.useCallback)(
7380
7973
  (trackIds, solo) => {
7381
7974
  for (const id of trackIds) {
7382
7975
  setTracks(
@@ -7388,7 +7981,7 @@ function useGeneratorPanelCore({
7388
7981
  },
7389
7982
  [host]
7390
7983
  );
7391
- const deleteGroup = (0, import_react29.useCallback)(
7984
+ const deleteGroup = (0, import_react30.useCallback)(
7392
7985
  async (members, cleanupKeySuffixes) => {
7393
7986
  for (const member of members) {
7394
7987
  try {
@@ -7408,7 +8001,7 @@ function useGeneratorPanelCore({
7408
8001
  },
7409
8002
  [host, activeSceneId, loadTracks]
7410
8003
  );
7411
- const handlers = (0, import_react29.useMemo)(
8004
+ const handlers = (0, import_react30.useMemo)(
7412
8005
  () => ({
7413
8006
  promptChange: handlePromptChange,
7414
8007
  generate: (trackId) => {
@@ -7485,6 +8078,8 @@ function useGeneratorPanelCore({
7485
8078
  crossfadeMemberDbIds,
7486
8079
  resolvedFades,
7487
8080
  fadeMemberDbIds,
8081
+ resolvedSingleFades,
8082
+ resolvedGroupFades,
7488
8083
  resolvedGenericGroups,
7489
8084
  genericGroupMemberDbIds,
7490
8085
  availableInstruments,
@@ -7522,8 +8117,8 @@ function useGeneratorPanelCore({
7522
8117
  }
7523
8118
 
7524
8119
  // src/panel-core/GeneratorPanelShell.tsx
7525
- var import_react30 = __toESM(require("react"));
7526
- var import_jsx_runtime26 = require("react/jsx-runtime");
8120
+ var import_react31 = __toESM(require("react"));
8121
+ var import_jsx_runtime27 = require("react/jsx-runtime");
7527
8122
  function GeneratorPanelShell({ core, slots }) {
7528
8123
  const {
7529
8124
  ui,
@@ -7552,7 +8147,8 @@ function GeneratorPanelShell({ core, slots }) {
7552
8147
  fadesMeta,
7553
8148
  resolvedCrossfadePairs,
7554
8149
  crossfadeMemberDbIds,
7555
- resolvedFades,
8150
+ resolvedSingleFades,
8151
+ resolvedGroupFades,
7556
8152
  fadeMemberDbIds,
7557
8153
  resolvedGenericGroups,
7558
8154
  genericGroupMemberDbIds,
@@ -7578,7 +8174,7 @@ function GeneratorPanelShell({ core, slots }) {
7578
8174
  const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
7579
8175
  const panelBus = usePanelBus(host, activeSceneId);
7580
8176
  const { identity, features } = adapter;
7581
- const buildRowProps = (0, import_react30.useCallback)(
8177
+ const buildRowProps = (0, import_react31.useCallback)(
7582
8178
  (track, drag) => {
7583
8179
  const id = track.handle.id;
7584
8180
  const pickerProps = features.instrumentPicker ? {
@@ -7678,12 +8274,12 @@ function GeneratorPanelShell({ core, slots }) {
7678
8274
  ]
7679
8275
  );
7680
8276
  if (!activeSceneId) {
7681
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8277
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7682
8278
  "div",
7683
8279
  {
7684
8280
  "data-testid": `no-scene-placeholder-${identity.familyKey}`,
7685
8281
  className: "flex items-center justify-center py-8",
7686
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8282
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7687
8283
  "button",
7688
8284
  {
7689
8285
  onClick: () => onSelectScene?.(),
@@ -7695,12 +8291,12 @@ function GeneratorPanelShell({ core, slots }) {
7695
8291
  );
7696
8292
  }
7697
8293
  if (!sceneContext?.hasContract) {
7698
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8294
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7699
8295
  "div",
7700
8296
  {
7701
8297
  "data-testid": `no-contract-placeholder-${identity.familyKey}`,
7702
8298
  className: "flex items-center justify-center py-8",
7703
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8299
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7704
8300
  "button",
7705
8301
  {
7706
8302
  onClick: () => onOpenContract?.(),
@@ -7712,7 +8308,7 @@ function GeneratorPanelShell({ core, slots }) {
7712
8308
  );
7713
8309
  }
7714
8310
  if (features.bulkComposePlaceholders && isComposing) {
7715
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
8311
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
7716
8312
  }
7717
8313
  const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
7718
8314
  if (activePlaceholders.length > 0) {
@@ -7723,18 +8319,18 @@ function GeneratorPanelShell({ core, slots }) {
7723
8319
  tracksByDbId.set(t.handle.id, t);
7724
8320
  }
7725
8321
  }
7726
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
8322
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
7727
8323
  const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
7728
8324
  if (loadedTrack) {
7729
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
8325
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
7730
8326
  }
7731
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8327
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7732
8328
  "div",
7733
8329
  {
7734
8330
  "data-testid": "bulk-placeholder-track",
7735
8331
  className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
7736
8332
  style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
7737
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
8333
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
7738
8334
  },
7739
8335
  ph.id
7740
8336
  );
@@ -7746,13 +8342,13 @@ function GeneratorPanelShell({ core, slots }) {
7746
8342
  supportsMeters,
7747
8343
  levels: supportsMeters ? trackLevels : void 0,
7748
8344
  handlers,
7749
- renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
8345
+ renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
7750
8346
  setGroupMute,
7751
8347
  setGroupSolo,
7752
8348
  deleteGroup
7753
8349
  };
7754
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
7755
- features.importTracks && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8350
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
8351
+ features.importTracks && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7756
8352
  ImportTrackModal,
7757
8353
  {
7758
8354
  host,
@@ -7765,7 +8361,7 @@ function GeneratorPanelShell({ core, slots }) {
7765
8361
  testIdPrefix: `${identity.familyKey}-import`
7766
8362
  }
7767
8363
  ),
7768
- features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8364
+ features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7769
8365
  ImportTrackModal,
7770
8366
  {
7771
8367
  host,
@@ -7780,7 +8376,7 @@ function GeneratorPanelShell({ core, slots }) {
7780
8376
  }
7781
8377
  ),
7782
8378
  slots?.modals,
7783
- canCrossfade && xfFromId && xfToId && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8379
+ canCrossfade && xfFromId && xfToId && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7784
8380
  TransitionDesigner,
7785
8381
  {
7786
8382
  host,
@@ -7791,14 +8387,16 @@ function GeneratorPanelShell({ core, slots }) {
7791
8387
  ...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
7792
8388
  ...fadesMeta.map((f) => f.meta.sourceTrackDbId)
7793
8389
  ],
7794
- onCreateCrossfade: transition.handleCreateCrossfade,
7795
- onCreateFade: transition.handleCreateFade,
8390
+ onCreateCrossfade: adapter.transitionGroup?.fadeOnly ? void 0 : transition.handleCreateCrossfade,
8391
+ onCreateFade: adapter.transitionGroup ? (sel, dir) => transition.handleCreateVerbatimGroupFade(sel, dir) : transition.handleCreateFade,
8392
+ mapColumnSubjects: adapter.transitionGroup ? (sceneId, tracks2) => adapter.transitionGroup.mapColumnSubjects(sceneId, tracks2) : void 0,
8393
+ fadeEstimateMs: adapter.transitionGroup?.fadeEstimateMs,
7796
8394
  familyLabel: identity.familyLabel,
7797
8395
  testIdPrefix: `${identity.familyKey}-transition-designer`
7798
8396
  }
7799
8397
  ) }),
7800
- !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
7801
- panelBus.supported && panelBus.bus && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8398
+ !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
8399
+ panelBus.supported && panelBus.bus && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7802
8400
  PanelMasterStrip,
7803
8401
  {
7804
8402
  bus: panelBus.bus,
@@ -7819,7 +8417,7 @@ function GeneratorPanelShell({ core, slots }) {
7819
8417
  }
7820
8418
  ),
7821
8419
  slots?.beforeRows,
7822
- resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8420
+ resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7823
8421
  CrossfadeTrackRow,
7824
8422
  {
7825
8423
  accentColor: identity.transitionAccentColor,
@@ -7856,7 +8454,7 @@ function GeneratorPanelShell({ core, slots }) {
7856
8454
  },
7857
8455
  pair.groupId
7858
8456
  )),
7859
- resolvedFades.map((fade) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8457
+ resolvedSingleFades.map((fade) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7860
8458
  FadeTrackRow,
7861
8459
  {
7862
8460
  accentColor: identity.transitionAccentColor,
@@ -7881,21 +8479,56 @@ function GeneratorPanelShell({ core, slots }) {
7881
8479
  },
7882
8480
  fade.dbId
7883
8481
  )),
8482
+ resolvedGroupFades.map((group) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
8483
+ GroupFadeTrackRow,
8484
+ {
8485
+ accentColor: identity.transitionAccentColor,
8486
+ levels: supportsMeters ? trackLevels : void 0,
8487
+ direction: group.direction,
8488
+ gesture: group.gesture,
8489
+ sliderPos: group.sliderPos,
8490
+ groupLabel: adapter.transitionGroup?.groupRowLabel?.(group.members.length) ?? `Group (${group.members.length} tracks)`,
8491
+ members: group.members.map((m) => ({
8492
+ trackId: m.track.handle.id,
8493
+ name: m.track.handle.name,
8494
+ role: m.track.role,
8495
+ sourceName: m.meta.sourceName,
8496
+ soundLabel: m.meta.soundLabel,
8497
+ memberLabel: m.meta.memberLabel,
8498
+ runtimeState: m.track.runtimeState
8499
+ })),
8500
+ onMemberMuteToggle: (trackId) => handlers.muteToggle(trackId),
8501
+ onMemberSoloToggle: (trackId) => handlers.soloToggle(trackId),
8502
+ onMemberVolumeChange: (trackId, vol) => handlers.volumeChange(trackId, vol),
8503
+ onMemberPanChange: (trackId, pan) => handlers.panChange(trackId, pan),
8504
+ onMuteAll: () => setGroupMute(
8505
+ group.members.map((m) => m.track.handle.id),
8506
+ !group.members.every((m) => m.track.runtimeState.muted)
8507
+ ),
8508
+ onSoloAll: () => setGroupSolo(
8509
+ group.members.map((m) => m.track.handle.id),
8510
+ !group.members.every((m) => m.track.runtimeState.solo)
8511
+ ),
8512
+ onSliderChange: (pos) => transition.handleGroupFadeSlider(group, pos),
8513
+ onDelete: () => transition.handleGroupFadeDelete(group)
8514
+ },
8515
+ group.groupId
8516
+ )),
7884
8517
  (adapter.groupExtensions ?? []).flatMap(
7885
- (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react30.default.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
8518
+ (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).filter((group) => !group.members.every((m) => fadeMemberDbIds.has(m.dbId))).map((group) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react31.default.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
7886
8519
  ),
7887
8520
  tracks.map((track, index) => {
7888
8521
  if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
7889
8522
  return null;
7890
8523
  }
7891
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
8524
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
7892
8525
  }),
7893
8526
  slots?.afterRows
7894
8527
  ] })),
7895
8528
  features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
7896
8529
  const hasAnyMidi = tracks.some((t) => t.hasMidi);
7897
8530
  const exportDisabled = isExportingMidi || !hasAnyMidi;
7898
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "pt-2", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8531
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "pt-2", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7899
8532
  "button",
7900
8533
  {
7901
8534
  "data-testid": "export-midi-tracks-button",
@@ -7963,7 +8596,7 @@ function createSurgeSoundAdapter(host, overrides = {}) {
7963
8596
  }
7964
8597
 
7965
8598
  // src/constants/sdk-version.ts
7966
- var PLUGIN_SDK_VERSION = "2.39.0";
8599
+ var PLUGIN_SDK_VERSION = "2.40.0";
7967
8600
 
7968
8601
  // src/utils/format-concurrent-tracks.ts
7969
8602
  function formatConcurrentTracks(ctx) {
@@ -8131,6 +8764,7 @@ function pickTopKWeighted(scored, options = {}) {
8131
8764
  FxToggleBar,
8132
8765
  GUTTER_W,
8133
8766
  GeneratorPanelShell,
8767
+ GroupFadeTrackRow,
8134
8768
  ImportTrackModal,
8135
8769
  InstrumentDrawer,
8136
8770
  LevelMeter,
@@ -8199,6 +8833,7 @@ function pickTopKWeighted(scored, options = {}) {
8199
8833
  sliderToDb,
8200
8834
  slotsEqual,
8201
8835
  soundIdentity,
8836
+ splitFadeEntries,
8202
8837
  synthesizeCuePoints,
8203
8838
  tokenizePrompt,
8204
8839
  trackDataKey,