@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.mjs CHANGED
@@ -820,12 +820,12 @@ function useTrackExternalFx(host, trackId) {
820
820
  void reload();
821
821
  }, [reload]);
822
822
  const loadFxList = useCallback2(
823
- async (force) => {
823
+ async (opts) => {
824
824
  if (!supported || !host.getAvailableFx) return;
825
- if (fxLoadedRef.current && !force) return;
825
+ if (fxLoadedRef.current && !opts.force && !opts.rescan) return;
826
826
  setFxLoading(true);
827
827
  try {
828
- const list = await host.getAvailableFx();
828
+ const list = opts.rescan && host.rescanAvailableFx ? await host.rescanAvailableFx() : await host.getAvailableFx();
829
829
  setAvailableFx(list);
830
830
  fxLoadedRef.current = true;
831
831
  } catch {
@@ -838,7 +838,7 @@ function useTrackExternalFx(host, trackId) {
838
838
  const openPicker = useCallback2(
839
839
  (open) => {
840
840
  setPickerOpen(open);
841
- if (open) void loadFxList(false);
841
+ if (open) void loadFxList({});
842
842
  },
843
843
  [loadFxList]
844
844
  );
@@ -862,7 +862,7 @@ function useTrackExternalFx(host, trackId) {
862
862
  fxLoading,
863
863
  pickerOpen,
864
864
  setPickerOpen: openPicker,
865
- refreshFx: () => void loadFxList(true),
865
+ refreshFx: () => void loadFxList({ rescan: true }),
866
866
  reload,
867
867
  onAddFx: (pluginId) => mutate(host.loadTrackExternalFx && (async () => {
868
868
  await host.loadTrackExternalFx(trackId, pluginId);
@@ -999,7 +999,7 @@ function TrackExternalFxSection({
999
999
  onClick: () => refreshFx(),
1000
1000
  disabled: fxLoading,
1001
1001
  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",
1002
- title: "Re-scan plugins",
1002
+ title: "Re-scan plugins \u2014 picks up newly installed FX and retries any that failed a previous scan",
1003
1003
  children: fxLoading ? "..." : "Refresh"
1004
1004
  }
1005
1005
  )
@@ -2848,7 +2848,10 @@ function asFadeMeta(val) {
2848
2848
  sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
2849
2849
  sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
2850
2850
  soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
2851
- sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
2851
+ sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5,
2852
+ groupId: typeof m.groupId === "string" && m.groupId ? m.groupId : void 0,
2853
+ memberIndex: typeof m.memberIndex === "number" ? m.memberIndex : void 0,
2854
+ memberLabel: typeof m.memberLabel === "string" && m.memberLabel ? m.memberLabel : void 0
2852
2855
  };
2853
2856
  }
2854
2857
  function parseFades(sceneData) {
@@ -2862,6 +2865,33 @@ function parseFades(sceneData) {
2862
2865
  }
2863
2866
  return out;
2864
2867
  }
2868
+ function splitFadeEntries(entries) {
2869
+ const singles = [];
2870
+ const byGroup = /* @__PURE__ */ new Map();
2871
+ for (const entry of entries) {
2872
+ const gid = entry.meta.groupId;
2873
+ if (!gid) {
2874
+ singles.push(entry);
2875
+ continue;
2876
+ }
2877
+ const list = byGroup.get(gid) ?? [];
2878
+ list.push(entry);
2879
+ byGroup.set(gid, list);
2880
+ }
2881
+ const groups = [];
2882
+ for (const [groupId, members] of byGroup) {
2883
+ members.sort((a, b) => (a.meta.memberIndex ?? 0) - (b.meta.memberIndex ?? 0));
2884
+ const head = members[0].meta;
2885
+ groups.push({
2886
+ groupId,
2887
+ direction: head.direction,
2888
+ gesture: head.gesture,
2889
+ sliderPos: head.sliderPos,
2890
+ members
2891
+ });
2892
+ }
2893
+ return { singles, groups };
2894
+ }
2865
2895
  function buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture, steps = 32) {
2866
2896
  const durationSeconds = bars * 4 * 60 / Math.max(1, bpm);
2867
2897
  if (gesture === "build") {
@@ -3046,9 +3076,191 @@ function FadeTrackRow({
3046
3076
  );
3047
3077
  }
3048
3078
 
3079
+ // src/components/GroupFadeTrackRow.tsx
3080
+ import React12 from "react";
3081
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
3082
+ function MemberCaption({
3083
+ member,
3084
+ direction
3085
+ }) {
3086
+ const tag = direction === "in" ? "Fade in" : "Fade out";
3087
+ return /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
3088
+ member.memberLabel && /* @__PURE__ */ jsx15("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: member.memberLabel }),
3089
+ /* @__PURE__ */ jsx15("span", { className: "text-[11px] text-sas-text truncate", title: member.sourceName ?? member.name, children: member.sourceName ?? member.name }),
3090
+ member.soundLabel && /* @__PURE__ */ jsxs11("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: member.soundLabel, children: [
3091
+ "\xB7 ",
3092
+ member.soundLabel
3093
+ ] }),
3094
+ !member.memberLabel && /* @__PURE__ */ jsxs11("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: [
3095
+ "\xB7 ",
3096
+ tag
3097
+ ] })
3098
+ ] });
3099
+ }
3100
+ function GroupFadeTrackRow({
3101
+ groupLabel,
3102
+ direction,
3103
+ gesture,
3104
+ sliderPos = 0.5,
3105
+ members,
3106
+ onMemberMuteToggle,
3107
+ onMemberSoloToggle,
3108
+ onMemberVolumeChange,
3109
+ onMemberPanChange,
3110
+ onMuteAll,
3111
+ onSoloAll,
3112
+ onDelete,
3113
+ onSliderChange,
3114
+ levels,
3115
+ accentColor = "#9333EA"
3116
+ }) {
3117
+ const [confirmDelete, setConfirmDelete] = React12.useState(false);
3118
+ const allMuted = members.length > 0 && members.every((m) => m.runtimeState.muted);
3119
+ const allSolo = members.length > 0 && members.every((m) => m.runtimeState.solo);
3120
+ const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
3121
+ const leftLabel = direction === "in" ? "(silent)" : groupLabel;
3122
+ const rightLabel = direction === "in" ? groupLabel : "(silent)";
3123
+ return /* @__PURE__ */ jsxs11(
3124
+ "div",
3125
+ {
3126
+ "data-testid": "group-fade-track-row",
3127
+ className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
3128
+ style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
3129
+ children: [
3130
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60 gap-2", children: [
3131
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 min-w-0", children: [
3132
+ /* @__PURE__ */ jsx15(
3133
+ "span",
3134
+ {
3135
+ "data-testid": "group-fade-direction-badge",
3136
+ className: "text-[10px] font-bold uppercase tracking-wide flex-shrink-0",
3137
+ style: { color: accentColor },
3138
+ children: badge
3139
+ }
3140
+ ),
3141
+ /* @__PURE__ */ jsx15(
3142
+ "span",
3143
+ {
3144
+ "data-testid": "group-fade-label",
3145
+ className: "text-[11px] text-sas-text truncate",
3146
+ title: `${groupLabel} \xB7 ${gesture}`,
3147
+ children: groupLabel
3148
+ }
3149
+ )
3150
+ ] }),
3151
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1 flex-shrink-0", children: [
3152
+ /* @__PURE__ */ jsx15(
3153
+ "button",
3154
+ {
3155
+ "data-testid": "group-fade-mute-button",
3156
+ onClick: onMuteAll,
3157
+ 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"}`,
3158
+ title: allMuted ? "Unmute all members" : "Mute all members",
3159
+ children: "M"
3160
+ }
3161
+ ),
3162
+ /* @__PURE__ */ jsx15(
3163
+ "button",
3164
+ {
3165
+ "data-testid": "group-fade-solo-button",
3166
+ onClick: onSoloAll,
3167
+ 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"}`,
3168
+ title: allSolo ? "Unsolo all members" : "Solo all members",
3169
+ children: "S"
3170
+ }
3171
+ ),
3172
+ /* @__PURE__ */ jsx15(
3173
+ "button",
3174
+ {
3175
+ "data-testid": "group-fade-delete-button",
3176
+ onClick: () => setConfirmDelete(true),
3177
+ className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
3178
+ title: "Delete group fade",
3179
+ "aria-label": "Delete group fade",
3180
+ children: "x"
3181
+ }
3182
+ )
3183
+ ] })
3184
+ ] }),
3185
+ members.map((member) => /* @__PURE__ */ jsx15(
3186
+ TrackRow,
3187
+ {
3188
+ track: { id: member.trackId, name: "", role: member.role },
3189
+ runtimeState: member.runtimeState,
3190
+ fxDetailState: EMPTY_FX_DETAIL_STATE,
3191
+ drawerOpen: false,
3192
+ drawerTab: "fx",
3193
+ levels,
3194
+ accentColor,
3195
+ contentSlot: /* @__PURE__ */ jsx15(MemberCaption, { member, direction }),
3196
+ onMuteToggle: () => onMemberMuteToggle(member.trackId),
3197
+ onSoloToggle: () => onMemberSoloToggle(member.trackId),
3198
+ onVolumeChange: (vol) => onMemberVolumeChange(member.trackId, vol),
3199
+ onPanChange: (pan) => onMemberPanChange(member.trackId, pan)
3200
+ },
3201
+ member.trackId
3202
+ )),
3203
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "group-fade-slider-row", children: [
3204
+ /* @__PURE__ */ jsx15(
3205
+ "span",
3206
+ {
3207
+ className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
3208
+ title: leftLabel,
3209
+ children: leftLabel
3210
+ }
3211
+ ),
3212
+ /* @__PURE__ */ jsx15(
3213
+ "input",
3214
+ {
3215
+ type: "range",
3216
+ "data-testid": "group-fade-slider",
3217
+ min: 0,
3218
+ max: 1,
3219
+ step: 0.01,
3220
+ value: sliderPos,
3221
+ disabled: !onSliderChange,
3222
+ onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
3223
+ style: { accentColor },
3224
+ className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
3225
+ "aria-label": "Group fade position"
3226
+ }
3227
+ ),
3228
+ /* @__PURE__ */ jsx15(
3229
+ "span",
3230
+ {
3231
+ className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
3232
+ title: rightLabel,
3233
+ children: rightLabel
3234
+ }
3235
+ )
3236
+ ] }),
3237
+ /* @__PURE__ */ jsx15(
3238
+ ConfirmDialog,
3239
+ {
3240
+ open: confirmDelete,
3241
+ title: "Delete group fade?",
3242
+ message: /* @__PURE__ */ jsxs11(Fragment4, { children: [
3243
+ "All ",
3244
+ members.length,
3245
+ " member tracks of this fade will be permanently removed from this scene. This cannot be undone."
3246
+ ] }),
3247
+ confirmLabel: "Delete",
3248
+ onConfirm: () => {
3249
+ setConfirmDelete(false);
3250
+ onDelete();
3251
+ },
3252
+ onCancel: () => setConfirmDelete(false),
3253
+ testIdPrefix: "group-fade-delete-confirm"
3254
+ }
3255
+ )
3256
+ ]
3257
+ }
3258
+ );
3259
+ }
3260
+
3049
3261
  // src/components/FadeModal.tsx
3050
3262
  import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef8, useState as useState9 } from "react";
3051
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
3263
+ import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
3052
3264
  function shortId(dbId) {
3053
3265
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3054
3266
  }
@@ -3091,7 +3303,7 @@ function OrphanRow({
3091
3303
  }) {
3092
3304
  const primary = track.prompt?.trim() || track.name;
3093
3305
  const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
3094
- return /* @__PURE__ */ jsxs11(
3306
+ return /* @__PURE__ */ jsxs12(
3095
3307
  "button",
3096
3308
  {
3097
3309
  type: "button",
@@ -3103,8 +3315,8 @@ function OrphanRow({
3103
3315
  disabled,
3104
3316
  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"}`,
3105
3317
  children: [
3106
- /* @__PURE__ */ jsx15("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3107
- meta && /* @__PURE__ */ jsx15("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3318
+ /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3319
+ meta && /* @__PURE__ */ jsx16("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3108
3320
  ]
3109
3321
  }
3110
3322
  );
@@ -3199,16 +3411,16 @@ function FadeModal({
3199
3411
  if (!open) return null;
3200
3412
  const renderSection = (heading, list, section) => {
3201
3413
  if (list.length === 0) return null;
3202
- return /* @__PURE__ */ jsxs11("div", { className: "block", children: [
3203
- /* @__PURE__ */ jsx15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
3204
- /* @__PURE__ */ jsx15(
3414
+ return /* @__PURE__ */ jsxs12("div", { className: "block", children: [
3415
+ /* @__PURE__ */ jsx16("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
3416
+ /* @__PURE__ */ jsx16(
3205
3417
  "div",
3206
3418
  {
3207
3419
  role: "radiogroup",
3208
3420
  "aria-label": heading,
3209
3421
  "data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
3210
3422
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3211
- children: list.map((t) => /* @__PURE__ */ jsx15(
3423
+ children: list.map((t) => /* @__PURE__ */ jsx16(
3212
3424
  OrphanRow,
3213
3425
  {
3214
3426
  track: t,
@@ -3224,32 +3436,32 @@ function FadeModal({
3224
3436
  )
3225
3437
  ] });
3226
3438
  };
3227
- return /* @__PURE__ */ jsx15(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs11(
3439
+ return /* @__PURE__ */ jsx16(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs12(
3228
3440
  "div",
3229
3441
  {
3230
3442
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3231
3443
  onClick: (e) => e.stopPropagation(),
3232
3444
  "data-testid": `${testIdPrefix}-box`,
3233
3445
  children: [
3234
- /* @__PURE__ */ jsx15("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
3235
- /* @__PURE__ */ jsxs11("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3446
+ /* @__PURE__ */ jsx16("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
3447
+ /* @__PURE__ */ jsxs12("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3236
3448
  "Tracks with no counterpart between",
3237
3449
  " ",
3238
- /* @__PURE__ */ jsx15("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3450
+ /* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3239
3451
  " and",
3240
3452
  " ",
3241
- /* @__PURE__ */ jsx15("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3453
+ /* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3242
3454
  " can gracefully fade out (leaving) or fade in (entering) across this transition."
3243
3455
  ] }),
3244
- load.status === "loading" && /* @__PURE__ */ jsx15("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3245
- load.status === "error" && /* @__PURE__ */ jsx15("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3246
- load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ jsx15("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__ */ jsxs11(Fragment4, { children: [
3456
+ load.status === "loading" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3457
+ load.status === "error" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3458
+ load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ jsx16("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__ */ jsxs12(Fragment5, { children: [
3247
3459
  renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
3248
3460
  renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
3249
3461
  ] })),
3250
- error && /* @__PURE__ */ jsx15("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3251
- /* @__PURE__ */ jsxs11("div", { className: "flex justify-end gap-2 pt-1", children: [
3252
- /* @__PURE__ */ jsx15(
3462
+ error && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3463
+ /* @__PURE__ */ jsxs12("div", { className: "flex justify-end gap-2 pt-1", children: [
3464
+ /* @__PURE__ */ jsx16(
3253
3465
  "button",
3254
3466
  {
3255
3467
  ref: cancelRef,
@@ -3260,7 +3472,7 @@ function FadeModal({
3260
3472
  children: "Cancel"
3261
3473
  }
3262
3474
  ),
3263
- /* @__PURE__ */ jsx15(
3475
+ /* @__PURE__ */ jsx16(
3264
3476
  "button",
3265
3477
  {
3266
3478
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3278,7 +3490,7 @@ function FadeModal({
3278
3490
 
3279
3491
  // src/components/ImportTrackModal.tsx
3280
3492
  import { useCallback as useCallback6, useEffect as useEffect8, useState as useState10 } from "react";
3281
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
3493
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
3282
3494
  function ImportTrackModal({
3283
3495
  host,
3284
3496
  open,
@@ -3357,16 +3569,16 @@ function ImportTrackModal({
3357
3569
  if (!open) return null;
3358
3570
  const scenes = load.status === "ready" ? load.scenes : [];
3359
3571
  const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
3360
- return /* @__PURE__ */ jsx16(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs12(
3572
+ return /* @__PURE__ */ jsx17(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs13(
3361
3573
  "div",
3362
3574
  {
3363
3575
  className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
3364
3576
  onClick: (e) => e.stopPropagation(),
3365
3577
  "data-testid": `${testIdPrefix}-modal`,
3366
3578
  children: [
3367
- /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3368
- /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2", children: [
3369
- selectedScene && /* @__PURE__ */ jsx16(
3579
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3580
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2", children: [
3581
+ selectedScene && /* @__PURE__ */ jsx17(
3370
3582
  "button",
3371
3583
  {
3372
3584
  className: "text-sas-muted hover:text-sas-accent text-xs",
@@ -3375,9 +3587,9 @@ function ImportTrackModal({
3375
3587
  children: "\u2190"
3376
3588
  }
3377
3589
  ),
3378
- /* @__PURE__ */ jsx16("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3590
+ /* @__PURE__ */ jsx17("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3379
3591
  ] }),
3380
- /* @__PURE__ */ jsx16(
3592
+ /* @__PURE__ */ jsx17(
3381
3593
  "button",
3382
3594
  {
3383
3595
  className: "text-sas-muted hover:text-sas-accent text-sm",
@@ -3387,30 +3599,30 @@ function ImportTrackModal({
3387
3599
  }
3388
3600
  )
3389
3601
  ] }),
3390
- /* @__PURE__ */ jsxs12("div", { className: "overflow-y-auto p-2 flex-1", children: [
3391
- load.status === "loading" && /* @__PURE__ */ jsx16("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3392
- load.status === "error" && /* @__PURE__ */ jsx16("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3393
- load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx16("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." }),
3394
- load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx16("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx16("li", { children: /* @__PURE__ */ jsxs12(
3602
+ /* @__PURE__ */ jsxs13("div", { className: "overflow-y-auto p-2 flex-1", children: [
3603
+ load.status === "loading" && /* @__PURE__ */ jsx17("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3604
+ load.status === "error" && /* @__PURE__ */ jsx17("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3605
+ load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx17("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." }),
3606
+ load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx17("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsxs13(
3395
3607
  "button",
3396
3608
  {
3397
3609
  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",
3398
3610
  onClick: () => setSelectedSceneId(scene.sceneId),
3399
3611
  "data-testid": `${testIdPrefix}-scene`,
3400
3612
  children: [
3401
- /* @__PURE__ */ jsx16("span", { className: "truncate", children: scene.sceneName }),
3402
- /* @__PURE__ */ jsxs12("span", { className: "text-sas-muted", children: [
3613
+ /* @__PURE__ */ jsx17("span", { className: "truncate", children: scene.sceneName }),
3614
+ /* @__PURE__ */ jsxs13("span", { className: "text-sas-muted", children: [
3403
3615
  scene.tracks.length,
3404
3616
  " \u2192"
3405
3617
  ] })
3406
3618
  ]
3407
3619
  }
3408
3620
  ) }, scene.sceneId)) }),
3409
- selectedScene && /* @__PURE__ */ jsx16("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3621
+ selectedScene && /* @__PURE__ */ jsx17("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3410
3622
  const busy = importingTrackId === track.trackId;
3411
3623
  const gated = mode === "track" && !track.importable;
3412
3624
  const disabled = gated || busy;
3413
- return /* @__PURE__ */ jsx16("li", { children: /* @__PURE__ */ jsxs12(
3625
+ return /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsxs13(
3414
3626
  "button",
3415
3627
  {
3416
3628
  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"}`,
@@ -3420,14 +3632,14 @@ function ImportTrackModal({
3420
3632
  "data-testid": `${testIdPrefix}-track`,
3421
3633
  "data-importable": mode === "sound" || track.importable ? "true" : "false",
3422
3634
  children: [
3423
- /* @__PURE__ */ jsxs12("span", { className: "truncate", children: [
3635
+ /* @__PURE__ */ jsxs13("span", { className: "truncate", children: [
3424
3636
  track.name,
3425
- track.role ? /* @__PURE__ */ jsxs12("span", { className: "text-sas-muted", children: [
3637
+ track.role ? /* @__PURE__ */ jsxs13("span", { className: "text-sas-muted", children: [
3426
3638
  " \xB7 ",
3427
3639
  track.role
3428
3640
  ] }) : null
3429
3641
  ] }),
3430
- busy ? /* @__PURE__ */ jsx16("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx16("span", { className: "text-sas-muted", children: "\u2298" }) : null
3642
+ busy ? /* @__PURE__ */ jsx17("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx17("span", { className: "text-sas-muted", children: "\u2298" }) : null
3431
3643
  ]
3432
3644
  }
3433
3645
  ) }, track.dbId);
@@ -3440,7 +3652,7 @@ function ImportTrackModal({
3440
3652
 
3441
3653
  // src/components/CrossfadeModal.tsx
3442
3654
  import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef9, useState as useState11 } from "react";
3443
- import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
3655
+ import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
3444
3656
  function shortId2(dbId) {
3445
3657
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3446
3658
  }
@@ -3453,7 +3665,7 @@ function CandidateRow({
3453
3665
  }) {
3454
3666
  const primary = track.prompt?.trim() || track.name;
3455
3667
  const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
3456
- return /* @__PURE__ */ jsxs13(
3668
+ return /* @__PURE__ */ jsxs14(
3457
3669
  "button",
3458
3670
  {
3459
3671
  type: "button",
@@ -3465,8 +3677,8 @@ function CandidateRow({
3465
3677
  disabled,
3466
3678
  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"}`,
3467
3679
  children: [
3468
- /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3469
- meta && /* @__PURE__ */ jsx17("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3680
+ /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3681
+ meta && /* @__PURE__ */ jsx18("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3470
3682
  ]
3471
3683
  }
3472
3684
  );
@@ -3563,26 +3775,26 @@ function CrossfadeModal({
3563
3775
  const fromLabel = fromName ?? fromSceneName ?? null;
3564
3776
  const toLabel = toName ?? toSceneName ?? null;
3565
3777
  if (!open) return null;
3566
- return /* @__PURE__ */ jsx17(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs13(
3778
+ return /* @__PURE__ */ jsx18(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs14(
3567
3779
  "div",
3568
3780
  {
3569
3781
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3570
3782
  onClick: (e) => e.stopPropagation(),
3571
3783
  "data-testid": `${testIdPrefix}-box`,
3572
3784
  children: [
3573
- /* @__PURE__ */ jsx17("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
3574
- /* @__PURE__ */ jsxs13("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3785
+ /* @__PURE__ */ jsx18("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
3786
+ /* @__PURE__ */ jsxs14("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3575
3787
  "Bridge a track from",
3576
3788
  " ",
3577
- /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3789
+ /* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3578
3790
  " into one from",
3579
3791
  " ",
3580
- /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3792
+ /* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3581
3793
  ". Both layers share one generated part; each keeps its own preset."
3582
3794
  ] }),
3583
- load.status === "loading" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3584
- load.status === "error" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3585
- load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs13(
3795
+ load.status === "loading" && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3796
+ load.status === "error" && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3797
+ load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs14(
3586
3798
  "div",
3587
3799
  {
3588
3800
  className: "text-xs text-sas-muted py-4 text-center",
@@ -3593,20 +3805,20 @@ function CrossfadeModal({
3593
3805
  ". Add one (or free one from another crossfade) first."
3594
3806
  ]
3595
3807
  }
3596
- ) : /* @__PURE__ */ jsxs13(Fragment5, { children: [
3597
- /* @__PURE__ */ jsxs13("div", { className: "block", children: [
3598
- /* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3808
+ ) : /* @__PURE__ */ jsxs14(Fragment6, { children: [
3809
+ /* @__PURE__ */ jsxs14("div", { className: "block", children: [
3810
+ /* @__PURE__ */ jsxs14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3599
3811
  "Origin ",
3600
3812
  fromLabel ? `(${fromLabel})` : "(top)"
3601
3813
  ] }),
3602
- /* @__PURE__ */ jsx17(
3814
+ /* @__PURE__ */ jsx18(
3603
3815
  "div",
3604
3816
  {
3605
3817
  role: "radiogroup",
3606
3818
  "aria-label": "Origin track",
3607
3819
  "data-testid": `${testIdPrefix}-origin-list`,
3608
3820
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3609
- children: originCandidates.map((t) => /* @__PURE__ */ jsx17(
3821
+ children: originCandidates.map((t) => /* @__PURE__ */ jsx18(
3610
3822
  CandidateRow,
3611
3823
  {
3612
3824
  track: t,
@@ -3620,23 +3832,23 @@ function CrossfadeModal({
3620
3832
  }
3621
3833
  )
3622
3834
  ] }),
3623
- /* @__PURE__ */ jsxs13("div", { className: "block", children: [
3624
- /* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3835
+ /* @__PURE__ */ jsxs14("div", { className: "block", children: [
3836
+ /* @__PURE__ */ jsxs14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3625
3837
  "Target ",
3626
3838
  toLabel ? `(${toLabel})` : "(bottom)"
3627
3839
  ] }),
3628
- targetCandidates.length === 0 ? /* @__PURE__ */ jsxs13("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3840
+ targetCandidates.length === 0 ? /* @__PURE__ */ jsxs14("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3629
3841
  "No available tracks in ",
3630
3842
  toLabel ?? "the target scene",
3631
3843
  " to crossfade into."
3632
- ] }) : /* @__PURE__ */ jsx17(
3844
+ ] }) : /* @__PURE__ */ jsx18(
3633
3845
  "div",
3634
3846
  {
3635
3847
  role: "radiogroup",
3636
3848
  "aria-label": "Target track",
3637
3849
  "data-testid": `${testIdPrefix}-target-list`,
3638
3850
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3639
- children: targetCandidates.map((t) => /* @__PURE__ */ jsx17(
3851
+ children: targetCandidates.map((t) => /* @__PURE__ */ jsx18(
3640
3852
  CandidateRow,
3641
3853
  {
3642
3854
  track: t,
@@ -3651,9 +3863,9 @@ function CrossfadeModal({
3651
3863
  )
3652
3864
  ] })
3653
3865
  ] })),
3654
- error && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3655
- /* @__PURE__ */ jsxs13("div", { className: "flex justify-end gap-2 pt-1", children: [
3656
- /* @__PURE__ */ jsx17(
3866
+ error && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3867
+ /* @__PURE__ */ jsxs14("div", { className: "flex justify-end gap-2 pt-1", children: [
3868
+ /* @__PURE__ */ jsx18(
3657
3869
  "button",
3658
3870
  {
3659
3871
  ref: cancelRef,
@@ -3664,7 +3876,7 @@ function CrossfadeModal({
3664
3876
  children: "Cancel"
3665
3877
  }
3666
3878
  ),
3667
- /* @__PURE__ */ jsx17(
3879
+ /* @__PURE__ */ jsx18(
3668
3880
  "button",
3669
3881
  {
3670
3882
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3888,7 +4100,7 @@ function dbIdsFromKeys(keys) {
3888
4100
  }
3889
4101
 
3890
4102
  // src/components/TransitionDesigner.tsx
3891
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
4103
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
3892
4104
  var CROSSFADE_ESTIMATE_MS = 15e3;
3893
4105
  var FADE_ESTIMATE_MS = 11e3;
3894
4106
  var CREATE_ALL_CONCURRENCY = 5;
@@ -3909,9 +4121,12 @@ function TransitionDesigner({
3909
4121
  onCreateCrossfade,
3910
4122
  onCreateFade,
3911
4123
  onCreateAudioTransition,
4124
+ mapColumnSubjects,
4125
+ fadeEstimateMs,
3912
4126
  familyLabel,
3913
4127
  testIdPrefix = "transition-designer"
3914
4128
  }) {
4129
+ const fadeOnly = !onCreateCrossfade;
3915
4130
  const [load, setLoad] = useState13({ status: "loading" });
3916
4131
  const [fromName, setFromName] = useState13(null);
3917
4132
  const [toName, setToName] = useState13(null);
@@ -3956,9 +4171,17 @@ function TransitionDesigner({
3956
4171
  }
3957
4172
  setLoad({ status: "loading" });
3958
4173
  try {
3959
- const [origin, target, fName, tName, draftRaw] = await Promise.all([
4174
+ let [origin, target] = await Promise.all([
3960
4175
  host.listSceneFamilyTracks(fromSceneId),
3961
- host.listSceneFamilyTracks(toSceneId),
4176
+ host.listSceneFamilyTracks(toSceneId)
4177
+ ]);
4178
+ if (mapColumnSubjects) {
4179
+ [origin, target] = await Promise.all([
4180
+ mapColumnSubjects(fromSceneId, origin),
4181
+ mapColumnSubjects(toSceneId, target)
4182
+ ]);
4183
+ }
4184
+ const [fName, tName, draftRaw] = await Promise.all([
3962
4185
  host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
3963
4186
  host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
3964
4187
  host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
@@ -3983,7 +4206,7 @@ function TransitionDesigner({
3983
4206
  message: err instanceof Error ? err.message : "Failed to load tracks."
3984
4207
  });
3985
4208
  }
3986
- }, [host, fromSceneId, toSceneId, transitionSceneId]);
4209
+ }, [host, fromSceneId, toSceneId, transitionSceneId, mapColumnSubjects]);
3987
4210
  useEffect10(() => {
3988
4211
  void refresh();
3989
4212
  }, [refresh]);
@@ -4054,13 +4277,31 @@ function TransitionDesigner({
4054
4277
  [originSlots, targetSlots, mutate]
4055
4278
  );
4056
4279
  const rows = useMemo6(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
4280
+ const fadeOnlyRows = useMemo6(
4281
+ () => fadeOnly ? [
4282
+ ...originPool.map((t) => ({
4283
+ originId: t.dbId,
4284
+ targetId: null,
4285
+ type: "fade-out"
4286
+ })),
4287
+ ...targetPool.map((t) => ({
4288
+ originId: null,
4289
+ targetId: t.dbId,
4290
+ type: "fade-in"
4291
+ }))
4292
+ ] : [],
4293
+ [fadeOnly, originPool, targetPool]
4294
+ );
4295
+ const fadeOnlyRowsRef = useRef11(fadeOnlyRows);
4296
+ fadeOnlyRowsRef.current = fadeOnlyRows;
4297
+ const effectiveRows = fadeOnly ? fadeOnlyRows : rows;
4057
4298
  const creatingDbIds = useMemo6(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
4058
4299
  const eligibleCount = useMemo6(
4059
- () => rows.filter((r) => {
4300
+ () => effectiveRows.filter((r) => {
4060
4301
  const k = rowKey(r);
4061
4302
  return k !== null && !creatingKeys.has(k);
4062
4303
  }).length,
4063
- [rows, creatingKeys]
4304
+ [effectiveRows, creatingKeys]
4064
4305
  );
4065
4306
  const createRow = useCallback9(
4066
4307
  async (row) => {
@@ -4078,6 +4319,7 @@ function TransitionDesigner({
4078
4319
  const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
4079
4320
  const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
4080
4321
  if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
4322
+ if (!onCreateCrossfade) throw new Error("This panel does not support crossfades.");
4081
4323
  await onCreateCrossfade(
4082
4324
  { dbId: o.dbId, name: o.name, role: o.role },
4083
4325
  { dbId: t.dbId, name: t.name, role: t.role }
@@ -4117,7 +4359,8 @@ function TransitionDesigner({
4117
4359
  [onCreateCrossfade, onCreateFade, onCreateAudioTransition]
4118
4360
  );
4119
4361
  const createAll = useCallback9(async () => {
4120
- const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
4362
+ const source = fadeOnly ? fadeOnlyRowsRef.current : buildRowSlots(originSlotsRef.current, targetSlotsRef.current);
4363
+ const eligible = source.filter((r) => {
4121
4364
  const k = rowKey(r);
4122
4365
  return k !== null && !creatingKeysRef.current.has(k);
4123
4366
  });
@@ -4133,7 +4376,7 @@ function TransitionDesigner({
4133
4376
  await Promise.all(
4134
4377
  Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
4135
4378
  );
4136
- }, [createRow]);
4379
+ }, [createRow, fadeOnly]);
4137
4380
  const fromLabel = fromName ?? "origin";
4138
4381
  const toLabel = toName ?? "target";
4139
4382
  const cellDragProps = (col, index, locked) => ({
@@ -4184,15 +4427,15 @@ function TransitionDesigner({
4184
4427
  const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
4185
4428
  const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
4186
4429
  if (slotId === null) {
4187
- return /* @__PURE__ */ jsxs14(
4430
+ return /* @__PURE__ */ jsxs15(
4188
4431
  "div",
4189
4432
  {
4190
4433
  ...cellDragProps(col, index, false),
4191
4434
  "data-testid": `${testIdPrefix}-${col}-gap-${index}`,
4192
4435
  className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
4193
4436
  children: [
4194
- /* @__PURE__ */ jsx18("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4195
- /* @__PURE__ */ jsx18(
4437
+ /* @__PURE__ */ jsx19("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4438
+ /* @__PURE__ */ jsx19(
4196
4439
  "button",
4197
4440
  {
4198
4441
  type: "button",
@@ -4209,7 +4452,7 @@ function TransitionDesigner({
4209
4452
  }
4210
4453
  const primary = track ? track.prompt?.trim() || track.name : slotId;
4211
4454
  const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
4212
- return /* @__PURE__ */ jsx18(
4455
+ return /* @__PURE__ */ jsx19(
4213
4456
  "div",
4214
4457
  {
4215
4458
  ...cellDragProps(col, index, locked),
@@ -4217,13 +4460,13 @@ function TransitionDesigner({
4217
4460
  "data-value": slotId,
4218
4461
  className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
4219
4462
  title: track ? track.dbId : "Track no longer available",
4220
- children: /* @__PURE__ */ jsxs14("div", { className: "flex items-start gap-1", children: [
4221
- /* @__PURE__ */ jsx18("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4222
- /* @__PURE__ */ jsxs14("div", { className: "min-w-0 flex-1", children: [
4223
- /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-text truncate", children: primary }),
4224
- meta && /* @__PURE__ */ jsx18("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4463
+ children: /* @__PURE__ */ jsxs15("div", { className: "flex items-start gap-1", children: [
4464
+ /* @__PURE__ */ jsx19("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4465
+ /* @__PURE__ */ jsxs15("div", { className: "min-w-0 flex-1", children: [
4466
+ /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-text truncate", children: primary }),
4467
+ meta && /* @__PURE__ */ jsx19("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4225
4468
  ] }),
4226
- /* @__PURE__ */ jsx18(
4469
+ /* @__PURE__ */ jsx19(
4227
4470
  "button",
4228
4471
  {
4229
4472
  type: "button",
@@ -4239,22 +4482,159 @@ function TransitionDesigner({
4239
4482
  }
4240
4483
  );
4241
4484
  };
4242
- return /* @__PURE__ */ jsxs14("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4243
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4244
- /* @__PURE__ */ jsxs14("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4245
- /* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: fromLabel }),
4485
+ const renderFadeOnlyRow = (row, i) => {
4486
+ const slotId = row.originId ?? row.targetId;
4487
+ const byId = row.type === "fade-out" ? originById : targetById;
4488
+ const track = byId.get(slotId);
4489
+ const key = rowKey(row);
4490
+ const isCreatingThis = key !== null && creatingKeys.has(key);
4491
+ const errMsg = key !== null ? rowErrors[key] : void 0;
4492
+ return /* @__PURE__ */ jsxs15(
4493
+ "div",
4494
+ {
4495
+ "data-testid": `${testIdPrefix}-row-${i}`,
4496
+ className: "grid grid-cols-[1fr_auto] gap-2 items-center",
4497
+ children: [
4498
+ /* @__PURE__ */ jsxs15(
4499
+ "div",
4500
+ {
4501
+ "data-testid": `${testIdPrefix}-subject-${slotId}`,
4502
+ className: `rounded-sm border border-sas-border bg-sas-panel px-2 py-1.5 ${isCreatingThis ? "opacity-60" : ""}`,
4503
+ title: track ? track.dbId : "Track no longer available",
4504
+ children: [
4505
+ /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-text truncate", children: track ? track.prompt?.trim() || track.name : slotId }),
4506
+ /* @__PURE__ */ jsx19("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing" })
4507
+ ]
4508
+ }
4509
+ ),
4510
+ /* @__PURE__ */ jsxs15("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4511
+ /* @__PURE__ */ jsx19(
4512
+ "span",
4513
+ {
4514
+ "data-testid": `${testIdPrefix}-type-${i}`,
4515
+ className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
4516
+ children: row.type ? TYPE_LABEL[row.type] : "\u2014"
4517
+ }
4518
+ ),
4519
+ isCreatingThis ? /* @__PURE__ */ jsx19("div", { className: "w-full", children: /* @__PURE__ */ jsx19(
4520
+ SorceryProgressBar,
4521
+ {
4522
+ isLoading: true,
4523
+ heightClass: "h-5",
4524
+ statusText: "CREATING",
4525
+ estimatedDurationMs: fadeEstimateMs ?? FADE_ESTIMATE_MS
4526
+ }
4527
+ ) }) : /* @__PURE__ */ jsx19(
4528
+ "button",
4529
+ {
4530
+ type: "button",
4531
+ "data-testid": `${testIdPrefix}-create-${i}`,
4532
+ onClick: () => createRow(row),
4533
+ 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",
4534
+ children: "Create"
4535
+ }
4536
+ ),
4537
+ errMsg && /* @__PURE__ */ jsx19(
4538
+ "span",
4539
+ {
4540
+ "data-testid": `${testIdPrefix}-row-error-${i}`,
4541
+ className: "text-[10px] text-sas-danger text-center leading-tight",
4542
+ children: errMsg
4543
+ }
4544
+ )
4545
+ ] })
4546
+ ]
4547
+ },
4548
+ slotId
4549
+ );
4550
+ };
4551
+ if (fadeOnly) {
4552
+ const outRows = fadeOnlyRows.filter((r) => r.type === "fade-out");
4553
+ const inRows = fadeOnlyRows.filter((r) => r.type === "fade-in");
4554
+ return /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4555
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4556
+ /* @__PURE__ */ jsxs15("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4557
+ /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: fromLabel }),
4558
+ " \u2192",
4559
+ " ",
4560
+ /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: toLabel }),
4561
+ familyLabel ? ` \xB7 ${familyLabel}` : "",
4562
+ " \xB7 each entry fades on its own \u2014 origin fades out, target fades in."
4563
+ ] }),
4564
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 shrink-0", children: [
4565
+ creatingKeys.size > 0 && /* @__PURE__ */ jsxs15(
4566
+ "span",
4567
+ {
4568
+ className: "text-[10px] text-sas-accent whitespace-nowrap",
4569
+ "data-testid": `${testIdPrefix}-creating-count`,
4570
+ children: [
4571
+ creatingKeys.size,
4572
+ " creating\u2026"
4573
+ ]
4574
+ }
4575
+ ),
4576
+ /* @__PURE__ */ jsxs15(
4577
+ "button",
4578
+ {
4579
+ type: "button",
4580
+ "data-testid": `${testIdPrefix}-create-all`,
4581
+ onClick: createAll,
4582
+ disabled: eligibleCount === 0,
4583
+ title: "Create every fade at once (runs several concurrently)",
4584
+ 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"}`,
4585
+ children: [
4586
+ "Create all",
4587
+ eligibleCount > 0 ? ` (${eligibleCount})` : ""
4588
+ ]
4589
+ }
4590
+ )
4591
+ ] })
4592
+ ] }),
4593
+ load.status === "loading" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4594
+ load.status === "error" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4595
+ load.status === "ready" && (fadeOnlyRows.length === 0 ? /* @__PURE__ */ jsxs15("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4596
+ "Nothing to fade in this panel for either scene. Add tracks to ",
4597
+ fromLabel,
4598
+ " or ",
4599
+ toLabel,
4600
+ " ",
4601
+ "first (or free one by deleting an existing fade)."
4602
+ ] }) : /* @__PURE__ */ jsxs15("div", { className: "space-y-3", children: [
4603
+ outRows.length > 0 && /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-out-section`, children: [
4604
+ /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4605
+ "Origin (",
4606
+ fromLabel,
4607
+ ") \u2014 fades out"
4608
+ ] }),
4609
+ outRows.map((row, i) => renderFadeOnlyRow(row, i))
4610
+ ] }),
4611
+ inRows.length > 0 && /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-in-section`, children: [
4612
+ /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4613
+ "Target (",
4614
+ toLabel,
4615
+ ") \u2014 fades in"
4616
+ ] }),
4617
+ inRows.map((row, j) => renderFadeOnlyRow(row, outRows.length + j))
4618
+ ] })
4619
+ ] }))
4620
+ ] });
4621
+ }
4622
+ return /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4623
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4624
+ /* @__PURE__ */ jsxs15("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4625
+ /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: fromLabel }),
4246
4626
  " \u2192",
4247
4627
  " ",
4248
- /* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: toLabel }),
4628
+ /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: toLabel }),
4249
4629
  familyLabel ? ` \xB7 ${familyLabel}` : "",
4250
4630
  " \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
4251
4631
  ] }),
4252
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2 shrink-0", children: [
4253
- creatingKeys.size > 0 && /* @__PURE__ */ jsxs14("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4632
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 shrink-0", children: [
4633
+ creatingKeys.size > 0 && /* @__PURE__ */ jsxs15("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4254
4634
  creatingKeys.size,
4255
4635
  " creating\u2026"
4256
4636
  ] }),
4257
- /* @__PURE__ */ jsxs14(
4637
+ /* @__PURE__ */ jsxs15(
4258
4638
  "button",
4259
4639
  {
4260
4640
  type: "button",
@@ -4271,49 +4651,49 @@ function TransitionDesigner({
4271
4651
  )
4272
4652
  ] })
4273
4653
  ] }),
4274
- /* @__PURE__ */ jsxs14("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4275
- /* @__PURE__ */ jsxs14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4654
+ /* @__PURE__ */ jsxs15("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4655
+ /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4276
4656
  "Origin (",
4277
4657
  fromLabel,
4278
4658
  ")"
4279
4659
  ] }),
4280
- /* @__PURE__ */ jsx18("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4281
- /* @__PURE__ */ jsxs14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4660
+ /* @__PURE__ */ jsx19("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4661
+ /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4282
4662
  "Target (",
4283
4663
  toLabel,
4284
4664
  ")"
4285
4665
  ] })
4286
4666
  ] }),
4287
- load.status === "loading" && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4288
- load.status === "error" && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4289
- load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ jsxs14("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4667
+ load.status === "loading" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4668
+ load.status === "error" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4669
+ load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ jsxs15("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4290
4670
  "No tracks to arrange in this panel for either scene. Add tracks to ",
4291
4671
  fromLabel,
4292
4672
  " or ",
4293
4673
  toLabel,
4294
4674
  " ",
4295
4675
  "first (or free one by deleting an existing crossfade/fade)."
4296
- ] }) : /* @__PURE__ */ jsx18("div", { className: "space-y-2", children: rows.map((row, i) => {
4676
+ ] }) : /* @__PURE__ */ jsx19("div", { className: "space-y-2", children: rows.map((row, i) => {
4297
4677
  const key = rowKey(row);
4298
4678
  const isCreatingThis = key !== null && creatingKeys.has(key);
4299
4679
  const errMsg = key !== null ? rowErrors[key] : void 0;
4300
- return /* @__PURE__ */ jsxs14(
4680
+ return /* @__PURE__ */ jsxs15(
4301
4681
  "div",
4302
4682
  {
4303
4683
  "data-testid": `${testIdPrefix}-row-${i}`,
4304
4684
  className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
4305
4685
  children: [
4306
4686
  renderCell("origin", i, row.originId),
4307
- /* @__PURE__ */ jsxs14("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4308
- !row.type ? /* @__PURE__ */ jsx18("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ jsx18(
4687
+ /* @__PURE__ */ jsxs15("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4688
+ !row.type ? /* @__PURE__ */ jsx19("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ jsx19(
4309
4689
  "span",
4310
4690
  {
4311
4691
  "data-testid": `${testIdPrefix}-type-${i}`,
4312
4692
  className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
4313
4693
  children: TYPE_LABEL[row.type]
4314
4694
  }
4315
- ) : audioEffectsEnabled ? /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4316
- /* @__PURE__ */ jsx18(
4695
+ ) : audioEffectsEnabled ? /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4696
+ /* @__PURE__ */ jsx19(
4317
4697
  "select",
4318
4698
  {
4319
4699
  "data-testid": `${testIdPrefix}-effect-${i}`,
@@ -4323,11 +4703,11 @@ function TransitionDesigner({
4323
4703
  if (id) setRowEffect(id, e.target.value);
4324
4704
  },
4325
4705
  className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
4326
- children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx18("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4706
+ children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx19("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4327
4707
  }
4328
4708
  ),
4329
- /* @__PURE__ */ jsx18("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4330
- ] }) : /* @__PURE__ */ jsx18(
4709
+ /* @__PURE__ */ jsx19("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4710
+ ] }) : /* @__PURE__ */ jsx19(
4331
4711
  "span",
4332
4712
  {
4333
4713
  "data-testid": `${testIdPrefix}-type-${i}`,
@@ -4335,7 +4715,7 @@ function TransitionDesigner({
4335
4715
  children: TYPE_LABEL[row.type]
4336
4716
  }
4337
4717
  ),
4338
- isCreatingThis ? /* @__PURE__ */ jsx18("div", { className: "w-full", children: /* @__PURE__ */ jsx18(
4718
+ isCreatingThis ? /* @__PURE__ */ jsx19("div", { className: "w-full", children: /* @__PURE__ */ jsx19(
4339
4719
  SorceryProgressBar,
4340
4720
  {
4341
4721
  isLoading: true,
@@ -4343,7 +4723,7 @@ function TransitionDesigner({
4343
4723
  statusText: "CREATING",
4344
4724
  estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
4345
4725
  }
4346
- ) }) : /* @__PURE__ */ jsx18(
4726
+ ) }) : /* @__PURE__ */ jsx19(
4347
4727
  "button",
4348
4728
  {
4349
4729
  type: "button",
@@ -4354,7 +4734,7 @@ function TransitionDesigner({
4354
4734
  children: "Create"
4355
4735
  }
4356
4736
  ),
4357
- errMsg && /* @__PURE__ */ jsx18(
4737
+ errMsg && /* @__PURE__ */ jsx19(
4358
4738
  "span",
4359
4739
  {
4360
4740
  "data-testid": `${testIdPrefix}-row-error-${i}`,
@@ -4374,7 +4754,7 @@ function TransitionDesigner({
4374
4754
 
4375
4755
  // src/components/PanelMasterStrip.tsx
4376
4756
  import { useMemo as useMemo7, useState as useState14 } from "react";
4377
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
4757
+ import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
4378
4758
  function PanelMasterStrip({
4379
4759
  bus,
4380
4760
  levels = null,
@@ -4401,14 +4781,14 @@ function PanelMasterStrip({
4401
4781
  (fx) => fx.name.toLowerCase().includes(q) || fx.manufacturer.toLowerCase().includes(q)
4402
4782
  );
4403
4783
  }, [availableFx, search]);
4404
- return /* @__PURE__ */ jsxs15(
4784
+ return /* @__PURE__ */ jsxs16(
4405
4785
  "div",
4406
4786
  {
4407
4787
  "data-testid": "panel-master-strip",
4408
4788
  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" : ""}`,
4409
4789
  children: [
4410
- /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", children: [
4411
- /* @__PURE__ */ jsx19(
4790
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
4791
+ /* @__PURE__ */ jsx20(
4412
4792
  "span",
4413
4793
  {
4414
4794
  className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
@@ -4416,8 +4796,8 @@ function PanelMasterStrip({
4416
4796
  children: "BUS"
4417
4797
  }
4418
4798
  ),
4419
- /* @__PURE__ */ jsxs15("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
4420
- /* @__PURE__ */ jsx19(
4799
+ /* @__PURE__ */ jsxs16("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
4800
+ /* @__PURE__ */ jsx20(
4421
4801
  VolumeSlider,
4422
4802
  {
4423
4803
  value: dbToSlider(bus.volume),
@@ -4425,8 +4805,8 @@ function PanelMasterStrip({
4425
4805
  disabled
4426
4806
  }
4427
4807
  ),
4428
- /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
4429
- /* @__PURE__ */ jsx19(
4808
+ /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
4809
+ /* @__PURE__ */ jsx20(
4430
4810
  LevelMeter,
4431
4811
  {
4432
4812
  peakDb: levels?.leftDb ?? -120,
@@ -4436,7 +4816,7 @@ function PanelMasterStrip({
4436
4816
  "data-testid": "bus-meter-left"
4437
4817
  }
4438
4818
  ),
4439
- /* @__PURE__ */ jsx19(
4819
+ /* @__PURE__ */ jsx20(
4440
4820
  LevelMeter,
4441
4821
  {
4442
4822
  peakDb: levels?.rightDb ?? -120,
@@ -4447,7 +4827,7 @@ function PanelMasterStrip({
4447
4827
  )
4448
4828
  ] })
4449
4829
  ] }),
4450
- /* @__PURE__ */ jsx19(
4830
+ /* @__PURE__ */ jsx20(
4451
4831
  "button",
4452
4832
  {
4453
4833
  "data-testid": "bus-mute-button",
@@ -4458,7 +4838,7 @@ function PanelMasterStrip({
4458
4838
  children: "M"
4459
4839
  }
4460
4840
  ),
4461
- /* @__PURE__ */ jsx19(
4841
+ /* @__PURE__ */ jsx20(
4462
4842
  "button",
4463
4843
  {
4464
4844
  "data-testid": "bus-solo-button",
@@ -4469,14 +4849,14 @@ function PanelMasterStrip({
4469
4849
  children: "S"
4470
4850
  }
4471
4851
  ),
4472
- /* @__PURE__ */ jsx19("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ jsxs15(
4852
+ /* @__PURE__ */ jsx20("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ jsxs16(
4473
4853
  "span",
4474
4854
  {
4475
4855
  "data-testid": `bus-fx-chip-${fx.index}`,
4476
4856
  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"}`,
4477
4857
  title: `${fx.name}${fx.enabled ? "" : " (bypassed)"}`,
4478
4858
  children: [
4479
- /* @__PURE__ */ jsx19(
4859
+ /* @__PURE__ */ jsx20(
4480
4860
  "button",
4481
4861
  {
4482
4862
  "data-testid": `bus-fx-toggle-${fx.index}`,
@@ -4487,7 +4867,7 @@ function PanelMasterStrip({
4487
4867
  children: fx.enabled ? "\u25CF" : "\u25CB"
4488
4868
  }
4489
4869
  ),
4490
- onShowFxEditor ? /* @__PURE__ */ jsx19(
4870
+ onShowFxEditor ? /* @__PURE__ */ jsx20(
4491
4871
  "button",
4492
4872
  {
4493
4873
  "data-testid": `bus-fx-edit-${fx.index}`,
@@ -4497,8 +4877,8 @@ function PanelMasterStrip({
4497
4877
  title: `Open ${fx.name} editor`,
4498
4878
  children: fx.name
4499
4879
  }
4500
- ) : /* @__PURE__ */ jsx19("span", { className: "max-w-[80px] truncate", children: fx.name }),
4501
- /* @__PURE__ */ jsx19(
4880
+ ) : /* @__PURE__ */ jsx20("span", { className: "max-w-[80px] truncate", children: fx.name }),
4881
+ /* @__PURE__ */ jsx20(
4502
4882
  "button",
4503
4883
  {
4504
4884
  "data-testid": `bus-fx-remove-${fx.index}`,
@@ -4513,7 +4893,7 @@ function PanelMasterStrip({
4513
4893
  },
4514
4894
  `${fx.index}:${fx.pluginId}`
4515
4895
  )) }),
4516
- /* @__PURE__ */ jsx19(
4896
+ /* @__PURE__ */ jsx20(
4517
4897
  "button",
4518
4898
  {
4519
4899
  "data-testid": "bus-fx-add-button",
@@ -4525,9 +4905,9 @@ function PanelMasterStrip({
4525
4905
  }
4526
4906
  )
4527
4907
  ] }),
4528
- fxPickerOpen && /* @__PURE__ */ jsxs15("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
4529
- /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", children: [
4530
- /* @__PURE__ */ jsx19(
4908
+ fxPickerOpen && /* @__PURE__ */ jsxs16("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
4909
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
4910
+ /* @__PURE__ */ jsx20(
4531
4911
  "input",
4532
4912
  {
4533
4913
  type: "text",
@@ -4537,19 +4917,19 @@ function PanelMasterStrip({
4537
4917
  className: "sas-input flex-1 px-2 py-1 text-xs"
4538
4918
  }
4539
4919
  ),
4540
- onRefreshFx && /* @__PURE__ */ jsx19(
4920
+ onRefreshFx && /* @__PURE__ */ jsx20(
4541
4921
  "button",
4542
4922
  {
4543
4923
  onClick: () => onRefreshFx(),
4544
4924
  disabled: fxLoading,
4545
4925
  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",
4546
- title: "Re-scan plugins",
4926
+ title: "Re-scan plugins \u2014 picks up newly installed FX and retries any that failed a previous scan",
4547
4927
  children: fxLoading ? "..." : "Refresh"
4548
4928
  }
4549
4929
  )
4550
4930
  ] }),
4551
- fxLoading && availableFx.length === 0 ? /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs15("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
4552
- filtered.map((fx) => /* @__PURE__ */ jsxs15(
4931
+ fxLoading && availableFx.length === 0 ? /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs16("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
4932
+ filtered.map((fx) => /* @__PURE__ */ jsxs16(
4553
4933
  "button",
4554
4934
  {
4555
4935
  "data-testid": `bus-fx-pick-${fx.pluginId}`,
@@ -4557,13 +4937,13 @@ function PanelMasterStrip({
4557
4937
  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",
4558
4938
  title: `${fx.name} by ${fx.manufacturer} (${fx.type.toUpperCase()})`,
4559
4939
  children: [
4560
- /* @__PURE__ */ jsx19("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
4561
- /* @__PURE__ */ jsx19("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
4940
+ /* @__PURE__ */ jsx20("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
4941
+ /* @__PURE__ */ jsx20("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
4562
4942
  ]
4563
4943
  },
4564
4944
  fx.pluginId
4565
4945
  )),
4566
- filtered.length === 0 && /* @__PURE__ */ jsx19("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
4946
+ filtered.length === 0 && /* @__PURE__ */ jsx20("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
4567
4947
  ] })
4568
4948
  ] })
4569
4949
  ]
@@ -4623,12 +5003,12 @@ function usePanelBus(host, activeSceneId) {
4623
5003
  };
4624
5004
  }, [supported, activeSceneId, bus?.engaged, host]);
4625
5005
  const loadFxList = useCallback10(
4626
- async (force) => {
5006
+ async (opts) => {
4627
5007
  if (!supported || !host.getAvailableFx) return;
4628
- if (fxLoadedRef.current && !force) return;
5008
+ if (fxLoadedRef.current && !opts.force && !opts.rescan) return;
4629
5009
  setFxLoading(true);
4630
5010
  try {
4631
- const list = await host.getAvailableFx();
5011
+ const list = opts.rescan && host.rescanAvailableFx ? await host.rescanAvailableFx() : await host.getAvailableFx();
4632
5012
  setAvailableFx(list);
4633
5013
  fxLoadedRef.current = true;
4634
5014
  } catch {
@@ -4641,7 +5021,7 @@ function usePanelBus(host, activeSceneId) {
4641
5021
  const openPicker = useCallback10(
4642
5022
  (open) => {
4643
5023
  setFxPickerOpen(open);
4644
- if (open) void loadFxList(false);
5024
+ if (open) void loadFxList({});
4645
5025
  },
4646
5026
  [loadFxList]
4647
5027
  );
@@ -4666,7 +5046,7 @@ function usePanelBus(host, activeSceneId) {
4666
5046
  fxLoading,
4667
5047
  fxPickerOpen,
4668
5048
  setFxPickerOpen: openPicker,
4669
- refreshFx: () => void loadFxList(true),
5049
+ refreshFx: () => void loadFxList({ rescan: true }),
4670
5050
  reload,
4671
5051
  onVolumeChange: (volumeDb) => mutate(host.setPanelBusVolume && (() => host.setPanelBusVolume(activeSceneId, volumeDb))),
4672
5052
  onMuteToggle: () => mutate(
@@ -4690,7 +5070,7 @@ function usePanelBus(host, activeSceneId) {
4690
5070
 
4691
5071
  // src/components/DownloadPackButton.tsx
4692
5072
  import { useCallback as useCallback11, useEffect as useEffect12, useState as useState16 } from "react";
4693
- import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
5073
+ import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
4694
5074
  function formatSize(bytes) {
4695
5075
  if (!bytes || bytes <= 0) return "";
4696
5076
  const gb = bytes / 1024 ** 3;
@@ -4778,8 +5158,8 @@ var DownloadPackButton = ({
4778
5158
  } else {
4779
5159
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
4780
5160
  }
4781
- return /* @__PURE__ */ jsxs16("div", { children: [
4782
- /* @__PURE__ */ jsx20(
5161
+ return /* @__PURE__ */ jsxs17("div", { children: [
5162
+ /* @__PURE__ */ jsx21(
4783
5163
  "button",
4784
5164
  {
4785
5165
  "data-testid": `download-pack-button-${packId}`,
@@ -4790,12 +5170,12 @@ var DownloadPackButton = ({
4790
5170
  children: buttonLabel
4791
5171
  }
4792
5172
  ),
4793
- variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
5173
+ variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx21("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
4794
5174
  ] });
4795
5175
  };
4796
5176
 
4797
5177
  // src/components/SamplePackCTACard.tsx
4798
- import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
5178
+ import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
4799
5179
  var SamplePackCTACard = ({
4800
5180
  host,
4801
5181
  pack,
@@ -4803,7 +5183,7 @@ var SamplePackCTACard = ({
4803
5183
  onDownloadComplete
4804
5184
  }) => {
4805
5185
  if (status === "checking") {
4806
- return /* @__PURE__ */ jsx21(
5186
+ return /* @__PURE__ */ jsx22(
4807
5187
  "div",
4808
5188
  {
4809
5189
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -4814,16 +5194,16 @@ var SamplePackCTACard = ({
4814
5194
  }
4815
5195
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
4816
5196
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
4817
- return /* @__PURE__ */ jsxs17(
5197
+ return /* @__PURE__ */ jsxs18(
4818
5198
  "div",
4819
5199
  {
4820
5200
  "data-testid": `sample-pack-cta-${pack.packId}`,
4821
5201
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
4822
5202
  children: [
4823
- /* @__PURE__ */ jsx21("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
4824
- /* @__PURE__ */ jsx21("div", { className: "text-base text-sas-text mb-1", children: headline }),
4825
- /* @__PURE__ */ jsx21("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
4826
- /* @__PURE__ */ jsx21(
5203
+ /* @__PURE__ */ jsx22("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
5204
+ /* @__PURE__ */ jsx22("div", { className: "text-base text-sas-text mb-1", children: headline }),
5205
+ /* @__PURE__ */ jsx22("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
5206
+ /* @__PURE__ */ jsx22(
4827
5207
  DownloadPackButton,
4828
5208
  {
4829
5209
  host,
@@ -4903,7 +5283,7 @@ function drawWaveform(canvas, peaks, options = {}) {
4903
5283
  }
4904
5284
 
4905
5285
  // src/components/WaveformView.tsx
4906
- import { jsx as jsx22 } from "react/jsx-runtime";
5286
+ import { jsx as jsx23 } from "react/jsx-runtime";
4907
5287
  var WaveformView = ({
4908
5288
  host,
4909
5289
  filePath,
@@ -4951,7 +5331,7 @@ var WaveformView = ({
4951
5331
  observer.observe(canvas);
4952
5332
  return () => observer.disconnect();
4953
5333
  }, [peaks, fillStyle]);
4954
- return /* @__PURE__ */ jsx22(
5334
+ return /* @__PURE__ */ jsx23(
4955
5335
  "canvas",
4956
5336
  {
4957
5337
  ref: canvasRef,
@@ -4963,7 +5343,7 @@ var WaveformView = ({
4963
5343
 
4964
5344
  // src/components/ScrollingWaveform.tsx
4965
5345
  import { useEffect as useEffect14, useRef as useRef14 } from "react";
4966
- import { jsx as jsx23 } from "react/jsx-runtime";
5346
+ import { jsx as jsx24 } from "react/jsx-runtime";
4967
5347
  var ScrollingWaveform = ({
4968
5348
  getPeakDb,
4969
5349
  active,
@@ -5039,7 +5419,7 @@ var ScrollingWaveform = ({
5039
5419
  }
5040
5420
  };
5041
5421
  }, [active, getPeakDb, fillStyle]);
5042
- return /* @__PURE__ */ jsx23(
5422
+ return /* @__PURE__ */ jsx24(
5043
5423
  "canvas",
5044
5424
  {
5045
5425
  ref: canvasRef,
@@ -5051,7 +5431,7 @@ var ScrollingWaveform = ({
5051
5431
 
5052
5432
  // src/components/OffsetScrubber.tsx
5053
5433
  import { useCallback as useCallback12, useEffect as useEffect15, useMemo as useMemo8, useRef as useRef15, useState as useState18 } from "react";
5054
- import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
5434
+ import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
5055
5435
  var SLIDER_HEIGHT_PX = 28;
5056
5436
  var TICK_HEIGHT_PX = 14;
5057
5437
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -5166,9 +5546,9 @@ function OffsetScrubber({
5166
5546
  });
5167
5547
  }, [cuePoints, sampleToFraction]);
5168
5548
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
5169
- return /* @__PURE__ */ jsxs18("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
5170
- /* @__PURE__ */ jsx24("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
5171
- /* @__PURE__ */ jsxs18(
5549
+ return /* @__PURE__ */ jsxs19("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
5550
+ /* @__PURE__ */ jsx25("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
5551
+ /* @__PURE__ */ jsxs19(
5172
5552
  "div",
5173
5553
  {
5174
5554
  ref: trackRef,
@@ -5184,7 +5564,7 @@ function OffsetScrubber({
5184
5564
  "aria-valuenow": draftOffset,
5185
5565
  "aria-disabled": isDisabled,
5186
5566
  children: [
5187
- /* @__PURE__ */ jsx24(
5567
+ /* @__PURE__ */ jsx25(
5188
5568
  "div",
5189
5569
  {
5190
5570
  "aria-hidden": "true",
@@ -5192,7 +5572,7 @@ function OffsetScrubber({
5192
5572
  style: { left: "50%" }
5193
5573
  }
5194
5574
  ),
5195
- ticks.map((t) => /* @__PURE__ */ jsx24(
5575
+ ticks.map((t) => /* @__PURE__ */ jsx25(
5196
5576
  "div",
5197
5577
  {
5198
5578
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -5207,7 +5587,7 @@ function OffsetScrubber({
5207
5587
  },
5208
5588
  t.i
5209
5589
  )),
5210
- /* @__PURE__ */ jsx24(
5590
+ /* @__PURE__ */ jsx25(
5211
5591
  "div",
5212
5592
  {
5213
5593
  "data-testid": "offset-scrubber-thumb",
@@ -5224,7 +5604,7 @@ function OffsetScrubber({
5224
5604
  ]
5225
5605
  }
5226
5606
  ),
5227
- /* @__PURE__ */ jsx24(
5607
+ /* @__PURE__ */ jsx25(
5228
5608
  "span",
5229
5609
  {
5230
5610
  "data-testid": "offset-scrubber-readout",
@@ -5232,7 +5612,7 @@ function OffsetScrubber({
5232
5612
  children: formatOffset(draftOffset, sampleRate)
5233
5613
  }
5234
5614
  ),
5235
- /* @__PURE__ */ jsx24(
5615
+ /* @__PURE__ */ jsx25(
5236
5616
  "button",
5237
5617
  {
5238
5618
  type: "button",
@@ -5244,7 +5624,7 @@ function OffsetScrubber({
5244
5624
  children: "\u2316"
5245
5625
  }
5246
5626
  ),
5247
- bpmMismatch && /* @__PURE__ */ jsx24(
5627
+ bpmMismatch && /* @__PURE__ */ jsx25(
5248
5628
  "span",
5249
5629
  {
5250
5630
  "data-testid": "offset-bpm-mismatch",
@@ -5650,6 +6030,23 @@ function useTransitionOps({
5650
6030
  },
5651
6031
  [host]
5652
6032
  );
6033
+ const copyTrackFx = useCallback15(
6034
+ async (newTrackId, sourceDbId) => {
6035
+ if (typeof host.copyTrackFxFrom !== "function") return;
6036
+ try {
6037
+ const res = await host.copyTrackFxFrom(newTrackId, sourceDbId);
6038
+ if (res.externalMissing.length > 0) {
6039
+ host.showToast(
6040
+ "warning",
6041
+ "Some FX not copied",
6042
+ `Missing plugin(s): ${res.externalMissing.join(", ")}`
6043
+ );
6044
+ }
6045
+ } catch {
6046
+ }
6047
+ },
6048
+ [host]
6049
+ );
5653
6050
  const [isCreatingCrossfade, setIsCreatingCrossfade] = useState22(false);
5654
6051
  const handleCreateCrossfade = useCallback15(
5655
6052
  async (origin, target) => {
@@ -5728,6 +6125,8 @@ function useTransitionOps({
5728
6125
  };
5729
6126
  const originLabel = await copySound(top.id, origin.dbId);
5730
6127
  const targetLabel = await copySound(bottom.id, target.dbId);
6128
+ await copyTrackFx(top.id, origin.dbId);
6129
+ await copyTrackFx(bottom.id, target.dbId);
5731
6130
  await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
5732
6131
  const groupId = top.dbId;
5733
6132
  const originMeta = {
@@ -5779,6 +6178,7 @@ function useTransitionOps({
5779
6178
  loadTracks
5780
6179
  ]
5781
6180
  );
6181
+ const [isCreatingGroupFade, setIsCreatingGroupFade] = useState22(false);
5782
6182
  const [isCreatingFade, setIsCreatingFade] = useState22(false);
5783
6183
  const handleCreateFade = useCallback15(
5784
6184
  async (selection, direction, gesture) => {
@@ -5847,6 +6247,7 @@ function useTransitionOps({
5847
6247
  soundLabel = await adapter.sound.copySnapshot(track.id, snap);
5848
6248
  }
5849
6249
  }
6250
+ await copyTrackFx(track.id, selection.dbId);
5850
6251
  await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
5851
6252
  appliedFadeAutomationRef.current.add(track.id);
5852
6253
  const meta = {
@@ -6031,6 +6432,188 @@ function useTransitionOps({
6031
6432
  },
6032
6433
  [host, activeSceneId, applyFadeAutomation, setFadesMeta]
6033
6434
  );
6435
+ const handleCreateVerbatimGroupFade = useCallback15(
6436
+ async (subject, direction) => {
6437
+ const groupAdapter = adapter.transitionGroup;
6438
+ if (!groupAdapter) throw new Error("This panel does not support group fades.");
6439
+ const scene = activeSceneId;
6440
+ const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
6441
+ const toSceneId = sceneContext?.transitionToSceneId ?? "";
6442
+ if (!scene) throw new Error("No active scene.");
6443
+ if (!isConnected) throw new Error("Systems not connected.");
6444
+ const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
6445
+ const members = await groupAdapter.expandSubject(sourceSceneId, subject.dbId);
6446
+ if (members.length === 0) {
6447
+ throw new Error("Nothing to fade \u2014 the source group has no members.");
6448
+ }
6449
+ if (tracks.length + members.length > identity.maxTracks) {
6450
+ throw new Error(`Not enough track slots for this fade (needs ${members.length}).`);
6451
+ }
6452
+ setIsCreatingGroupFade(true);
6453
+ const created = [];
6454
+ try {
6455
+ const mc = await host.getMusicalContext();
6456
+ const sliderPos = groupAdapter.defaultSliderPos?.(direction) ?? 0.5;
6457
+ const gesture = "volume";
6458
+ const clipEnd = mc.bars * 4 * 60 / mc.bpm;
6459
+ const maxBeats = mc.bars * 4;
6460
+ for (const member of members) {
6461
+ const handle = await host.createTrack({
6462
+ name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}-v${member.memberIndex}`,
6463
+ ...adapter.createTrackOptions()
6464
+ });
6465
+ created.push({ handle, member, soundLabel: "default" });
6466
+ const role = member.role ?? subject.role ?? "";
6467
+ if (role) await host.setTrackRole(handle.id, role).catch(() => {
6468
+ });
6469
+ const srcMidi = host.readImportableTrackMidi ? await host.readImportableTrackMidi(member.dbId) : { clips: [] };
6470
+ const srcNotes = srcMidi.clips[0]?.notes ?? [];
6471
+ const notes = srcNotes.filter((n) => n.startBeat < maxBeats).map((n) => ({
6472
+ ...n,
6473
+ durationBeats: Math.min(n.durationBeats, maxBeats - n.startBeat)
6474
+ }));
6475
+ if (notes.length > 0) {
6476
+ const clip = { startTime: 0, endTime: clipEnd, tempo: mc.bpm, notes };
6477
+ await host.writeMidiClip(handle.id, clip);
6478
+ }
6479
+ if (host.getTrackSound) {
6480
+ const snap = await host.getTrackSound(member.dbId);
6481
+ if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
6482
+ created[created.length - 1].soundLabel = await adapter.sound.copySnapshot(
6483
+ handle.id,
6484
+ snap
6485
+ );
6486
+ }
6487
+ }
6488
+ await copyTrackFx(handle.id, member.dbId);
6489
+ await applyFadeAutomation(handle.id, direction, mc.bars, mc.bpm, sliderPos, gesture);
6490
+ appliedFadeAutomationRef.current.add(handle.id);
6491
+ }
6492
+ const groupId = created[0].handle.dbId;
6493
+ for (const { handle, member, soundLabel } of created) {
6494
+ const meta = {
6495
+ direction,
6496
+ gesture,
6497
+ sourceTrackDbId: member.dbId,
6498
+ sourceSceneId,
6499
+ sourceName: member.name,
6500
+ soundLabel,
6501
+ sliderPos,
6502
+ groupId,
6503
+ memberIndex: member.memberIndex,
6504
+ memberLabel: member.memberLabel
6505
+ };
6506
+ await host.setSceneData(scene, `track:${handle.dbId}:fade`, meta);
6507
+ }
6508
+ await groupAdapter.writeGroupMetas(
6509
+ scene,
6510
+ created.map(({ handle, member }) => ({ newDbId: handle.dbId, member })),
6511
+ groupId
6512
+ );
6513
+ await loadTracks(true);
6514
+ host.showToast(
6515
+ "success",
6516
+ direction === "in" ? "Fade in created" : "Fade out created",
6517
+ subject.name
6518
+ );
6519
+ } catch (err) {
6520
+ for (const { handle } of [...created].reverse()) {
6521
+ try {
6522
+ await host.deleteTrack(handle.id);
6523
+ } catch {
6524
+ }
6525
+ await host.deleteSceneData(scene, `track:${handle.dbId}:fade`).catch(() => {
6526
+ });
6527
+ for (const suffix of groupAdapter.cleanupKeySuffixes) {
6528
+ await host.deleteSceneData(scene, `track:${handle.dbId}:${suffix}`).catch(() => {
6529
+ });
6530
+ }
6531
+ }
6532
+ throw err instanceof Error ? err : new Error(String(err));
6533
+ } finally {
6534
+ setIsCreatingGroupFade(false);
6535
+ }
6536
+ },
6537
+ [
6538
+ host,
6539
+ adapter,
6540
+ identity,
6541
+ activeSceneId,
6542
+ isConnected,
6543
+ tracks.length,
6544
+ sceneContext,
6545
+ applyFadeAutomation,
6546
+ copyTrackFx,
6547
+ loadTracks
6548
+ ]
6549
+ );
6550
+ const groupFadeSliderTimers = useRef18({});
6551
+ const handleGroupFadeSlider = useCallback15(
6552
+ (group, pos) => {
6553
+ setFadesMeta(
6554
+ (prev) => prev.map(
6555
+ (f) => f.meta.groupId === group.groupId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f
6556
+ )
6557
+ );
6558
+ if (groupFadeSliderTimers.current[group.groupId]) {
6559
+ clearTimeout(groupFadeSliderTimers.current[group.groupId]);
6560
+ }
6561
+ groupFadeSliderTimers.current[group.groupId] = setTimeout(() => {
6562
+ void (async () => {
6563
+ const mc = await host.getMusicalContext();
6564
+ const sceneData = activeSceneId ? await host.getAllSceneData(activeSceneId) : null;
6565
+ for (const member of group.members) {
6566
+ await applyFadeAutomation(
6567
+ member.track.handle.id,
6568
+ group.direction,
6569
+ mc.bars,
6570
+ mc.bpm,
6571
+ pos,
6572
+ group.gesture
6573
+ );
6574
+ if (activeSceneId && sceneData) {
6575
+ const meta = asFadeMeta(sceneData[`track:${member.dbId}:fade`]);
6576
+ if (meta) {
6577
+ host.setSceneData(activeSceneId, `track:${member.dbId}:fade`, {
6578
+ ...meta,
6579
+ sliderPos: pos
6580
+ }).catch(() => {
6581
+ });
6582
+ }
6583
+ }
6584
+ }
6585
+ })();
6586
+ }, 200);
6587
+ },
6588
+ [host, activeSceneId, applyFadeAutomation, setFadesMeta]
6589
+ );
6590
+ const handleGroupFadeDelete = useCallback15(
6591
+ async (group) => {
6592
+ const suffixes = ["fade", ...adapter.transitionGroup?.cleanupKeySuffixes ?? []];
6593
+ try {
6594
+ for (const member of group.members) {
6595
+ await host.deleteTrack(member.track.handle.id);
6596
+ if (activeSceneId) {
6597
+ for (const suffix of suffixes) {
6598
+ await host.deleteSceneData(activeSceneId, `track:${member.dbId}:${suffix}`).catch(() => {
6599
+ });
6600
+ }
6601
+ }
6602
+ }
6603
+ setFadesMeta((prev) => prev.filter((f) => f.meta.groupId !== group.groupId));
6604
+ const gone = new Set(group.members.map((m) => m.track.handle.id));
6605
+ setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
6606
+ host.showToast("success", "Fade removed");
6607
+ } catch (err) {
6608
+ host.showToast(
6609
+ "error",
6610
+ "Failed to delete fade",
6611
+ err instanceof Error ? err.message : String(err)
6612
+ );
6613
+ }
6614
+ },
6615
+ [host, adapter, activeSceneId, setFadesMeta, setTracks]
6616
+ );
6034
6617
  const lastResyncKeyRef = useRef18("");
6035
6618
  useEffect17(() => {
6036
6619
  if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
@@ -6102,12 +6685,16 @@ function useTransitionOps({
6102
6685
  handleCrossfadeDelete,
6103
6686
  handleCrossfadeSlider,
6104
6687
  handleFadeDelete,
6105
- handleFadeSlider
6688
+ handleFadeSlider,
6689
+ isCreatingGroupFade,
6690
+ handleCreateVerbatimGroupFade,
6691
+ handleGroupFadeSlider,
6692
+ handleGroupFadeDelete
6106
6693
  };
6107
6694
  }
6108
6695
 
6109
6696
  // src/panel-core/useGeneratorPanelCore.tsx
6110
- import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
6697
+ import { jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
6111
6698
  var EMPTY_PLACEHOLDERS = [];
6112
6699
  function useGeneratorPanelCore({
6113
6700
  ui,
@@ -6590,8 +7177,8 @@ function useGeneratorPanelCore({
6590
7177
  if (!onHeaderContent) return;
6591
7178
  const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
6592
7179
  onHeaderContent(
6593
- /* @__PURE__ */ jsxs19("div", { className: "flex gap-1 items-center", children: [
6594
- features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx25(
7180
+ /* @__PURE__ */ jsxs20("div", { className: "flex gap-1 items-center", children: [
7181
+ features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx26(
6595
7182
  "button",
6596
7183
  {
6597
7184
  "data-testid": `import-from-scene-${identity.familyKey}-button`,
@@ -6605,7 +7192,7 @@ function useGeneratorPanelCore({
6605
7192
  children: identity.importTrackLabel ?? "Import Track"
6606
7193
  }
6607
7194
  ),
6608
- (!canCrossfade || !designerView) && /* @__PURE__ */ jsx25(
7195
+ (!canCrossfade || !designerView) && /* @__PURE__ */ jsx26(
6609
7196
  "button",
6610
7197
  {
6611
7198
  "data-testid": `add-${identity.familyKey}-track-button`,
@@ -6621,7 +7208,7 @@ function useGeneratorPanelCore({
6621
7208
  children: identity.addTrackLabel ?? "Add Track"
6622
7209
  }
6623
7210
  ),
6624
- canCrossfade && /* @__PURE__ */ jsxs19(
7211
+ canCrossfade && /* @__PURE__ */ jsxs20(
6625
7212
  "button",
6626
7213
  {
6627
7214
  "data-testid": `${identity.familyKey}-view-toggle`,
@@ -6640,7 +7227,7 @@ function useGeneratorPanelCore({
6640
7227
  title: designerView ? "Back to the track list" : "Open the transition designer",
6641
7228
  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",
6642
7229
  children: [
6643
- transitionSourceTotal > 0 && /* @__PURE__ */ jsx25(
7230
+ transitionSourceTotal > 0 && /* @__PURE__ */ jsx26(
6644
7231
  "span",
6645
7232
  {
6646
7233
  className: "absolute inset-y-0 left-0 bg-sas-accent/25",
@@ -6648,7 +7235,7 @@ function useGeneratorPanelCore({
6648
7235
  "aria-hidden": true
6649
7236
  }
6650
7237
  ),
6651
- /* @__PURE__ */ jsxs19("span", { className: "relative", children: [
7238
+ /* @__PURE__ */ jsxs20("span", { className: "relative", children: [
6652
7239
  "\u21C4 ",
6653
7240
  designerView ? "Transition" : "Tracks",
6654
7241
  transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
@@ -7206,6 +7793,10 @@ function useGeneratorPanelCore({
7206
7793
  }
7207
7794
  return { resolvedFades: list, fadeMemberDbIds: members };
7208
7795
  }, [tracks, fadesMeta]);
7796
+ const { singles: resolvedSingleFades, groups: resolvedGroupFades } = useMemo10(
7797
+ () => splitFadeEntries(resolvedFades),
7798
+ [resolvedFades]
7799
+ );
7209
7800
  const transition = useTransitionOps({
7210
7801
  host,
7211
7802
  adapter,
@@ -7342,6 +7933,8 @@ function useGeneratorPanelCore({
7342
7933
  crossfadeMemberDbIds,
7343
7934
  resolvedFades,
7344
7935
  fadeMemberDbIds,
7936
+ resolvedSingleFades,
7937
+ resolvedGroupFades,
7345
7938
  resolvedGenericGroups,
7346
7939
  genericGroupMemberDbIds,
7347
7940
  availableInstruments,
@@ -7379,8 +7972,8 @@ function useGeneratorPanelCore({
7379
7972
  }
7380
7973
 
7381
7974
  // src/panel-core/GeneratorPanelShell.tsx
7382
- import React22, { useCallback as useCallback17 } from "react";
7383
- import { Fragment as Fragment6, jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
7975
+ import React23, { useCallback as useCallback17 } from "react";
7976
+ import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
7384
7977
  function GeneratorPanelShell({ core, slots }) {
7385
7978
  const {
7386
7979
  ui,
@@ -7409,7 +8002,8 @@ function GeneratorPanelShell({ core, slots }) {
7409
8002
  fadesMeta,
7410
8003
  resolvedCrossfadePairs,
7411
8004
  crossfadeMemberDbIds,
7412
- resolvedFades,
8005
+ resolvedSingleFades,
8006
+ resolvedGroupFades,
7413
8007
  fadeMemberDbIds,
7414
8008
  resolvedGenericGroups,
7415
8009
  genericGroupMemberDbIds,
@@ -7535,12 +8129,12 @@ function GeneratorPanelShell({ core, slots }) {
7535
8129
  ]
7536
8130
  );
7537
8131
  if (!activeSceneId) {
7538
- return /* @__PURE__ */ jsx26(
8132
+ return /* @__PURE__ */ jsx27(
7539
8133
  "div",
7540
8134
  {
7541
8135
  "data-testid": `no-scene-placeholder-${identity.familyKey}`,
7542
8136
  className: "flex items-center justify-center py-8",
7543
- children: /* @__PURE__ */ jsx26(
8137
+ children: /* @__PURE__ */ jsx27(
7544
8138
  "button",
7545
8139
  {
7546
8140
  onClick: () => onSelectScene?.(),
@@ -7552,12 +8146,12 @@ function GeneratorPanelShell({ core, slots }) {
7552
8146
  );
7553
8147
  }
7554
8148
  if (!sceneContext?.hasContract) {
7555
- return /* @__PURE__ */ jsx26(
8149
+ return /* @__PURE__ */ jsx27(
7556
8150
  "div",
7557
8151
  {
7558
8152
  "data-testid": `no-contract-placeholder-${identity.familyKey}`,
7559
8153
  className: "flex items-center justify-center py-8",
7560
- children: /* @__PURE__ */ jsx26(
8154
+ children: /* @__PURE__ */ jsx27(
7561
8155
  "button",
7562
8156
  {
7563
8157
  onClick: () => onOpenContract?.(),
@@ -7569,7 +8163,7 @@ function GeneratorPanelShell({ core, slots }) {
7569
8163
  );
7570
8164
  }
7571
8165
  if (features.bulkComposePlaceholders && isComposing) {
7572
- return /* @__PURE__ */ jsx26("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx26(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
8166
+ return /* @__PURE__ */ jsx27("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx27(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
7573
8167
  }
7574
8168
  const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
7575
8169
  if (activePlaceholders.length > 0) {
@@ -7580,18 +8174,18 @@ function GeneratorPanelShell({ core, slots }) {
7580
8174
  tracksByDbId.set(t.handle.id, t);
7581
8175
  }
7582
8176
  }
7583
- return /* @__PURE__ */ jsx26("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
8177
+ return /* @__PURE__ */ jsx27("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
7584
8178
  const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
7585
8179
  if (loadedTrack) {
7586
- return /* @__PURE__ */ jsx26(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
8180
+ return /* @__PURE__ */ jsx27(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
7587
8181
  }
7588
- return /* @__PURE__ */ jsx26(
8182
+ return /* @__PURE__ */ jsx27(
7589
8183
  "div",
7590
8184
  {
7591
8185
  "data-testid": "bulk-placeholder-track",
7592
8186
  className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
7593
8187
  style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
7594
- children: /* @__PURE__ */ jsx26(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
8188
+ children: /* @__PURE__ */ jsx27(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
7595
8189
  },
7596
8190
  ph.id
7597
8191
  );
@@ -7603,13 +8197,13 @@ function GeneratorPanelShell({ core, slots }) {
7603
8197
  supportsMeters,
7604
8198
  levels: supportsMeters ? trackLevels : void 0,
7605
8199
  handlers,
7606
- renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx26(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
8200
+ renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx27(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
7607
8201
  setGroupMute,
7608
8202
  setGroupSolo,
7609
8203
  deleteGroup
7610
8204
  };
7611
- return /* @__PURE__ */ jsxs20("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
7612
- features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx26(
8205
+ return /* @__PURE__ */ jsxs21("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
8206
+ features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx27(
7613
8207
  ImportTrackModal,
7614
8208
  {
7615
8209
  host,
@@ -7622,7 +8216,7 @@ function GeneratorPanelShell({ core, slots }) {
7622
8216
  testIdPrefix: `${identity.familyKey}-import`
7623
8217
  }
7624
8218
  ),
7625
- features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx26(
8219
+ features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx27(
7626
8220
  ImportTrackModal,
7627
8221
  {
7628
8222
  host,
@@ -7637,7 +8231,7 @@ function GeneratorPanelShell({ core, slots }) {
7637
8231
  }
7638
8232
  ),
7639
8233
  slots?.modals,
7640
- canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx26("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx26(
8234
+ canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx27("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx27(
7641
8235
  TransitionDesigner,
7642
8236
  {
7643
8237
  host,
@@ -7648,14 +8242,16 @@ function GeneratorPanelShell({ core, slots }) {
7648
8242
  ...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
7649
8243
  ...fadesMeta.map((f) => f.meta.sourceTrackDbId)
7650
8244
  ],
7651
- onCreateCrossfade: transition.handleCreateCrossfade,
7652
- onCreateFade: transition.handleCreateFade,
8245
+ onCreateCrossfade: adapter.transitionGroup?.fadeOnly ? void 0 : transition.handleCreateCrossfade,
8246
+ onCreateFade: adapter.transitionGroup ? (sel, dir) => transition.handleCreateVerbatimGroupFade(sel, dir) : transition.handleCreateFade,
8247
+ mapColumnSubjects: adapter.transitionGroup ? (sceneId, tracks2) => adapter.transitionGroup.mapColumnSubjects(sceneId, tracks2) : void 0,
8248
+ fadeEstimateMs: adapter.transitionGroup?.fadeEstimateMs,
7653
8249
  familyLabel: identity.familyLabel,
7654
8250
  testIdPrefix: `${identity.familyKey}-transition-designer`
7655
8251
  }
7656
8252
  ) }),
7657
- !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx26("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs20(Fragment6, { children: [
7658
- panelBus.supported && panelBus.bus && /* @__PURE__ */ jsx26(
8253
+ !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx27("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs21(Fragment7, { children: [
8254
+ panelBus.supported && panelBus.bus && /* @__PURE__ */ jsx27(
7659
8255
  PanelMasterStrip,
7660
8256
  {
7661
8257
  bus: panelBus.bus,
@@ -7676,7 +8272,7 @@ function GeneratorPanelShell({ core, slots }) {
7676
8272
  }
7677
8273
  ),
7678
8274
  slots?.beforeRows,
7679
- resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx26(
8275
+ resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx27(
7680
8276
  CrossfadeTrackRow,
7681
8277
  {
7682
8278
  accentColor: identity.transitionAccentColor,
@@ -7713,7 +8309,7 @@ function GeneratorPanelShell({ core, slots }) {
7713
8309
  },
7714
8310
  pair.groupId
7715
8311
  )),
7716
- resolvedFades.map((fade) => /* @__PURE__ */ jsx26(
8312
+ resolvedSingleFades.map((fade) => /* @__PURE__ */ jsx27(
7717
8313
  FadeTrackRow,
7718
8314
  {
7719
8315
  accentColor: identity.transitionAccentColor,
@@ -7738,21 +8334,56 @@ function GeneratorPanelShell({ core, slots }) {
7738
8334
  },
7739
8335
  fade.dbId
7740
8336
  )),
8337
+ resolvedGroupFades.map((group) => /* @__PURE__ */ jsx27(
8338
+ GroupFadeTrackRow,
8339
+ {
8340
+ accentColor: identity.transitionAccentColor,
8341
+ levels: supportsMeters ? trackLevels : void 0,
8342
+ direction: group.direction,
8343
+ gesture: group.gesture,
8344
+ sliderPos: group.sliderPos,
8345
+ groupLabel: adapter.transitionGroup?.groupRowLabel?.(group.members.length) ?? `Group (${group.members.length} tracks)`,
8346
+ members: group.members.map((m) => ({
8347
+ trackId: m.track.handle.id,
8348
+ name: m.track.handle.name,
8349
+ role: m.track.role,
8350
+ sourceName: m.meta.sourceName,
8351
+ soundLabel: m.meta.soundLabel,
8352
+ memberLabel: m.meta.memberLabel,
8353
+ runtimeState: m.track.runtimeState
8354
+ })),
8355
+ onMemberMuteToggle: (trackId) => handlers.muteToggle(trackId),
8356
+ onMemberSoloToggle: (trackId) => handlers.soloToggle(trackId),
8357
+ onMemberVolumeChange: (trackId, vol) => handlers.volumeChange(trackId, vol),
8358
+ onMemberPanChange: (trackId, pan) => handlers.panChange(trackId, pan),
8359
+ onMuteAll: () => setGroupMute(
8360
+ group.members.map((m) => m.track.handle.id),
8361
+ !group.members.every((m) => m.track.runtimeState.muted)
8362
+ ),
8363
+ onSoloAll: () => setGroupSolo(
8364
+ group.members.map((m) => m.track.handle.id),
8365
+ !group.members.every((m) => m.track.runtimeState.solo)
8366
+ ),
8367
+ onSliderChange: (pos) => transition.handleGroupFadeSlider(group, pos),
8368
+ onDelete: () => transition.handleGroupFadeDelete(group)
8369
+ },
8370
+ group.groupId
8371
+ )),
7741
8372
  (adapter.groupExtensions ?? []).flatMap(
7742
- (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ jsx26(React22.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
8373
+ (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).filter((group) => !group.members.every((m) => fadeMemberDbIds.has(m.dbId))).map((group) => /* @__PURE__ */ jsx27(React23.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
7743
8374
  ),
7744
8375
  tracks.map((track, index) => {
7745
8376
  if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
7746
8377
  return null;
7747
8378
  }
7748
- return /* @__PURE__ */ jsx26(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
8379
+ return /* @__PURE__ */ jsx27(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
7749
8380
  }),
7750
8381
  slots?.afterRows
7751
8382
  ] })),
7752
8383
  features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
7753
8384
  const hasAnyMidi = tracks.some((t) => t.hasMidi);
7754
8385
  const exportDisabled = isExportingMidi || !hasAnyMidi;
7755
- return /* @__PURE__ */ jsx26("div", { className: "pt-2", children: /* @__PURE__ */ jsx26(
8386
+ return /* @__PURE__ */ jsx27("div", { className: "pt-2", children: /* @__PURE__ */ jsx27(
7756
8387
  "button",
7757
8388
  {
7758
8389
  "data-testid": "export-midi-tracks-button",
@@ -7820,7 +8451,7 @@ function createSurgeSoundAdapter(host, overrides = {}) {
7820
8451
  }
7821
8452
 
7822
8453
  // src/constants/sdk-version.ts
7823
- var PLUGIN_SDK_VERSION = "2.39.0";
8454
+ var PLUGIN_SDK_VERSION = "2.40.0";
7824
8455
 
7825
8456
  // src/utils/format-concurrent-tracks.ts
7826
8457
  function formatConcurrentTracks(ctx) {
@@ -7987,6 +8618,7 @@ export {
7987
8618
  FxToggleBar,
7988
8619
  GUTTER_W,
7989
8620
  GeneratorPanelShell,
8621
+ GroupFadeTrackRow,
7990
8622
  ImportTrackModal,
7991
8623
  TrackDrawer as InstrumentDrawer,
7992
8624
  LevelMeter,
@@ -8055,6 +8687,7 @@ export {
8055
8687
  sliderToDb,
8056
8688
  slotsEqual,
8057
8689
  soundIdentity,
8690
+ splitFadeEntries,
8058
8691
  synthesizeCuePoints,
8059
8692
  tokenizePrompt,
8060
8693
  trackDataKey,