@signalsandsorcery/plugin-sdk 2.35.1 → 2.35.3

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
@@ -65,11 +65,11 @@ var EMPTY_FX_DETAIL_STATE = {
65
65
  };
66
66
 
67
67
  // src/components/TrackRow.tsx
68
- import React8 from "react";
68
+ import React9 from "react";
69
69
  import { AlertCircle, ChevronDown, GripVertical } from "lucide-react";
70
70
 
71
71
  // src/components/TrackDrawer.tsx
72
- import { useState as useState2, useMemo as useMemo2 } from "react";
72
+ import { useState as useState4, useMemo as useMemo3 } from "react";
73
73
 
74
74
  // src/constants/fx-presets.ts
75
75
  var EQ_PRESETS = {
@@ -789,8 +789,250 @@ function PianoRollEditor({
789
789
  ] });
790
790
  }
791
791
 
792
- // src/components/TrackDrawer.tsx
792
+ // src/components/TrackExternalFxSection.tsx
793
+ import { useMemo as useMemo2, useState as useState3 } from "react";
794
+
795
+ // src/hooks/useTrackExternalFx.ts
796
+ import { useCallback as useCallback2, useEffect, useRef as useRef2, useState as useState2 } from "react";
797
+ function useTrackExternalFx(host, trackId) {
798
+ const supported = typeof host.getTrackExternalFx === "function";
799
+ const [fx, setFx] = useState2(null);
800
+ const [availableFx, setAvailableFx] = useState2([]);
801
+ const [fxLoading, setFxLoading] = useState2(false);
802
+ const [pickerOpen, setPickerOpen] = useState2(false);
803
+ const fxLoadedRef = useRef2(false);
804
+ const loadSeqRef = useRef2(0);
805
+ const reload = useCallback2(async () => {
806
+ if (!supported || !trackId || !host.getTrackExternalFx) {
807
+ setFx(null);
808
+ return;
809
+ }
810
+ const seq = ++loadSeqRef.current;
811
+ try {
812
+ const list = await host.getTrackExternalFx(trackId);
813
+ if (loadSeqRef.current === seq) setFx(list);
814
+ } catch {
815
+ }
816
+ }, [host, trackId, supported]);
817
+ useEffect(() => {
818
+ setFx(null);
819
+ setPickerOpen(false);
820
+ void reload();
821
+ }, [reload]);
822
+ const loadFxList = useCallback2(
823
+ async (force) => {
824
+ if (!supported || !host.getAvailableFx) return;
825
+ if (fxLoadedRef.current && !force) return;
826
+ setFxLoading(true);
827
+ try {
828
+ const list = await host.getAvailableFx();
829
+ setAvailableFx(list);
830
+ fxLoadedRef.current = true;
831
+ } catch {
832
+ } finally {
833
+ setFxLoading(false);
834
+ }
835
+ },
836
+ [host, supported]
837
+ );
838
+ const openPicker = useCallback2(
839
+ (open) => {
840
+ setPickerOpen(open);
841
+ if (open) void loadFxList(false);
842
+ },
843
+ [loadFxList]
844
+ );
845
+ const mutate = useCallback2(
846
+ (fn) => {
847
+ if (!fn || !trackId) return;
848
+ void (async () => {
849
+ try {
850
+ await fn();
851
+ } catch {
852
+ }
853
+ await reload();
854
+ })();
855
+ },
856
+ [trackId, reload]
857
+ );
858
+ return {
859
+ supported,
860
+ fx,
861
+ availableFx,
862
+ fxLoading,
863
+ pickerOpen,
864
+ setPickerOpen: openPicker,
865
+ refreshFx: () => void loadFxList(true),
866
+ reload,
867
+ onAddFx: (pluginId) => mutate(host.loadTrackExternalFx && (async () => {
868
+ await host.loadTrackExternalFx(trackId, pluginId);
869
+ })),
870
+ onRemoveFx: (fxIndex) => mutate(host.removeTrackExternalFx && (() => host.removeTrackExternalFx(trackId, fxIndex))),
871
+ onToggleFxEnabled: (fxIndex, enabled) => mutate(
872
+ host.setTrackExternalFxEnabled && (() => host.setTrackExternalFxEnabled(trackId, fxIndex, enabled))
873
+ ),
874
+ onShowFxEditor: (fxIndex) => mutate(
875
+ host.showTrackExternalFxEditor && (() => host.showTrackExternalFxEditor(trackId, fxIndex))
876
+ )
877
+ };
878
+ }
879
+
880
+ // src/components/TrackExternalFxSection.tsx
793
881
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
882
+ function TrackExternalFxSection({
883
+ host,
884
+ trackId,
885
+ disabled = false
886
+ }) {
887
+ const {
888
+ supported,
889
+ fx,
890
+ availableFx,
891
+ fxLoading,
892
+ pickerOpen,
893
+ setPickerOpen,
894
+ refreshFx,
895
+ onAddFx,
896
+ onRemoveFx,
897
+ onToggleFxEnabled,
898
+ onShowFxEditor
899
+ } = useTrackExternalFx(host, trackId);
900
+ const [search, setSearch] = useState3("");
901
+ const filtered = useMemo2(() => {
902
+ const q = search.trim().toLowerCase();
903
+ if (!q) return availableFx;
904
+ return availableFx.filter(
905
+ (candidate) => candidate.name.toLowerCase().includes(q) || candidate.manufacturer.toLowerCase().includes(q)
906
+ );
907
+ }, [availableFx, search]);
908
+ if (!supported) return null;
909
+ const entries = fx ?? [];
910
+ return /* @__PURE__ */ jsxs3(
911
+ "div",
912
+ {
913
+ "data-testid": "track-external-fx-section",
914
+ className: "flex flex-col gap-1.5 pt-2 mt-1 border-t border-sas-border/60",
915
+ children: [
916
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
917
+ /* @__PURE__ */ jsx3(
918
+ "span",
919
+ {
920
+ className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
921
+ title: "Third-party FX inserts (VST3/AU) on this track, before its fader. Settings persist with the project.",
922
+ children: "3RD-PARTY FX"
923
+ }
924
+ ),
925
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1 flex-1 min-w-0 overflow-x-auto", children: [
926
+ entries.map((entry) => /* @__PURE__ */ jsxs3(
927
+ "span",
928
+ {
929
+ "data-testid": `track-fx-chip-${entry.index}`,
930
+ className: `flex items-center gap-1 px-1.5 py-0.5 rounded-sm border text-[10px] whitespace-nowrap ${entry.enabled ? "border-sas-accent/60 text-sas-accent bg-sas-accent/10" : "border-sas-border text-sas-muted/50 bg-sas-panel"}`,
931
+ title: `${entry.name}${entry.enabled ? "" : " (bypassed)"}`,
932
+ children: [
933
+ /* @__PURE__ */ jsx3(
934
+ "button",
935
+ {
936
+ "data-testid": `track-fx-toggle-${entry.index}`,
937
+ onClick: () => onToggleFxEnabled(entry.index, !entry.enabled),
938
+ disabled,
939
+ className: "hover:opacity-70 disabled:opacity-50",
940
+ title: entry.enabled ? `Bypass ${entry.name}` : `Enable ${entry.name}`,
941
+ children: entry.enabled ? "\u25CF" : "\u25CB"
942
+ }
943
+ ),
944
+ /* @__PURE__ */ jsx3(
945
+ "button",
946
+ {
947
+ "data-testid": `track-fx-edit-${entry.index}`,
948
+ onClick: () => onShowFxEditor(entry.index),
949
+ disabled,
950
+ className: "max-w-[110px] truncate hover:underline disabled:opacity-50",
951
+ title: `Open ${entry.name} editor`,
952
+ children: entry.name
953
+ }
954
+ ),
955
+ /* @__PURE__ */ jsx3(
956
+ "button",
957
+ {
958
+ "data-testid": `track-fx-remove-${entry.index}`,
959
+ onClick: () => onRemoveFx(entry.index),
960
+ disabled,
961
+ className: "text-sas-muted/60 hover:text-sas-danger disabled:opacity-50",
962
+ title: `Remove ${entry.name} from this track`,
963
+ children: "\u2715"
964
+ }
965
+ )
966
+ ]
967
+ },
968
+ `${entry.index}:${entry.pluginId}`
969
+ )),
970
+ entries.length === 0 && /* @__PURE__ */ jsx3("span", { className: "text-[10px] text-sas-muted/40 select-none", children: "none" })
971
+ ] }),
972
+ /* @__PURE__ */ jsx3(
973
+ "button",
974
+ {
975
+ "data-testid": "track-fx-add-button",
976
+ onClick: () => setPickerOpen(!pickerOpen),
977
+ disabled,
978
+ className: `px-1.5 py-0.5 rounded-sm border text-xs whitespace-nowrap transition-colors ${pickerOpen ? "border-sas-accent text-sas-accent bg-sas-accent/10" : "border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"} disabled:opacity-50`,
979
+ title: pickerOpen ? "Close the FX picker" : "Add a VST3/AU FX plugin to this track",
980
+ children: pickerOpen ? "FX \u25B4" : "FX +"
981
+ }
982
+ )
983
+ ] }),
984
+ pickerOpen && /* @__PURE__ */ jsxs3("div", { "data-testid": "track-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
985
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
986
+ /* @__PURE__ */ jsx3(
987
+ "input",
988
+ {
989
+ type: "text",
990
+ value: search,
991
+ onChange: (e) => setSearch(e.target.value),
992
+ placeholder: "Search FX...",
993
+ className: "sas-input flex-1 px-2 py-1 text-xs"
994
+ }
995
+ ),
996
+ /* @__PURE__ */ jsx3(
997
+ "button",
998
+ {
999
+ onClick: () => refreshFx(),
1000
+ disabled: fxLoading,
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",
1003
+ children: fxLoading ? "..." : "Refresh"
1004
+ }
1005
+ )
1006
+ ] }),
1007
+ fxLoading && availableFx.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
1008
+ filtered.map((candidate) => /* @__PURE__ */ jsxs3(
1009
+ "button",
1010
+ {
1011
+ "data-testid": `track-fx-pick-${candidate.pluginId}`,
1012
+ onClick: () => {
1013
+ onAddFx(candidate.pluginId);
1014
+ setPickerOpen(false);
1015
+ },
1016
+ disabled,
1017
+ 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 disabled:opacity-50",
1018
+ title: `${candidate.name} by ${candidate.manufacturer} (${candidate.type.toUpperCase()})`,
1019
+ children: [
1020
+ /* @__PURE__ */ jsx3("span", { className: "text-xs font-medium truncate w-full", children: candidate.name }),
1021
+ /* @__PURE__ */ jsx3("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: candidate.manufacturer || candidate.type.toUpperCase() })
1022
+ ]
1023
+ },
1024
+ candidate.pluginId
1025
+ )),
1026
+ filtered.length === 0 && /* @__PURE__ */ jsx3("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
1027
+ ] })
1028
+ ] })
1029
+ ]
1030
+ }
1031
+ );
1032
+ }
1033
+
1034
+ // src/components/TrackDrawer.tsx
1035
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
794
1036
  var TAB_LABELS = {
795
1037
  fx: "FX",
796
1038
  pick: "Pick",
@@ -807,6 +1049,7 @@ function TrackDrawer({
807
1049
  onFxPresetChange,
808
1050
  onFxDryWetChange,
809
1051
  fxDisabled = false,
1052
+ externalFxHost,
810
1053
  instruments = [],
811
1054
  currentPluginId = null,
812
1055
  isLoading = false,
@@ -829,13 +1072,13 @@ function TrackDrawer({
829
1072
  editSnap,
830
1073
  onAuditionNote
831
1074
  }) {
832
- const [search, setSearch] = useState2("");
1075
+ const [search, setSearch] = useState4("");
833
1076
  const fxEnabled = !!onFxToggle;
834
1077
  const pickEnabled = !!onSelect;
835
1078
  const historyEnabled = !!onRestoreSound;
836
1079
  const importEnabled = !!onImportSound;
837
1080
  const editEnabled = !!onNotesChange;
838
- const enabledTabs = useMemo2(() => {
1081
+ const enabledTabs = useMemo3(() => {
839
1082
  const tabs = [];
840
1083
  if (fxEnabled) tabs.push("fx");
841
1084
  if (pickEnabled) tabs.push("pick");
@@ -845,7 +1088,7 @@ function TrackDrawer({
845
1088
  return tabs;
846
1089
  }, [fxEnabled, pickEnabled, historyEnabled, importEnabled, editEnabled]);
847
1090
  const SURGE_XT_DEFAULT_ID = "Surge XT";
848
- const filtered = useMemo2(() => {
1091
+ const filtered = useMemo3(() => {
849
1092
  let all = instruments.filter((i) => i.name !== "Surge XT");
850
1093
  if (search.trim()) {
851
1094
  const q = search.toLowerCase();
@@ -865,12 +1108,12 @@ function TrackDrawer({
865
1108
  const history = soundHistory ?? [];
866
1109
  const effectiveTab = enabledTabs.includes(activeTab) ? activeTab : enabledTabs[0] ?? "fx";
867
1110
  const tabClass = (active) => `px-2 py-0.5 text-xs rounded-sm transition-colors ${active ? "bg-sas-accent/20 text-sas-accent font-medium" : "text-sas-muted hover:text-sas-accent"}`;
868
- const strip = enabledTabs.length > 1 ? /* @__PURE__ */ jsx3(
1111
+ const strip = enabledTabs.length > 1 ? /* @__PURE__ */ jsx4(
869
1112
  "div",
870
1113
  {
871
1114
  className: "flex items-center gap-1 border-b border-sas-border pb-1",
872
1115
  "data-testid": "sdk-drawer-tabs",
873
- children: enabledTabs.map((tab) => /* @__PURE__ */ jsx3(
1116
+ children: enabledTabs.map((tab) => /* @__PURE__ */ jsx4(
874
1117
  "button",
875
1118
  {
876
1119
  type: "button",
@@ -884,9 +1127,9 @@ function TrackDrawer({
884
1127
  }
885
1128
  ) : null;
886
1129
  const currentSound = soundHistoryCursor >= 0 && soundHistoryCursor < history.length ? history[soundHistoryCursor].label : null;
887
- const header = strip || currentSound ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-1", "data-testid": "sdk-drawer-header", children: [
1130
+ const header = strip || currentSound ? /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1", "data-testid": "sdk-drawer-header", children: [
888
1131
  strip,
889
- currentSound && /* @__PURE__ */ jsx3(
1132
+ currentSound && /* @__PURE__ */ jsx4(
890
1133
  "span",
891
1134
  {
892
1135
  className: "text-[10px] text-sas-muted/60 truncate px-0.5",
@@ -896,9 +1139,9 @@ function TrackDrawer({
896
1139
  )
897
1140
  ] }) : null;
898
1141
  if (effectiveTab === "edit") {
899
- return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-edit", children: [
1142
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-edit", children: [
900
1143
  header,
901
- /* @__PURE__ */ jsx3(
1144
+ /* @__PURE__ */ jsx4(
902
1145
  PianoRollEditor,
903
1146
  {
904
1147
  notes: editNotes ?? [],
@@ -913,9 +1156,9 @@ function TrackDrawer({
913
1156
  ] });
914
1157
  }
915
1158
  if (effectiveTab === "fx") {
916
- return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-fx", children: [
1159
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-fx", children: [
917
1160
  header,
918
- /* @__PURE__ */ jsx3(
1161
+ /* @__PURE__ */ jsx4(
919
1162
  FxToggleBar,
920
1163
  {
921
1164
  trackId,
@@ -925,20 +1168,21 @@ function TrackDrawer({
925
1168
  onDryWetChange: (_t, category, value) => onFxDryWetChange?.(category, value),
926
1169
  disabled: fxDisabled
927
1170
  }
928
- )
1171
+ ),
1172
+ externalFxHost && /* @__PURE__ */ jsx4(TrackExternalFxSection, { host: externalFxHost, trackId, disabled: fxDisabled })
929
1173
  ] });
930
1174
  }
931
1175
  if (effectiveTab === "import") {
932
1176
  const soundNoun = /preset/i.test(importSoundLabel ?? "") ? "preset" : /sample/i.test(importSoundLabel ?? "") ? "sample" : "sound";
933
- return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-import", children: [
1177
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-import", children: [
934
1178
  header,
935
- /* @__PURE__ */ jsxs3("p", { className: "text-[11px] text-sas-muted/70 leading-snug", children: [
1179
+ /* @__PURE__ */ jsxs4("p", { className: "text-[11px] text-sas-muted/70 leading-snug", children: [
936
1180
  "Copy the sound from a matching track in another scene \u2014 your MIDI stays, only the",
937
1181
  " ",
938
1182
  soundNoun,
939
1183
  " changes."
940
1184
  ] }),
941
- /* @__PURE__ */ jsxs3(
1185
+ /* @__PURE__ */ jsxs4(
942
1186
  "button",
943
1187
  {
944
1188
  type: "button",
@@ -956,16 +1200,16 @@ function TrackDrawer({
956
1200
  }
957
1201
  if (effectiveTab === "history") {
958
1202
  const order = history.map((_, i) => i).reverse();
959
- return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-2", children: [
1203
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
960
1204
  header,
961
- history.length === 0 ? /* @__PURE__ */ jsx3(
1205
+ history.length === 0 ? /* @__PURE__ */ jsx4(
962
1206
  "div",
963
1207
  {
964
1208
  className: "text-xs text-sas-muted/60 text-center py-3",
965
1209
  "data-testid": "sdk-history-empty",
966
1210
  children: "No sounds yet \u2014 shuffle to build history."
967
1211
  }
968
- ) : /* @__PURE__ */ jsx3(
1212
+ ) : /* @__PURE__ */ jsx4(
969
1213
  "ul",
970
1214
  {
971
1215
  className: "flex flex-col gap-1 max-h-[160px] overflow-y-auto",
@@ -973,8 +1217,8 @@ function TrackDrawer({
973
1217
  children: order.map((i) => {
974
1218
  const entry = history[i];
975
1219
  const isCurrent = i === soundHistoryCursor;
976
- return /* @__PURE__ */ jsxs3("li", { className: "flex items-center gap-1", children: [
977
- /* @__PURE__ */ jsxs3(
1220
+ return /* @__PURE__ */ jsxs4("li", { className: "flex items-center gap-1", children: [
1221
+ /* @__PURE__ */ jsxs4(
978
1222
  "button",
979
1223
  {
980
1224
  type: "button",
@@ -984,12 +1228,12 @@ function TrackDrawer({
984
1228
  className: `flex-1 min-w-0 flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${isCurrent ? "border-sas-accent bg-sas-accent/20 text-sas-accent cursor-default" : "border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
985
1229
  title: isCurrent ? "Current sound" : `Restore: ${entry.label}`,
986
1230
  children: [
987
- /* @__PURE__ */ jsx3("span", { className: "truncate", children: entry.label }),
988
- /* @__PURE__ */ jsx3("span", { className: "text-[10px] text-sas-muted/60 flex-shrink-0 ml-2", children: isCurrent ? "\u25CF current" : "restore" })
1231
+ /* @__PURE__ */ jsx4("span", { className: "truncate", children: entry.label }),
1232
+ /* @__PURE__ */ jsx4("span", { className: "text-[10px] text-sas-muted/60 flex-shrink-0 ml-2", children: isCurrent ? "\u25CF current" : "restore" })
989
1233
  ]
990
1234
  }
991
1235
  ),
992
- onToggleFavorite && /* @__PURE__ */ jsx3(
1236
+ onToggleFavorite && /* @__PURE__ */ jsx4(
993
1237
  "button",
994
1238
  {
995
1239
  type: "button",
@@ -1007,10 +1251,10 @@ function TrackDrawer({
1007
1251
  ] });
1008
1252
  }
1009
1253
  if (effectiveTab === "pick" && editorStage) {
1010
- return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-2", children: [
1254
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
1011
1255
  header,
1012
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1013
- /* @__PURE__ */ jsx3(
1256
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
1257
+ /* @__PURE__ */ jsx4(
1014
1258
  "button",
1015
1259
  {
1016
1260
  onClick: () => onBackToInstruments?.(),
@@ -1018,9 +1262,9 @@ function TrackDrawer({
1018
1262
  children: "\u2190 Back"
1019
1263
  }
1020
1264
  ),
1021
- /* @__PURE__ */ jsx3("span", { className: "text-xs text-sas-muted font-medium truncate flex-1", children: selectedInstrumentName ?? "Plugin" })
1265
+ /* @__PURE__ */ jsx4("span", { className: "text-xs text-sas-muted font-medium truncate flex-1", children: selectedInstrumentName ?? "Plugin" })
1022
1266
  ] }),
1023
- /* @__PURE__ */ jsx3(
1267
+ /* @__PURE__ */ jsx4(
1024
1268
  "button",
1025
1269
  {
1026
1270
  onClick: () => onShowEditor?.(),
@@ -1032,10 +1276,10 @@ function TrackDrawer({
1032
1276
  }
1033
1277
  const isDefaultSelected = currentPluginId === null;
1034
1278
  const isSelected = (pluginId) => pluginId === currentPluginId;
1035
- return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-2", children: [
1279
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
1036
1280
  header,
1037
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1038
- /* @__PURE__ */ jsx3(
1281
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
1282
+ /* @__PURE__ */ jsx4(
1039
1283
  "input",
1040
1284
  {
1041
1285
  type: "text",
@@ -1045,7 +1289,7 @@ function TrackDrawer({
1045
1289
  className: "sas-input flex-1 px-2 py-1 text-xs"
1046
1290
  }
1047
1291
  ),
1048
- /* @__PURE__ */ jsx3(
1292
+ /* @__PURE__ */ jsx4(
1049
1293
  "button",
1050
1294
  {
1051
1295
  onClick: () => onRefresh?.(),
@@ -1056,54 +1300,54 @@ function TrackDrawer({
1056
1300
  }
1057
1301
  )
1058
1302
  ] }),
1059
- isLoading && instruments.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
1060
- /* @__PURE__ */ jsxs3(
1303
+ isLoading && instruments.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs4("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
1304
+ /* @__PURE__ */ jsxs4(
1061
1305
  "button",
1062
1306
  {
1063
1307
  onClick: () => onSelect?.(SURGE_XT_DEFAULT_ID),
1064
1308
  className: `flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${isDefaultSelected ? "border-sas-accent bg-sas-accent/20 text-sas-accent" : "border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
1065
1309
  title: "Surge XT \u2014 Default instrument",
1066
1310
  children: [
1067
- /* @__PURE__ */ jsxs3("span", { className: "text-xs font-medium truncate w-full", children: [
1311
+ /* @__PURE__ */ jsxs4("span", { className: "text-xs font-medium truncate w-full", children: [
1068
1312
  isDefaultSelected && "\u2713 ",
1069
1313
  "Surge XT"
1070
1314
  ] }),
1071
- /* @__PURE__ */ jsx3("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: "Default" })
1315
+ /* @__PURE__ */ jsx4("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: "Default" })
1072
1316
  ]
1073
1317
  },
1074
1318
  "__surge-xt-default__"
1075
1319
  ),
1076
1320
  filtered.map((inst) => {
1077
1321
  const selected = isSelected(inst.pluginId);
1078
- return /* @__PURE__ */ jsxs3(
1322
+ return /* @__PURE__ */ jsxs4(
1079
1323
  "button",
1080
1324
  {
1081
1325
  onClick: () => onSelect?.(inst.pluginId),
1082
1326
  className: `flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${selected ? "border-sas-accent bg-sas-accent/20 text-sas-accent" : inst.missing ? "border-amber-500/50 bg-amber-500/10 text-amber-400 hover:border-amber-500" : "border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
1083
1327
  title: `${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? " \u2014 MISSING" : ""}`,
1084
1328
  children: [
1085
- /* @__PURE__ */ jsxs3("span", { className: "text-xs font-medium truncate w-full", children: [
1329
+ /* @__PURE__ */ jsxs4("span", { className: "text-xs font-medium truncate w-full", children: [
1086
1330
  selected && "\u2713 ",
1087
1331
  inst.name
1088
1332
  ] }),
1089
- /* @__PURE__ */ jsx3("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: inst.manufacturer || inst.type.toUpperCase() })
1333
+ /* @__PURE__ */ jsx4("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: inst.manufacturer || inst.type.toUpperCase() })
1090
1334
  ]
1091
1335
  },
1092
1336
  inst.pluginId
1093
1337
  );
1094
1338
  }),
1095
- filtered.length === 0 && /* @__PURE__ */ jsx3("div", { className: "col-span-2 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No other plugins found" })
1339
+ filtered.length === 0 && /* @__PURE__ */ jsx4("div", { className: "col-span-2 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No other plugins found" })
1096
1340
  ] })
1097
1341
  ] });
1098
1342
  }
1099
1343
 
1100
1344
  // src/components/ConfirmDialog.tsx
1101
- import { useRef as useRef2 } from "react";
1345
+ import { useRef as useRef3 } from "react";
1102
1346
 
1103
1347
  // src/components/Modal.tsx
1104
- import { useEffect } from "react";
1348
+ import { useEffect as useEffect2 } from "react";
1105
1349
  import { createPortal } from "react-dom";
1106
- import { jsx as jsx4 } from "react/jsx-runtime";
1350
+ import { jsx as jsx5 } from "react/jsx-runtime";
1107
1351
  function Modal({
1108
1352
  open,
1109
1353
  onClose,
@@ -1113,7 +1357,7 @@ function Modal({
1113
1357
  closeOnEscape = true,
1114
1358
  initialFocusRef
1115
1359
  }) {
1116
- useEffect(() => {
1360
+ useEffect2(() => {
1117
1361
  if (!open) return void 0;
1118
1362
  const onKey = (e) => {
1119
1363
  if (closeOnEscape && e.key === "Escape") {
@@ -1127,7 +1371,7 @@ function Modal({
1127
1371
  }, [open, onClose, closeOnEscape, initialFocusRef]);
1128
1372
  if (!open) return null;
1129
1373
  return createPortal(
1130
- /* @__PURE__ */ jsx4(
1374
+ /* @__PURE__ */ jsx5(
1131
1375
  "div",
1132
1376
  {
1133
1377
  className: "fixed inset-0 z-[1000] flex items-center justify-center bg-black/60",
@@ -1141,7 +1385,7 @@ function Modal({
1141
1385
  }
1142
1386
 
1143
1387
  // src/components/ConfirmDialog.tsx
1144
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1388
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1145
1389
  function ConfirmDialog({
1146
1390
  open,
1147
1391
  title,
@@ -1153,8 +1397,8 @@ function ConfirmDialog({
1153
1397
  onCancel,
1154
1398
  testIdPrefix = "confirm-dialog"
1155
1399
  }) {
1156
- const cancelRef = useRef2(null);
1157
- return /* @__PURE__ */ jsx5(Modal, { open, onClose: onCancel, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs4(
1400
+ const cancelRef = useRef3(null);
1401
+ return /* @__PURE__ */ jsx6(Modal, { open, onClose: onCancel, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs5(
1158
1402
  "div",
1159
1403
  {
1160
1404
  className: "w-[360px] max-w-[90vw] flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
@@ -1164,8 +1408,8 @@ function ConfirmDialog({
1164
1408
  "aria-label": title,
1165
1409
  "data-testid": `${testIdPrefix}-modal`,
1166
1410
  children: [
1167
- /* @__PURE__ */ jsx5("div", { className: "px-4 py-3 border-b border-sas-border", children: /* @__PURE__ */ jsx5("span", { className: "text-sm font-medium text-sas-text", "data-testid": `${testIdPrefix}-title`, children: title }) }),
1168
- /* @__PURE__ */ jsx5(
1411
+ /* @__PURE__ */ jsx6("div", { className: "px-4 py-3 border-b border-sas-border", children: /* @__PURE__ */ jsx6("span", { className: "text-sm font-medium text-sas-text", "data-testid": `${testIdPrefix}-title`, children: title }) }),
1412
+ /* @__PURE__ */ jsx6(
1169
1413
  "div",
1170
1414
  {
1171
1415
  className: "px-4 py-3 text-xs text-sas-muted leading-relaxed break-words",
@@ -1173,8 +1417,8 @@ function ConfirmDialog({
1173
1417
  children: message
1174
1418
  }
1175
1419
  ),
1176
- /* @__PURE__ */ jsxs4("div", { className: "flex justify-end gap-2 px-4 py-3 border-t border-sas-border", children: [
1177
- /* @__PURE__ */ jsx5(
1420
+ /* @__PURE__ */ jsxs5("div", { className: "flex justify-end gap-2 px-4 py-3 border-t border-sas-border", children: [
1421
+ /* @__PURE__ */ jsx6(
1178
1422
  "button",
1179
1423
  {
1180
1424
  ref: cancelRef,
@@ -1185,7 +1429,7 @@ function ConfirmDialog({
1185
1429
  children: cancelLabel
1186
1430
  }
1187
1431
  ),
1188
- /* @__PURE__ */ jsx5(
1432
+ /* @__PURE__ */ jsx6(
1189
1433
  "button",
1190
1434
  {
1191
1435
  type: "button",
@@ -1202,7 +1446,7 @@ function ConfirmDialog({
1202
1446
  }
1203
1447
 
1204
1448
  // src/components/LevelMeter.tsx
1205
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1449
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1206
1450
  var COLOR_GREEN = "#2BD576";
1207
1451
  var COLOR_ORANGE = "#F5A623";
1208
1452
  var COLOR_RED = "#FF4D5E";
@@ -1230,7 +1474,7 @@ var LevelMeter = ({
1230
1474
  const widthPct = active ? dbToPct(peakDb) : 0;
1231
1475
  const showPeak = peakHoldDb != null && active && peakHoldDb > -60;
1232
1476
  const peakHoldPct = showPeak ? dbToPct(peakHoldDb) : 0;
1233
- return /* @__PURE__ */ jsxs5(
1477
+ return /* @__PURE__ */ jsxs6(
1234
1478
  "div",
1235
1479
  {
1236
1480
  className: `sas-level-meter ${className ?? ""}`,
@@ -1241,7 +1485,7 @@ var LevelMeter = ({
1241
1485
  gap: compact ? 0 : 6
1242
1486
  },
1243
1487
  children: [
1244
- /* @__PURE__ */ jsxs5(
1488
+ /* @__PURE__ */ jsxs6(
1245
1489
  "div",
1246
1490
  {
1247
1491
  style: {
@@ -1255,8 +1499,8 @@ var LevelMeter = ({
1255
1499
  minWidth: compact ? 0 : 60
1256
1500
  },
1257
1501
  children: [
1258
- /* @__PURE__ */ jsx6("div", { style: { position: "absolute", inset: 0, background: METER_GRADIENT } }),
1259
- /* @__PURE__ */ jsx6(
1502
+ /* @__PURE__ */ jsx7("div", { style: { position: "absolute", inset: 0, background: METER_GRADIENT } }),
1503
+ /* @__PURE__ */ jsx7(
1260
1504
  "div",
1261
1505
  {
1262
1506
  style: {
@@ -1270,7 +1514,7 @@ var LevelMeter = ({
1270
1514
  }
1271
1515
  }
1272
1516
  ),
1273
- /* @__PURE__ */ jsx6(
1517
+ /* @__PURE__ */ jsx7(
1274
1518
  "div",
1275
1519
  {
1276
1520
  "data-testid": `${id}-segments`,
@@ -1283,7 +1527,7 @@ var LevelMeter = ({
1283
1527
  }
1284
1528
  }
1285
1529
  ),
1286
- showPeak && /* @__PURE__ */ jsx6(
1530
+ showPeak && /* @__PURE__ */ jsx7(
1287
1531
  "div",
1288
1532
  {
1289
1533
  "data-testid": `${id}-peak`,
@@ -1304,7 +1548,7 @@ var LevelMeter = ({
1304
1548
  ]
1305
1549
  }
1306
1550
  ),
1307
- !compact && /* @__PURE__ */ jsx6(
1551
+ !compact && /* @__PURE__ */ jsx7(
1308
1552
  "span",
1309
1553
  {
1310
1554
  style: {
@@ -1317,7 +1561,7 @@ var LevelMeter = ({
1317
1561
  children: active && peakDb > -120 ? `${peakDb.toFixed(0)} dB` : "\u2014"
1318
1562
  }
1319
1563
  ),
1320
- clipped && /* @__PURE__ */ jsx6(
1564
+ clipped && /* @__PURE__ */ jsx7(
1321
1565
  "span",
1322
1566
  {
1323
1567
  "data-testid": `${id}-clip`,
@@ -1342,7 +1586,7 @@ var LevelMeter = ({
1342
1586
  };
1343
1587
 
1344
1588
  // src/hooks/useTrackLevels.ts
1345
- import { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
1589
+ import { useEffect as useEffect3, useRef as useRef4, useState as useState5 } from "react";
1346
1590
  var meterDiagRLast = /* @__PURE__ */ new Map();
1347
1591
  var POLL_INTERVAL_MS = 33;
1348
1592
  var HIDDEN_RECHECK_MS = 250;
@@ -1353,9 +1597,9 @@ function isHidden() {
1353
1597
  return typeof document !== "undefined" && document.hidden === true;
1354
1598
  }
1355
1599
  function useTrackLevels(host, enabled = true) {
1356
- const mapRef = useRef3(/* @__PURE__ */ new Map());
1357
- const listenersRef = useRef3(/* @__PURE__ */ new Set());
1358
- const handleRef = useRef3(null);
1600
+ const mapRef = useRef4(/* @__PURE__ */ new Map());
1601
+ const listenersRef = useRef4(/* @__PURE__ */ new Set());
1602
+ const handleRef = useRef4(null);
1359
1603
  if (handleRef.current === null) {
1360
1604
  handleRef.current = {
1361
1605
  getLevel: (trackId) => mapRef.current.get(trackId) ?? null,
@@ -1367,7 +1611,7 @@ function useTrackLevels(host, enabled = true) {
1367
1611
  }
1368
1612
  };
1369
1613
  }
1370
- useEffect2(() => {
1614
+ useEffect3(() => {
1371
1615
  const notify = () => {
1372
1616
  listenersRef.current.forEach((l) => l());
1373
1617
  };
@@ -1437,8 +1681,8 @@ function sameLevel(a, b) {
1437
1681
  return a.peakDb === b.peakDb && a.clipped === b.clipped;
1438
1682
  }
1439
1683
  function useTrackLevel(handle, trackId) {
1440
- const [level, setLevel] = useState3(null);
1441
- useEffect2(() => {
1684
+ const [level, setLevel] = useState5(null);
1685
+ useEffect3(() => {
1442
1686
  if (!handle) {
1443
1687
  setLevel(null);
1444
1688
  return;
@@ -1462,11 +1706,11 @@ function sameMeter(a, b) {
1462
1706
  return a.active === b.active && a.clipped === b.clipped && a.peakDb === b.peakDb && Math.round(a.peakHoldDb * 2) === Math.round(b.peakHoldDb * 2);
1463
1707
  }
1464
1708
  function useTrackMeter(handle, trackId) {
1465
- const [view, setView] = useState3(IDLE_METER_VIEW);
1466
- const heldDbRef = useRef3(METER_FLOOR_DB);
1467
- const heldAtRef = useRef3(0);
1468
- const lastTickRef = useRef3(0);
1469
- useEffect2(() => {
1709
+ const [view, setView] = useState5(IDLE_METER_VIEW);
1710
+ const heldDbRef = useRef4(METER_FLOOR_DB);
1711
+ const heldAtRef = useRef4(0);
1712
+ const lastTickRef = useRef4(0);
1713
+ useEffect3(() => {
1470
1714
  if (!handle) {
1471
1715
  heldDbRef.current = METER_FLOOR_DB;
1472
1716
  lastTickRef.current = 0;
@@ -1509,8 +1753,8 @@ function useTrackMeter(handle, trackId) {
1509
1753
  return view;
1510
1754
  }
1511
1755
  function useTransportPlaying(host) {
1512
- const [playing, setPlaying] = useState3(false);
1513
- useEffect2(() => {
1756
+ const [playing, setPlaying] = useState5(false);
1757
+ useEffect3(() => {
1514
1758
  if (!host) {
1515
1759
  setPlaying(false);
1516
1760
  return;
@@ -1538,7 +1782,7 @@ function useTransportPlaying(host) {
1538
1782
  }
1539
1783
 
1540
1784
  // src/components/TrackMeterStrip.tsx
1541
- import { jsx as jsx7 } from "react/jsx-runtime";
1785
+ import { jsx as jsx8 } from "react/jsx-runtime";
1542
1786
  var TrackMeterStrip = ({
1543
1787
  levels,
1544
1788
  trackId,
@@ -1546,12 +1790,12 @@ var TrackMeterStrip = ({
1546
1790
  className
1547
1791
  }) => {
1548
1792
  const meter = useTrackMeter(levels, trackId);
1549
- return /* @__PURE__ */ jsx7(
1793
+ return /* @__PURE__ */ jsx8(
1550
1794
  "div",
1551
1795
  {
1552
1796
  "data-testid": "sdk-track-meter",
1553
1797
  className: `w-full px-2 py-1 bg-sas-panel-alt border border-t-0 border-sas-border ${roundBottom ? "rounded-b-sm" : ""} ${className ?? ""}`,
1554
- children: /* @__PURE__ */ jsx7(
1798
+ children: /* @__PURE__ */ jsx8(
1555
1799
  LevelMeter,
1556
1800
  {
1557
1801
  compact: true,
@@ -1567,7 +1811,7 @@ var TrackMeterStrip = ({
1567
1811
  };
1568
1812
 
1569
1813
  // src/components/VolumeSlider.tsx
1570
- import { useCallback as useCallback2, useState as useState4, useRef as useRef4, useEffect as useEffect3 } from "react";
1814
+ import { useCallback as useCallback3, useState as useState6, useRef as useRef5, useEffect as useEffect4 } from "react";
1571
1815
 
1572
1816
  // src/utils/volume-conversion.ts
1573
1817
  var SLIDER_UNITY = 0.75;
@@ -1589,7 +1833,7 @@ function dbToSlider(db) {
1589
1833
  }
1590
1834
 
1591
1835
  // src/components/VolumeSlider.tsx
1592
- import { jsx as jsx8 } from "react/jsx-runtime";
1836
+ import { jsx as jsx9 } from "react/jsx-runtime";
1593
1837
  function formatDb(value) {
1594
1838
  const db = sliderToDb(value);
1595
1839
  if (db <= -60) return "-\u221E dB";
@@ -1597,12 +1841,12 @@ function formatDb(value) {
1597
1841
  return `${sign}${db.toFixed(1)} dB`;
1598
1842
  }
1599
1843
  function useDebouncedCallback(callback, delay) {
1600
- const timeoutRef = useRef4(null);
1601
- const callbackRef = useRef4(callback);
1602
- useEffect3(() => {
1844
+ const timeoutRef = useRef5(null);
1845
+ const callbackRef = useRef5(callback);
1846
+ useEffect4(() => {
1603
1847
  callbackRef.current = callback;
1604
1848
  }, [callback]);
1605
- const debouncedCallback = useCallback2(
1849
+ const debouncedCallback = useCallback3(
1606
1850
  (...args) => {
1607
1851
  if (timeoutRef.current) {
1608
1852
  clearTimeout(timeoutRef.current);
@@ -1613,7 +1857,7 @@ function useDebouncedCallback(callback, delay) {
1613
1857
  },
1614
1858
  [delay]
1615
1859
  );
1616
- useEffect3(() => {
1860
+ useEffect4(() => {
1617
1861
  return () => {
1618
1862
  if (timeoutRef.current) {
1619
1863
  clearTimeout(timeoutRef.current);
@@ -1628,15 +1872,15 @@ var VolumeSlider = ({
1628
1872
  disabled = false,
1629
1873
  className = ""
1630
1874
  }) => {
1631
- const [localValue, setLocalValue] = useState4(value);
1632
- const [isDragging, setIsDragging] = useState4(false);
1633
- useEffect3(() => {
1875
+ const [localValue, setLocalValue] = useState6(value);
1876
+ const [isDragging, setIsDragging] = useState6(false);
1877
+ useEffect4(() => {
1634
1878
  if (!isDragging) {
1635
1879
  setLocalValue(value);
1636
1880
  }
1637
1881
  }, [value, isDragging]);
1638
1882
  const debouncedOnChange = useDebouncedCallback(onChange, 50);
1639
- const handleChange = useCallback2(
1883
+ const handleChange = useCallback3(
1640
1884
  (e) => {
1641
1885
  const newValue = parseFloat(e.target.value);
1642
1886
  setLocalValue(newValue);
@@ -1644,19 +1888,19 @@ var VolumeSlider = ({
1644
1888
  },
1645
1889
  [debouncedOnChange]
1646
1890
  );
1647
- const handleMouseDown = useCallback2(() => {
1891
+ const handleMouseDown = useCallback3(() => {
1648
1892
  setIsDragging(true);
1649
1893
  }, []);
1650
- const handleMouseUp = useCallback2(() => {
1894
+ const handleMouseUp = useCallback3(() => {
1651
1895
  setIsDragging(false);
1652
1896
  onChange(localValue);
1653
1897
  }, [localValue, onChange]);
1654
- return /* @__PURE__ */ jsx8(
1898
+ return /* @__PURE__ */ jsx9(
1655
1899
  "div",
1656
1900
  {
1657
1901
  className: `flex items-center ${className}`,
1658
1902
  title: `Volume: ${formatDb(localValue)}`,
1659
- children: /* @__PURE__ */ jsx8(
1903
+ children: /* @__PURE__ */ jsx9(
1660
1904
  "input",
1661
1905
  {
1662
1906
  type: "range",
@@ -1696,8 +1940,8 @@ var VolumeSlider = ({
1696
1940
  };
1697
1941
 
1698
1942
  // src/components/PanSlider.tsx
1699
- import { useCallback as useCallback3, useState as useState5, useRef as useRef5, useEffect as useEffect4 } from "react";
1700
- import { jsx as jsx9 } from "react/jsx-runtime";
1943
+ import { useCallback as useCallback4, useState as useState7, useRef as useRef6, useEffect as useEffect5 } from "react";
1944
+ import { jsx as jsx10 } from "react/jsx-runtime";
1701
1945
  function toPanDisplay(value) {
1702
1946
  if (Math.abs(value) < 0.02) {
1703
1947
  return "Center";
@@ -1706,12 +1950,12 @@ function toPanDisplay(value) {
1706
1950
  return value < 0 ? `L${percent}` : `R${percent}`;
1707
1951
  }
1708
1952
  function useDebouncedCallback2(callback, delay) {
1709
- const timeoutRef = useRef5(null);
1710
- const callbackRef = useRef5(callback);
1711
- useEffect4(() => {
1953
+ const timeoutRef = useRef6(null);
1954
+ const callbackRef = useRef6(callback);
1955
+ useEffect5(() => {
1712
1956
  callbackRef.current = callback;
1713
1957
  }, [callback]);
1714
- const debouncedCallback = useCallback3(
1958
+ const debouncedCallback = useCallback4(
1715
1959
  (...args) => {
1716
1960
  if (timeoutRef.current) {
1717
1961
  clearTimeout(timeoutRef.current);
@@ -1722,7 +1966,7 @@ function useDebouncedCallback2(callback, delay) {
1722
1966
  },
1723
1967
  [delay]
1724
1968
  );
1725
- useEffect4(() => {
1969
+ useEffect5(() => {
1726
1970
  return () => {
1727
1971
  if (timeoutRef.current) {
1728
1972
  clearTimeout(timeoutRef.current);
@@ -1737,15 +1981,15 @@ var PanSlider = ({
1737
1981
  disabled = false,
1738
1982
  className = ""
1739
1983
  }) => {
1740
- const [localValue, setLocalValue] = useState5(value);
1741
- const [isDragging, setIsDragging] = useState5(false);
1742
- useEffect4(() => {
1984
+ const [localValue, setLocalValue] = useState7(value);
1985
+ const [isDragging, setIsDragging] = useState7(false);
1986
+ useEffect5(() => {
1743
1987
  if (!isDragging) {
1744
1988
  setLocalValue(value);
1745
1989
  }
1746
1990
  }, [value, isDragging]);
1747
1991
  const debouncedOnChange = useDebouncedCallback2(onChange, 50);
1748
- const handleChange = useCallback3(
1992
+ const handleChange = useCallback4(
1749
1993
  (e) => {
1750
1994
  const newValue = parseFloat(e.target.value);
1751
1995
  setLocalValue(newValue);
@@ -1753,23 +1997,23 @@ var PanSlider = ({
1753
1997
  },
1754
1998
  [debouncedOnChange]
1755
1999
  );
1756
- const handleMouseDown = useCallback3(() => {
2000
+ const handleMouseDown = useCallback4(() => {
1757
2001
  setIsDragging(true);
1758
2002
  }, []);
1759
- const handleMouseUp = useCallback3(() => {
2003
+ const handleMouseUp = useCallback4(() => {
1760
2004
  setIsDragging(false);
1761
2005
  onChange(localValue);
1762
2006
  }, [localValue, onChange]);
1763
- const handleDoubleClick = useCallback3(() => {
2007
+ const handleDoubleClick = useCallback4(() => {
1764
2008
  setLocalValue(0);
1765
2009
  onChange(0);
1766
2010
  }, [onChange]);
1767
- return /* @__PURE__ */ jsx9(
2011
+ return /* @__PURE__ */ jsx10(
1768
2012
  "div",
1769
2013
  {
1770
2014
  className: `flex items-center ${className}`,
1771
2015
  title: `Pan: ${toPanDisplay(localValue)}`,
1772
- children: /* @__PURE__ */ jsx9(
2016
+ children: /* @__PURE__ */ jsx10(
1773
2017
  "input",
1774
2018
  {
1775
2019
  type: "range",
@@ -1810,8 +2054,8 @@ var PanSlider = ({
1810
2054
  };
1811
2055
 
1812
2056
  // src/components/SorceryProgressBar.tsx
1813
- import { useState as useState6, useEffect as useEffect5, useRef as useRef6 } from "react";
1814
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2057
+ import { useState as useState8, useEffect as useEffect6, useRef as useRef7 } from "react";
2058
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1815
2059
  function calculateTimeBasedTarget(elapsedMs, estimatedDurationMs) {
1816
2060
  const t = elapsedMs / estimatedDurationMs;
1817
2061
  if (t <= 0) return 0;
@@ -1856,20 +2100,20 @@ function SorceryProgressBar({
1856
2100
  onProgressChange,
1857
2101
  estimatedDurationMs
1858
2102
  }) {
1859
- const [progress, setProgress] = useState6(initialProgress);
1860
- const timerRef = useRef6(null);
1861
- const isLoadingRef = useRef6(false);
1862
- const hasStartedRef = useRef6(false);
1863
- const startTimeRef = useRef6(0);
1864
- const onProgressChangeRef = useRef6(onProgressChange);
1865
- const onCompleteRef = useRef6(onComplete);
2103
+ const [progress, setProgress] = useState8(initialProgress);
2104
+ const timerRef = useRef7(null);
2105
+ const isLoadingRef = useRef7(false);
2106
+ const hasStartedRef = useRef7(false);
2107
+ const startTimeRef = useRef7(0);
2108
+ const onProgressChangeRef = useRef7(onProgressChange);
2109
+ const onCompleteRef = useRef7(onComplete);
1866
2110
  onProgressChangeRef.current = onProgressChange;
1867
2111
  onCompleteRef.current = onComplete;
1868
- const initialProgressRef = useRef6(initialProgress);
2112
+ const initialProgressRef = useRef7(initialProgress);
1869
2113
  initialProgressRef.current = initialProgress;
1870
- const estimatedDurationMsRef = useRef6(estimatedDurationMs);
2114
+ const estimatedDurationMsRef = useRef7(estimatedDurationMs);
1871
2115
  estimatedDurationMsRef.current = estimatedDurationMs;
1872
- useEffect5(() => {
2116
+ useEffect6(() => {
1873
2117
  const wasLoading = isLoadingRef.current;
1874
2118
  isLoadingRef.current = isLoading;
1875
2119
  if (isLoading && !wasLoading) {
@@ -1931,12 +2175,12 @@ function SorceryProgressBar({
1931
2175
  const displayProgress = Math.floor(progress);
1932
2176
  const isComplete = !isLoading && progress === 100;
1933
2177
  const transitionDuration = progress < 50 ? "300ms" : progress < 80 ? "500ms" : "700ms";
1934
- return /* @__PURE__ */ jsxs6(
2178
+ return /* @__PURE__ */ jsxs7(
1935
2179
  "div",
1936
2180
  {
1937
2181
  className: `relative w-full ${heightClass} bg-sas-panel-alt border border-sas-border rounded-sm overflow-hidden shadow-inner`,
1938
2182
  children: [
1939
- /* @__PURE__ */ jsx10(
2183
+ /* @__PURE__ */ jsx11(
1940
2184
  "div",
1941
2185
  {
1942
2186
  className: `
@@ -1954,13 +2198,13 @@ function SorceryProgressBar({
1954
2198
  }
1955
2199
  }
1956
2200
  ),
1957
- /* @__PURE__ */ jsx10("div", { className: "absolute inset-0 flex items-center justify-center", children: isLoading && progress < 100 ? /* @__PURE__ */ jsxs6("span", { className: "font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider", children: [
2201
+ /* @__PURE__ */ jsx11("div", { className: "absolute inset-0 flex items-center justify-center", children: isLoading && progress < 100 ? /* @__PURE__ */ jsxs7("span", { className: "font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider", children: [
1958
2202
  statusText,
1959
2203
  " ",
1960
2204
  displayProgress,
1961
2205
  "%"
1962
- ] }) : isComplete ? /* @__PURE__ */ jsx10("span", { className: "font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider", children: completeText }) : null }),
1963
- /* @__PURE__ */ jsx10(
2206
+ ] }) : isComplete ? /* @__PURE__ */ jsx11("span", { className: "font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider", children: completeText }) : null }),
2207
+ /* @__PURE__ */ jsx11(
1964
2208
  "div",
1965
2209
  {
1966
2210
  className: "absolute inset-0 pointer-events-none opacity-10",
@@ -1981,7 +2225,7 @@ function SorceryProgressBar({
1981
2225
  }
1982
2226
 
1983
2227
  // src/components/TrackRow.tsx
1984
- import { Fragment, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2228
+ import { Fragment, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
1985
2229
  function TrackRow({
1986
2230
  track,
1987
2231
  prompt,
@@ -2010,6 +2254,7 @@ function TrackRow({
2010
2254
  onFxToggle,
2011
2255
  onFxPresetChange,
2012
2256
  onFxDryWetChange,
2257
+ externalFxHost,
2013
2258
  onToggleFxDrawer,
2014
2259
  onProgressChange,
2015
2260
  accentColor = "#A78BFA",
@@ -2040,7 +2285,7 @@ function TrackRow({
2040
2285
  levels
2041
2286
  }) {
2042
2287
  const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;
2043
- const [confirmDelete, setConfirmDelete] = React8.useState(false);
2288
+ const [confirmDelete, setConfirmDelete] = React9.useState(false);
2044
2289
  const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);
2045
2290
  const hasFxActive = Object.values(fxDetailState).some(
2046
2291
  (d) => d.enabled
@@ -2055,8 +2300,8 @@ function TrackRow({
2055
2300
  };
2056
2301
  const borderColorStyle = needsGeneration ? void 0 : accentColor;
2057
2302
  const borderClass = needsGeneration ? "border-amber-400 animate-pulse" : "border-sas-border";
2058
- return /* @__PURE__ */ jsxs7("div", { "data-testid": "sdk-track-row-wrapper", className: "w-full", ...drag?.rowProps ?? {}, children: [
2059
- /* @__PURE__ */ jsxs7(
2303
+ return /* @__PURE__ */ jsxs8("div", { "data-testid": "sdk-track-row-wrapper", className: "w-full", ...drag?.rowProps ?? {}, children: [
2304
+ /* @__PURE__ */ jsxs8(
2060
2305
  "div",
2061
2306
  {
2062
2307
  "data-testid": "sdk-track-row",
@@ -2066,7 +2311,7 @@ function TrackRow({
2066
2311
  borderLeftWidth: "3px"
2067
2312
  },
2068
2313
  children: [
2069
- drag && /* @__PURE__ */ jsx11(
2314
+ drag && /* @__PURE__ */ jsx12(
2070
2315
  "div",
2071
2316
  {
2072
2317
  "data-testid": "sdk-drag-handle",
@@ -2074,10 +2319,10 @@ function TrackRow({
2074
2319
  className: "flex-shrink-0 self-stretch flex items-center -ml-0.5 pr-0.5 text-sas-muted/40 hover:text-sas-muted cursor-grab active:cursor-grabbing relative z-30",
2075
2320
  title: "Drag to reorder",
2076
2321
  "aria-label": "Drag to reorder track",
2077
- children: /* @__PURE__ */ jsx11(GripVertical, { className: "w-3.5 h-3.5", strokeWidth: 2 })
2322
+ children: /* @__PURE__ */ jsx12(GripVertical, { className: "w-3.5 h-3.5", strokeWidth: 2 })
2078
2323
  }
2079
2324
  ),
2080
- isGenerating && /* @__PURE__ */ jsx11("div", { className: "absolute left-0 top-0 bottom-0 right-44 z-20", children: /* @__PURE__ */ jsx11(
2325
+ isGenerating && /* @__PURE__ */ jsx12("div", { className: "absolute left-0 top-0 bottom-0 right-44 z-20", children: /* @__PURE__ */ jsx12(
2081
2326
  SorceryProgressBar,
2082
2327
  {
2083
2328
  isLoading: true,
@@ -2088,14 +2333,14 @@ function TrackRow({
2088
2333
  estimatedDurationMs: estimatedGenerationMs
2089
2334
  }
2090
2335
  ) }),
2091
- /* @__PURE__ */ jsxs7(
2336
+ /* @__PURE__ */ jsxs8(
2092
2337
  "div",
2093
2338
  {
2094
2339
  "data-testid": "sdk-track-content",
2095
2340
  className: `flex flex-col flex-1 min-w-0 relative z-10 transition-opacity ${soloedOut ? "opacity-40" : ""}`,
2096
2341
  title: soloedOut ? "Silenced \u2014 another track is soloed" : void 0,
2097
2342
  children: [
2098
- contentSlot ? contentSlot : onPromptChange ? /* @__PURE__ */ jsx11(
2343
+ contentSlot ? contentSlot : onPromptChange ? /* @__PURE__ */ jsx12(
2099
2344
  "input",
2100
2345
  {
2101
2346
  type: "text",
@@ -2108,10 +2353,10 @@ function TrackRow({
2108
2353
  className: "sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed"
2109
2354
  }
2110
2355
  ) : null,
2111
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 mt-1", children: [
2112
- track.name && /* @__PURE__ */ jsx11("span", { className: "text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]", title: track.name, children: track.name }),
2113
- /* @__PURE__ */ jsx11("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "vol:" }),
2114
- /* @__PURE__ */ jsx11(
2356
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 mt-1", children: [
2357
+ track.name && /* @__PURE__ */ jsx12("span", { className: "text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]", title: track.name, children: track.name }),
2358
+ /* @__PURE__ */ jsx12("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "vol:" }),
2359
+ /* @__PURE__ */ jsx12(
2115
2360
  VolumeSlider,
2116
2361
  {
2117
2362
  value: currentVolume,
@@ -2120,8 +2365,8 @@ function TrackRow({
2120
2365
  className: "flex-1 min-w-[40px]"
2121
2366
  }
2122
2367
  ),
2123
- /* @__PURE__ */ jsx11("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "pan:" }),
2124
- /* @__PURE__ */ jsx11(
2368
+ /* @__PURE__ */ jsx12("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "pan:" }),
2369
+ /* @__PURE__ */ jsx12(
2125
2370
  PanSlider,
2126
2371
  {
2127
2372
  value: currentPan,
@@ -2134,27 +2379,27 @@ function TrackRow({
2134
2379
  ]
2135
2380
  }
2136
2381
  ),
2137
- error && /* @__PURE__ */ jsx11(
2382
+ error && /* @__PURE__ */ jsx12(
2138
2383
  "div",
2139
2384
  {
2140
2385
  "data-testid": "sdk-error-indicator",
2141
2386
  className: "flex-shrink-0 relative z-10 self-stretch flex items-center px-1 group cursor-help",
2142
2387
  title: error,
2143
- children: /* @__PURE__ */ jsxs7("div", { className: "relative", children: [
2144
- /* @__PURE__ */ jsx11(
2388
+ children: /* @__PURE__ */ jsxs8("div", { className: "relative", children: [
2389
+ /* @__PURE__ */ jsx12(
2145
2390
  AlertCircle,
2146
2391
  {
2147
2392
  className: "w-5 h-5 text-red-500 animate-pulse",
2148
2393
  strokeWidth: 2.5
2149
2394
  }
2150
2395
  ),
2151
- /* @__PURE__ */ jsx11("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-red-900/95 text-red-100 text-xs rounded shadow-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 max-w-[200px] truncate", children: error })
2396
+ /* @__PURE__ */ jsx12("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-red-900/95 text-red-100 text-xs rounded shadow-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 max-w-[200px] truncate", children: error })
2152
2397
  ] })
2153
2398
  }
2154
2399
  ),
2155
- /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center", children: [
2156
- /* @__PURE__ */ jsxs7("div", { className: "flex gap-1 items-center", children: [
2157
- onGenerate && /* @__PURE__ */ jsx11(
2400
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center", children: [
2401
+ /* @__PURE__ */ jsxs8("div", { className: "flex gap-1 items-center", children: [
2402
+ onGenerate && /* @__PURE__ */ jsx12(
2158
2403
  "button",
2159
2404
  {
2160
2405
  "data-testid": "sdk-generate-button",
@@ -2165,7 +2410,7 @@ function TrackRow({
2165
2410
  children: "Create"
2166
2411
  }
2167
2412
  ),
2168
- onCopy && /* @__PURE__ */ jsx11(
2413
+ onCopy && /* @__PURE__ */ jsx12(
2169
2414
  "button",
2170
2415
  {
2171
2416
  "data-testid": "sdk-copy-button",
@@ -2176,7 +2421,7 @@ function TrackRow({
2176
2421
  children: "Copy"
2177
2422
  }
2178
2423
  ),
2179
- /* @__PURE__ */ jsx11(
2424
+ /* @__PURE__ */ jsx12(
2180
2425
  "button",
2181
2426
  {
2182
2427
  "data-testid": "sdk-mute-button",
@@ -2186,7 +2431,7 @@ function TrackRow({
2186
2431
  children: "M"
2187
2432
  }
2188
2433
  ),
2189
- onDelete && /* @__PURE__ */ jsx11(
2434
+ onDelete && /* @__PURE__ */ jsx12(
2190
2435
  "button",
2191
2436
  {
2192
2437
  "data-testid": "sdk-delete-button",
@@ -2197,8 +2442,8 @@ function TrackRow({
2197
2442
  }
2198
2443
  )
2199
2444
  ] }),
2200
- /* @__PURE__ */ jsxs7("div", { className: "flex gap-1 items-center", children: [
2201
- onShuffle && /* @__PURE__ */ jsx11(
2445
+ /* @__PURE__ */ jsxs8("div", { className: "flex gap-1 items-center", children: [
2446
+ onShuffle && /* @__PURE__ */ jsx12(
2202
2447
  "button",
2203
2448
  {
2204
2449
  "data-testid": "sdk-shuffle-button",
@@ -2209,7 +2454,7 @@ function TrackRow({
2209
2454
  children: "Shuffle"
2210
2455
  }
2211
2456
  ),
2212
- onToggleFxDrawer && /* @__PURE__ */ jsx11(
2457
+ onToggleFxDrawer && /* @__PURE__ */ jsx12(
2213
2458
  "button",
2214
2459
  {
2215
2460
  "data-testid": "sdk-fx-button",
@@ -2220,7 +2465,7 @@ function TrackRow({
2220
2465
  children: "FX"
2221
2466
  }
2222
2467
  ),
2223
- /* @__PURE__ */ jsx11(
2468
+ /* @__PURE__ */ jsx12(
2224
2469
  "button",
2225
2470
  {
2226
2471
  "data-testid": "sdk-solo-button",
@@ -2231,7 +2476,7 @@ function TrackRow({
2231
2476
  children: "S"
2232
2477
  }
2233
2478
  ),
2234
- onToggleDrawer && /* @__PURE__ */ jsx11(
2479
+ onToggleDrawer && /* @__PURE__ */ jsx12(
2235
2480
  "button",
2236
2481
  {
2237
2482
  "data-testid": "sdk-plugin-button",
@@ -2239,7 +2484,7 @@ function TrackRow({
2239
2484
  disabled: isGenerating,
2240
2485
  className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : soundTabOpen ? "bg-sas-accent border-sas-accent text-sas-bg" : instrumentMissing ? "bg-amber-500/20 text-amber-400 hover:bg-amber-500/40" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
2241
2486
  title: `Sound \u2014 presets & history${instrumentMissing ? " (instrument missing)" : ""}`,
2242
- children: /* @__PURE__ */ jsx11(ChevronDown, { className: "w-3 h-3", strokeWidth: 2.5 })
2487
+ children: /* @__PURE__ */ jsx12(ChevronDown, { className: "w-3 h-3", strokeWidth: 2.5 })
2243
2488
  }
2244
2489
  )
2245
2490
  ] })
@@ -2247,13 +2492,13 @@ function TrackRow({
2247
2492
  ]
2248
2493
  }
2249
2494
  ),
2250
- levels && /* @__PURE__ */ jsx11(TrackMeterStrip, { levels, trackId: track.id, roundBottom: !drawerOpen }),
2251
- drawerOpen && /* @__PURE__ */ jsx11(
2495
+ levels && /* @__PURE__ */ jsx12(TrackMeterStrip, { levels, trackId: track.id, roundBottom: !drawerOpen }),
2496
+ drawerOpen && /* @__PURE__ */ jsx12(
2252
2497
  "div",
2253
2498
  {
2254
2499
  "data-testid": "sdk-track-drawer",
2255
2500
  className: "border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2 max-h-[260px] overflow-y-auto",
2256
- children: /* @__PURE__ */ jsx11(
2501
+ children: /* @__PURE__ */ jsx12(
2257
2502
  TrackDrawer,
2258
2503
  {
2259
2504
  activeTab: drawerTab,
@@ -2263,6 +2508,7 @@ function TrackRow({
2263
2508
  onFxToggle,
2264
2509
  onFxPresetChange,
2265
2510
  onFxDryWetChange,
2511
+ externalFxHost,
2266
2512
  fxDisabled: isGenerating,
2267
2513
  instruments: availableInstruments,
2268
2514
  currentPluginId: currentInstrumentPluginId ?? null,
@@ -2289,13 +2535,13 @@ function TrackRow({
2289
2535
  )
2290
2536
  }
2291
2537
  ),
2292
- /* @__PURE__ */ jsx11(
2538
+ /* @__PURE__ */ jsx12(
2293
2539
  ConfirmDialog,
2294
2540
  {
2295
2541
  open: confirmDelete,
2296
2542
  title: "Delete track?",
2297
- message: /* @__PURE__ */ jsxs7(Fragment, { children: [
2298
- /* @__PURE__ */ jsx11("span", { className: "text-sas-text", children: track.name?.trim() || "This track" }),
2543
+ message: /* @__PURE__ */ jsxs8(Fragment, { children: [
2544
+ /* @__PURE__ */ jsx12("span", { className: "text-sas-text", children: track.name?.trim() || "This track" }),
2299
2545
  " will be permanently removed from this scene. This cannot be undone."
2300
2546
  ] }),
2301
2547
  confirmLabel: "Delete",
@@ -2311,13 +2557,13 @@ function TrackRow({
2311
2557
  }
2312
2558
 
2313
2559
  // src/components/CrossfadeTrackRow.tsx
2314
- import React9 from "react";
2315
- import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2560
+ import React10 from "react";
2561
+ import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2316
2562
  function LayerCaption({ tag, layer }) {
2317
- return /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
2318
- /* @__PURE__ */ jsx12("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
2319
- /* @__PURE__ */ jsx12("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
2320
- layer.soundLabel && /* @__PURE__ */ jsxs8("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
2563
+ return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
2564
+ /* @__PURE__ */ jsx13("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
2565
+ /* @__PURE__ */ jsx13("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
2566
+ layer.soundLabel && /* @__PURE__ */ jsxs9("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
2321
2567
  "\xB7 ",
2322
2568
  layer.soundLabel
2323
2569
  ] })
@@ -2336,8 +2582,8 @@ function CrossfadeTrackRow({
2336
2582
  levels,
2337
2583
  accentColor = "#9333EA"
2338
2584
  }) {
2339
- const [confirmDelete, setConfirmDelete] = React9.useState(false);
2340
- const renderLayer = (layer, slot, tag) => /* @__PURE__ */ jsx12(
2585
+ const [confirmDelete, setConfirmDelete] = React10.useState(false);
2586
+ const renderLayer = (layer, slot, tag) => /* @__PURE__ */ jsx13(
2341
2587
  TrackRow,
2342
2588
  {
2343
2589
  track: { id: layer.trackId, name: "", role: layer.role },
@@ -2347,23 +2593,23 @@ function CrossfadeTrackRow({
2347
2593
  drawerTab: "fx",
2348
2594
  levels,
2349
2595
  accentColor,
2350
- contentSlot: /* @__PURE__ */ jsx12(LayerCaption, { tag, layer }),
2596
+ contentSlot: /* @__PURE__ */ jsx13(LayerCaption, { tag, layer }),
2351
2597
  onMuteToggle,
2352
2598
  onSoloToggle,
2353
2599
  onVolumeChange: (v) => onVolumeChange(slot, v),
2354
2600
  onPanChange: (p) => onPanChange(slot, p)
2355
2601
  }
2356
2602
  );
2357
- return /* @__PURE__ */ jsxs8(
2603
+ return /* @__PURE__ */ jsxs9(
2358
2604
  "div",
2359
2605
  {
2360
2606
  "data-testid": "crossfade-track-row",
2361
2607
  className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
2362
2608
  style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
2363
2609
  children: [
2364
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
2365
- /* @__PURE__ */ jsx12("span", { className: "text-[10px] font-bold uppercase tracking-wide", style: { color: accentColor }, children: "\u21C4 Crossfade" }),
2366
- /* @__PURE__ */ jsx12(
2610
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
2611
+ /* @__PURE__ */ jsx13("span", { className: "text-[10px] font-bold uppercase tracking-wide", style: { color: accentColor }, children: "\u21C4 Crossfade" }),
2612
+ /* @__PURE__ */ jsx13(
2367
2613
  "button",
2368
2614
  {
2369
2615
  "data-testid": "crossfade-delete-button",
@@ -2376,8 +2622,8 @@ function CrossfadeTrackRow({
2376
2622
  )
2377
2623
  ] }),
2378
2624
  renderLayer(origin, "origin", "Origin"),
2379
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "crossfade-slider-row", children: [
2380
- /* @__PURE__ */ jsx12(
2625
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "crossfade-slider-row", children: [
2626
+ /* @__PURE__ */ jsx13(
2381
2627
  "span",
2382
2628
  {
2383
2629
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
@@ -2385,7 +2631,7 @@ function CrossfadeTrackRow({
2385
2631
  children: origin.sourceName ?? origin.name
2386
2632
  }
2387
2633
  ),
2388
- /* @__PURE__ */ jsx12(
2634
+ /* @__PURE__ */ jsx13(
2389
2635
  "input",
2390
2636
  {
2391
2637
  type: "range",
@@ -2401,7 +2647,7 @@ function CrossfadeTrackRow({
2401
2647
  "aria-label": "Crossfade position"
2402
2648
  }
2403
2649
  ),
2404
- /* @__PURE__ */ jsx12(
2650
+ /* @__PURE__ */ jsx13(
2405
2651
  "span",
2406
2652
  {
2407
2653
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
@@ -2411,12 +2657,12 @@ function CrossfadeTrackRow({
2411
2657
  )
2412
2658
  ] }),
2413
2659
  renderLayer(target, "target", "Target"),
2414
- /* @__PURE__ */ jsx12(
2660
+ /* @__PURE__ */ jsx13(
2415
2661
  ConfirmDialog,
2416
2662
  {
2417
2663
  open: confirmDelete,
2418
2664
  title: "Delete crossfade?",
2419
- message: /* @__PURE__ */ jsx12(Fragment2, { children: "This crossfade pair (both layers) will be permanently removed from this scene. This cannot be undone." }),
2665
+ message: /* @__PURE__ */ jsx13(Fragment2, { children: "This crossfade pair (both layers) will be permanently removed from this scene. This cannot be undone." }),
2420
2666
  confirmLabel: "Delete",
2421
2667
  onConfirm: () => {
2422
2668
  setConfirmDelete(false);
@@ -2659,22 +2905,22 @@ function defaultFadeGesture(role) {
2659
2905
  }
2660
2906
 
2661
2907
  // src/components/FadeTrackRow.tsx
2662
- import React10 from "react";
2663
- import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2908
+ import React11 from "react";
2909
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
2664
2910
  function FadeCaption({
2665
2911
  layer,
2666
2912
  direction,
2667
2913
  gesture
2668
2914
  }) {
2669
2915
  const tag = direction === "in" ? "Fade in" : "Fade out";
2670
- return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
2671
- /* @__PURE__ */ jsx13("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
2672
- /* @__PURE__ */ jsx13("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
2673
- layer.soundLabel && /* @__PURE__ */ jsxs9("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
2916
+ return /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
2917
+ /* @__PURE__ */ jsx14("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
2918
+ /* @__PURE__ */ jsx14("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
2919
+ layer.soundLabel && /* @__PURE__ */ jsxs10("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
2674
2920
  "\xB7 ",
2675
2921
  layer.soundLabel
2676
2922
  ] }),
2677
- /* @__PURE__ */ jsxs9("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", title: `Fade gesture: ${gesture}`, children: [
2923
+ /* @__PURE__ */ jsxs10("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", title: `Fade gesture: ${gesture}`, children: [
2678
2924
  "\xB7 ",
2679
2925
  gesture
2680
2926
  ] })
@@ -2695,20 +2941,20 @@ function FadeTrackRow({
2695
2941
  levels,
2696
2942
  accentColor = "#9333EA"
2697
2943
  }) {
2698
- const [confirmDelete, setConfirmDelete] = React10.useState(false);
2944
+ const [confirmDelete, setConfirmDelete] = React11.useState(false);
2699
2945
  const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
2700
2946
  const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
2701
2947
  const verb = effect && effect !== "fade" ? effect.charAt(0).toUpperCase() + effect.slice(1) : "Fade";
2702
2948
  const badge = direction === "in" ? `\u2197 ${verb} in` : `\u2198 ${verb} out`;
2703
- return /* @__PURE__ */ jsxs9(
2949
+ return /* @__PURE__ */ jsxs10(
2704
2950
  "div",
2705
2951
  {
2706
2952
  "data-testid": "fade-track-row",
2707
2953
  className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
2708
2954
  style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
2709
2955
  children: [
2710
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
2711
- /* @__PURE__ */ jsx13(
2956
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
2957
+ /* @__PURE__ */ jsx14(
2712
2958
  "span",
2713
2959
  {
2714
2960
  "data-testid": "fade-direction-badge",
@@ -2717,7 +2963,7 @@ function FadeTrackRow({
2717
2963
  children: badge
2718
2964
  }
2719
2965
  ),
2720
- /* @__PURE__ */ jsx13(
2966
+ /* @__PURE__ */ jsx14(
2721
2967
  "button",
2722
2968
  {
2723
2969
  "data-testid": "fade-delete-button",
@@ -2729,7 +2975,7 @@ function FadeTrackRow({
2729
2975
  }
2730
2976
  )
2731
2977
  ] }),
2732
- /* @__PURE__ */ jsx13(
2978
+ /* @__PURE__ */ jsx14(
2733
2979
  TrackRow,
2734
2980
  {
2735
2981
  track: { id: layer.trackId, name: "", role: layer.role },
@@ -2739,15 +2985,15 @@ function FadeTrackRow({
2739
2985
  drawerTab: "fx",
2740
2986
  levels,
2741
2987
  accentColor,
2742
- contentSlot: /* @__PURE__ */ jsx13(FadeCaption, { layer, direction, gesture }),
2988
+ contentSlot: /* @__PURE__ */ jsx14(FadeCaption, { layer, direction, gesture }),
2743
2989
  onMuteToggle,
2744
2990
  onSoloToggle,
2745
2991
  onVolumeChange,
2746
2992
  onPanChange
2747
2993
  }
2748
2994
  ),
2749
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "fade-slider-row", children: [
2750
- /* @__PURE__ */ jsx13(
2995
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "fade-slider-row", children: [
2996
+ /* @__PURE__ */ jsx14(
2751
2997
  "span",
2752
2998
  {
2753
2999
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
@@ -2755,7 +3001,7 @@ function FadeTrackRow({
2755
3001
  children: leftLabel
2756
3002
  }
2757
3003
  ),
2758
- /* @__PURE__ */ jsx13(
3004
+ /* @__PURE__ */ jsx14(
2759
3005
  "input",
2760
3006
  {
2761
3007
  type: "range",
@@ -2771,7 +3017,7 @@ function FadeTrackRow({
2771
3017
  "aria-label": "Fade position"
2772
3018
  }
2773
3019
  ),
2774
- /* @__PURE__ */ jsx13(
3020
+ /* @__PURE__ */ jsx14(
2775
3021
  "span",
2776
3022
  {
2777
3023
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
@@ -2780,12 +3026,12 @@ function FadeTrackRow({
2780
3026
  }
2781
3027
  )
2782
3028
  ] }),
2783
- /* @__PURE__ */ jsx13(
3029
+ /* @__PURE__ */ jsx14(
2784
3030
  ConfirmDialog,
2785
3031
  {
2786
3032
  open: confirmDelete,
2787
3033
  title: "Delete fade?",
2788
- message: /* @__PURE__ */ jsx13(Fragment3, { children: "This fade track will be permanently removed from this scene. This cannot be undone." }),
3034
+ message: /* @__PURE__ */ jsx14(Fragment3, { children: "This fade track will be permanently removed from this scene. This cannot be undone." }),
2789
3035
  confirmLabel: "Delete",
2790
3036
  onConfirm: () => {
2791
3037
  setConfirmDelete(false);
@@ -2801,8 +3047,8 @@ function FadeTrackRow({
2801
3047
  }
2802
3048
 
2803
3049
  // src/components/FadeModal.tsx
2804
- import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef7, useState as useState7 } from "react";
2805
- import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
3050
+ 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";
2806
3052
  function shortId(dbId) {
2807
3053
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
2808
3054
  }
@@ -2845,7 +3091,7 @@ function OrphanRow({
2845
3091
  }) {
2846
3092
  const primary = track.prompt?.trim() || track.name;
2847
3093
  const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
2848
- return /* @__PURE__ */ jsxs10(
3094
+ return /* @__PURE__ */ jsxs11(
2849
3095
  "button",
2850
3096
  {
2851
3097
  type: "button",
@@ -2857,8 +3103,8 @@ function OrphanRow({
2857
3103
  disabled,
2858
3104
  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"}`,
2859
3105
  children: [
2860
- /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
2861
- meta && /* @__PURE__ */ jsx14("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
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 })
2862
3108
  ]
2863
3109
  }
2864
3110
  );
@@ -2875,14 +3121,14 @@ function FadeModal({
2875
3121
  onCreate,
2876
3122
  testIdPrefix = "fade-modal"
2877
3123
  }) {
2878
- const [load, setLoad] = useState7({ status: "loading" });
2879
- const [selectedDbId, setSelectedDbId] = useState7("");
2880
- const [isCreating, setIsCreating] = useState7(false);
2881
- const [error, setError] = useState7(null);
2882
- const [fromName, setFromName] = useState7(null);
2883
- const [toName, setToName] = useState7(null);
2884
- const cancelRef = useRef7(null);
2885
- const refresh = useCallback4(async () => {
3124
+ const [load, setLoad] = useState9({ status: "loading" });
3125
+ const [selectedDbId, setSelectedDbId] = useState9("");
3126
+ const [isCreating, setIsCreating] = useState9(false);
3127
+ const [error, setError] = useState9(null);
3128
+ const [fromName, setFromName] = useState9(null);
3129
+ const [toName, setToName] = useState9(null);
3130
+ const cancelRef = useRef8(null);
3131
+ const refresh = useCallback5(async () => {
2886
3132
  if (!host.listSceneFamilyTracks) {
2887
3133
  setLoad({ status: "error", message: "This host does not support fades." });
2888
3134
  return;
@@ -2902,7 +3148,7 @@ function FadeModal({
2902
3148
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
2903
3149
  }
2904
3150
  }, [host, fromSceneId, toSceneId]);
2905
- useEffect6(() => {
3151
+ useEffect7(() => {
2906
3152
  if (open) {
2907
3153
  setError(null);
2908
3154
  setIsCreating(false);
@@ -2910,29 +3156,29 @@ function FadeModal({
2910
3156
  void refresh();
2911
3157
  }
2912
3158
  }, [open, refresh]);
2913
- const excludeSet = useMemo3(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
2914
- const { fadeOut, fadeIn } = useMemo3(
3159
+ const excludeSet = useMemo4(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3160
+ const { fadeOut, fadeIn } = useMemo4(
2915
3161
  () => load.status === "ready" ? computeOrphans(load.from, load.to, excludeSet) : { fadeOut: [], fadeIn: [] },
2916
3162
  [load, excludeSet]
2917
3163
  );
2918
- const allOrphans = useMemo3(
3164
+ const allOrphans = useMemo4(
2919
3165
  () => [
2920
3166
  ...fadeOut.map((t) => ({ track: t, direction: "out" })),
2921
3167
  ...fadeIn.map((t) => ({ track: t, direction: "in" }))
2922
3168
  ],
2923
3169
  [fadeOut, fadeIn]
2924
3170
  );
2925
- useEffect6(() => {
3171
+ useEffect7(() => {
2926
3172
  if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {
2927
3173
  setSelectedDbId(allOrphans[0]?.track.dbId ?? "");
2928
3174
  }
2929
3175
  }, [allOrphans, selectedDbId]);
2930
3176
  const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;
2931
3177
  const canCreate = !isCreating && !!selected;
2932
- const handleClose = useCallback4(() => {
3178
+ const handleClose = useCallback5(() => {
2933
3179
  if (!isCreating) onClose();
2934
3180
  }, [isCreating, onClose]);
2935
- const handleCreate = useCallback4(async () => {
3181
+ const handleCreate = useCallback5(async () => {
2936
3182
  if (!selected) return;
2937
3183
  setIsCreating(true);
2938
3184
  setError(null);
@@ -2953,16 +3199,16 @@ function FadeModal({
2953
3199
  if (!open) return null;
2954
3200
  const renderSection = (heading, list, section) => {
2955
3201
  if (list.length === 0) return null;
2956
- return /* @__PURE__ */ jsxs10("div", { className: "block", children: [
2957
- /* @__PURE__ */ jsx14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
2958
- /* @__PURE__ */ jsx14(
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(
2959
3205
  "div",
2960
3206
  {
2961
3207
  role: "radiogroup",
2962
3208
  "aria-label": heading,
2963
3209
  "data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
2964
3210
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
2965
- children: list.map((t) => /* @__PURE__ */ jsx14(
3211
+ children: list.map((t) => /* @__PURE__ */ jsx15(
2966
3212
  OrphanRow,
2967
3213
  {
2968
3214
  track: t,
@@ -2978,32 +3224,32 @@ function FadeModal({
2978
3224
  )
2979
3225
  ] });
2980
3226
  };
2981
- return /* @__PURE__ */ jsx14(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs10(
3227
+ return /* @__PURE__ */ jsx15(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs11(
2982
3228
  "div",
2983
3229
  {
2984
3230
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
2985
3231
  onClick: (e) => e.stopPropagation(),
2986
3232
  "data-testid": `${testIdPrefix}-box`,
2987
3233
  children: [
2988
- /* @__PURE__ */ jsx14("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
2989
- /* @__PURE__ */ jsxs10("p", { className: "text-[11px] text-sas-muted leading-relaxed", 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: [
2990
3236
  "Tracks with no counterpart between",
2991
3237
  " ",
2992
- /* @__PURE__ */ jsx14("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3238
+ /* @__PURE__ */ jsx15("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
2993
3239
  " and",
2994
3240
  " ",
2995
- /* @__PURE__ */ jsx14("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3241
+ /* @__PURE__ */ jsx15("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
2996
3242
  " can gracefully fade out (leaving) or fade in (entering) across this transition."
2997
3243
  ] }),
2998
- load.status === "loading" && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
2999
- load.status === "error" && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3000
- load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ jsx14("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__ */ jsxs10(Fragment4, { children: [
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: [
3001
3247
  renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
3002
3248
  renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
3003
3249
  ] })),
3004
- error && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3005
- /* @__PURE__ */ jsxs10("div", { className: "flex justify-end gap-2 pt-1", children: [
3006
- /* @__PURE__ */ jsx14(
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(
3007
3253
  "button",
3008
3254
  {
3009
3255
  ref: cancelRef,
@@ -3014,7 +3260,7 @@ function FadeModal({
3014
3260
  children: "Cancel"
3015
3261
  }
3016
3262
  ),
3017
- /* @__PURE__ */ jsx14(
3263
+ /* @__PURE__ */ jsx15(
3018
3264
  "button",
3019
3265
  {
3020
3266
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3031,8 +3277,8 @@ function FadeModal({
3031
3277
  }
3032
3278
 
3033
3279
  // src/components/ImportTrackModal.tsx
3034
- import { useCallback as useCallback5, useEffect as useEffect7, useState as useState8 } from "react";
3035
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
3280
+ import { useCallback as useCallback6, useEffect as useEffect8, useState as useState10 } from "react";
3281
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
3036
3282
  function ImportTrackModal({
3037
3283
  host,
3038
3284
  open,
@@ -3044,10 +3290,10 @@ function ImportTrackModal({
3044
3290
  onPick,
3045
3291
  onPortTrack
3046
3292
  }) {
3047
- const [load, setLoad] = useState8({ status: "loading" });
3048
- const [selectedSceneId, setSelectedSceneId] = useState8(null);
3049
- const [importingTrackId, setImportingTrackId] = useState8(null);
3050
- const refresh = useCallback5(async () => {
3293
+ const [load, setLoad] = useState10({ status: "loading" });
3294
+ const [selectedSceneId, setSelectedSceneId] = useState10(null);
3295
+ const [importingTrackId, setImportingTrackId] = useState10(null);
3296
+ const refresh = useCallback6(async () => {
3051
3297
  if (!host.listImportableTracks) {
3052
3298
  setLoad({ status: "error", message: "This host does not support importing tracks." });
3053
3299
  return;
@@ -3063,14 +3309,14 @@ function ImportTrackModal({
3063
3309
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
3064
3310
  }
3065
3311
  }, [host, mode, onPortTrack]);
3066
- useEffect7(() => {
3312
+ useEffect8(() => {
3067
3313
  if (open) {
3068
3314
  setSelectedSceneId(null);
3069
3315
  setImportingTrackId(null);
3070
3316
  void refresh();
3071
3317
  }
3072
3318
  }, [open, refresh]);
3073
- const handleImport = useCallback5(
3319
+ const handleImport = useCallback6(
3074
3320
  async (track, sourceSceneId, sceneName, isSameScene) => {
3075
3321
  if (isSameScene && onPortTrack) {
3076
3322
  if (!track.importable) return;
@@ -3111,16 +3357,16 @@ function ImportTrackModal({
3111
3357
  if (!open) return null;
3112
3358
  const scenes = load.status === "ready" ? load.scenes : [];
3113
3359
  const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
3114
- return /* @__PURE__ */ jsx15(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs11(
3360
+ return /* @__PURE__ */ jsx16(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs12(
3115
3361
  "div",
3116
3362
  {
3117
3363
  className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
3118
3364
  onClick: (e) => e.stopPropagation(),
3119
3365
  "data-testid": `${testIdPrefix}-modal`,
3120
3366
  children: [
3121
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3122
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
3123
- selectedScene && /* @__PURE__ */ jsx15(
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(
3124
3370
  "button",
3125
3371
  {
3126
3372
  className: "text-sas-muted hover:text-sas-accent text-xs",
@@ -3129,9 +3375,9 @@ function ImportTrackModal({
3129
3375
  children: "\u2190"
3130
3376
  }
3131
3377
  ),
3132
- /* @__PURE__ */ jsx15("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3378
+ /* @__PURE__ */ jsx16("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3133
3379
  ] }),
3134
- /* @__PURE__ */ jsx15(
3380
+ /* @__PURE__ */ jsx16(
3135
3381
  "button",
3136
3382
  {
3137
3383
  className: "text-sas-muted hover:text-sas-accent text-sm",
@@ -3141,30 +3387,30 @@ function ImportTrackModal({
3141
3387
  }
3142
3388
  )
3143
3389
  ] }),
3144
- /* @__PURE__ */ jsxs11("div", { className: "overflow-y-auto p-2 flex-1", children: [
3145
- load.status === "loading" && /* @__PURE__ */ jsx15("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3146
- load.status === "error" && /* @__PURE__ */ jsx15("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3147
- load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx15("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." }),
3148
- load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx15("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsxs11(
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(
3149
3395
  "button",
3150
3396
  {
3151
3397
  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",
3152
3398
  onClick: () => setSelectedSceneId(scene.sceneId),
3153
3399
  "data-testid": `${testIdPrefix}-scene`,
3154
3400
  children: [
3155
- /* @__PURE__ */ jsx15("span", { className: "truncate", children: scene.sceneName }),
3156
- /* @__PURE__ */ jsxs11("span", { className: "text-sas-muted", children: [
3401
+ /* @__PURE__ */ jsx16("span", { className: "truncate", children: scene.sceneName }),
3402
+ /* @__PURE__ */ jsxs12("span", { className: "text-sas-muted", children: [
3157
3403
  scene.tracks.length,
3158
3404
  " \u2192"
3159
3405
  ] })
3160
3406
  ]
3161
3407
  }
3162
3408
  ) }, scene.sceneId)) }),
3163
- selectedScene && /* @__PURE__ */ jsx15("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3409
+ selectedScene && /* @__PURE__ */ jsx16("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3164
3410
  const busy = importingTrackId === track.trackId;
3165
3411
  const gated = mode === "track" && !track.importable;
3166
3412
  const disabled = gated || busy;
3167
- return /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsxs11(
3413
+ return /* @__PURE__ */ jsx16("li", { children: /* @__PURE__ */ jsxs12(
3168
3414
  "button",
3169
3415
  {
3170
3416
  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"}`,
@@ -3174,14 +3420,14 @@ function ImportTrackModal({
3174
3420
  "data-testid": `${testIdPrefix}-track`,
3175
3421
  "data-importable": mode === "sound" || track.importable ? "true" : "false",
3176
3422
  children: [
3177
- /* @__PURE__ */ jsxs11("span", { className: "truncate", children: [
3423
+ /* @__PURE__ */ jsxs12("span", { className: "truncate", children: [
3178
3424
  track.name,
3179
- track.role ? /* @__PURE__ */ jsxs11("span", { className: "text-sas-muted", children: [
3425
+ track.role ? /* @__PURE__ */ jsxs12("span", { className: "text-sas-muted", children: [
3180
3426
  " \xB7 ",
3181
3427
  track.role
3182
3428
  ] }) : null
3183
3429
  ] }),
3184
- busy ? /* @__PURE__ */ jsx15("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx15("span", { className: "text-sas-muted", children: "\u2298" }) : null
3430
+ busy ? /* @__PURE__ */ jsx16("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx16("span", { className: "text-sas-muted", children: "\u2298" }) : null
3185
3431
  ]
3186
3432
  }
3187
3433
  ) }, track.dbId);
@@ -3193,8 +3439,8 @@ function ImportTrackModal({
3193
3439
  }
3194
3440
 
3195
3441
  // src/components/CrossfadeModal.tsx
3196
- import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo4, useRef as useRef8, useState as useState9 } from "react";
3197
- import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
3442
+ 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";
3198
3444
  function shortId2(dbId) {
3199
3445
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3200
3446
  }
@@ -3207,7 +3453,7 @@ function CandidateRow({
3207
3453
  }) {
3208
3454
  const primary = track.prompt?.trim() || track.name;
3209
3455
  const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
3210
- return /* @__PURE__ */ jsxs12(
3456
+ return /* @__PURE__ */ jsxs13(
3211
3457
  "button",
3212
3458
  {
3213
3459
  type: "button",
@@ -3219,8 +3465,8 @@ function CandidateRow({
3219
3465
  disabled,
3220
3466
  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"}`,
3221
3467
  children: [
3222
- /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3223
- meta && /* @__PURE__ */ jsx16("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
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 })
3224
3470
  ]
3225
3471
  }
3226
3472
  );
@@ -3237,15 +3483,15 @@ function CrossfadeModal({
3237
3483
  onCreate,
3238
3484
  testIdPrefix = "crossfade-modal"
3239
3485
  }) {
3240
- const [load, setLoad] = useState9({ status: "loading" });
3241
- const [originDbId, setOriginDbId] = useState9("");
3242
- const [targetDbId, setTargetDbId] = useState9("");
3243
- const [isCreating, setIsCreating] = useState9(false);
3244
- const [error, setError] = useState9(null);
3245
- const [fromName, setFromName] = useState9(null);
3246
- const [toName, setToName] = useState9(null);
3247
- const cancelRef = useRef8(null);
3248
- const refresh = useCallback6(async () => {
3486
+ const [load, setLoad] = useState11({ status: "loading" });
3487
+ const [originDbId, setOriginDbId] = useState11("");
3488
+ const [targetDbId, setTargetDbId] = useState11("");
3489
+ const [isCreating, setIsCreating] = useState11(false);
3490
+ const [error, setError] = useState11(null);
3491
+ const [fromName, setFromName] = useState11(null);
3492
+ const [toName, setToName] = useState11(null);
3493
+ const cancelRef = useRef9(null);
3494
+ const refresh = useCallback7(async () => {
3249
3495
  if (!host.listSceneFamilyTracks) {
3250
3496
  setLoad({ status: "error", message: "This host does not support crossfade tracks." });
3251
3497
  return;
@@ -3265,7 +3511,7 @@ function CrossfadeModal({
3265
3511
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
3266
3512
  }
3267
3513
  }, [host, fromSceneId, toSceneId]);
3268
- useEffect8(() => {
3514
+ useEffect9(() => {
3269
3515
  if (open) {
3270
3516
  setError(null);
3271
3517
  setIsCreating(false);
@@ -3274,21 +3520,21 @@ function CrossfadeModal({
3274
3520
  void refresh();
3275
3521
  }
3276
3522
  }, [open, refresh]);
3277
- const excludeSet = useMemo4(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3278
- const originCandidates = useMemo4(
3523
+ const excludeSet = useMemo5(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3524
+ const originCandidates = useMemo5(
3279
3525
  () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
3280
3526
  [load, excludeSet]
3281
3527
  );
3282
- const targetCandidates = useMemo4(
3528
+ const targetCandidates = useMemo5(
3283
3529
  () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
3284
3530
  [load, excludeSet]
3285
3531
  );
3286
- useEffect8(() => {
3532
+ useEffect9(() => {
3287
3533
  if (!originCandidates.some((t) => t.dbId === originDbId)) {
3288
3534
  setOriginDbId(originCandidates[0]?.dbId ?? "");
3289
3535
  }
3290
3536
  }, [originCandidates, originDbId]);
3291
- useEffect8(() => {
3537
+ useEffect9(() => {
3292
3538
  if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
3293
3539
  setTargetDbId(targetCandidates[0]?.dbId ?? "");
3294
3540
  }
@@ -3296,10 +3542,10 @@ function CrossfadeModal({
3296
3542
  const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
3297
3543
  const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
3298
3544
  const canCreate = !isCreating && !!originTrack && !!targetTrack;
3299
- const handleClose = useCallback6(() => {
3545
+ const handleClose = useCallback7(() => {
3300
3546
  if (!isCreating) onClose();
3301
3547
  }, [isCreating, onClose]);
3302
- const handleCreate = useCallback6(async () => {
3548
+ const handleCreate = useCallback7(async () => {
3303
3549
  if (!originTrack || !targetTrack) return;
3304
3550
  setIsCreating(true);
3305
3551
  setError(null);
@@ -3317,26 +3563,26 @@ function CrossfadeModal({
3317
3563
  const fromLabel = fromName ?? fromSceneName ?? null;
3318
3564
  const toLabel = toName ?? toSceneName ?? null;
3319
3565
  if (!open) return null;
3320
- return /* @__PURE__ */ jsx16(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs12(
3566
+ return /* @__PURE__ */ jsx17(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs13(
3321
3567
  "div",
3322
3568
  {
3323
3569
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3324
3570
  onClick: (e) => e.stopPropagation(),
3325
3571
  "data-testid": `${testIdPrefix}-box`,
3326
3572
  children: [
3327
- /* @__PURE__ */ jsx16("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
3328
- /* @__PURE__ */ jsxs12("p", { className: "text-[11px] text-sas-muted leading-relaxed", 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: [
3329
3575
  "Bridge a track from",
3330
3576
  " ",
3331
- /* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3577
+ /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3332
3578
  " into one from",
3333
3579
  " ",
3334
- /* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3580
+ /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3335
3581
  ". Both layers share one generated part; each keeps its own preset."
3336
3582
  ] }),
3337
- load.status === "loading" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3338
- load.status === "error" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3339
- load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs12(
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(
3340
3586
  "div",
3341
3587
  {
3342
3588
  className: "text-xs text-sas-muted py-4 text-center",
@@ -3347,20 +3593,20 @@ function CrossfadeModal({
3347
3593
  ". Add one (or free one from another crossfade) first."
3348
3594
  ]
3349
3595
  }
3350
- ) : /* @__PURE__ */ jsxs12(Fragment5, { children: [
3351
- /* @__PURE__ */ jsxs12("div", { className: "block", children: [
3352
- /* @__PURE__ */ jsxs12("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
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: [
3353
3599
  "Origin ",
3354
3600
  fromLabel ? `(${fromLabel})` : "(top)"
3355
3601
  ] }),
3356
- /* @__PURE__ */ jsx16(
3602
+ /* @__PURE__ */ jsx17(
3357
3603
  "div",
3358
3604
  {
3359
3605
  role: "radiogroup",
3360
3606
  "aria-label": "Origin track",
3361
3607
  "data-testid": `${testIdPrefix}-origin-list`,
3362
3608
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3363
- children: originCandidates.map((t) => /* @__PURE__ */ jsx16(
3609
+ children: originCandidates.map((t) => /* @__PURE__ */ jsx17(
3364
3610
  CandidateRow,
3365
3611
  {
3366
3612
  track: t,
@@ -3374,23 +3620,23 @@ function CrossfadeModal({
3374
3620
  }
3375
3621
  )
3376
3622
  ] }),
3377
- /* @__PURE__ */ jsxs12("div", { className: "block", children: [
3378
- /* @__PURE__ */ jsxs12("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3623
+ /* @__PURE__ */ jsxs13("div", { className: "block", children: [
3624
+ /* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3379
3625
  "Target ",
3380
3626
  toLabel ? `(${toLabel})` : "(bottom)"
3381
3627
  ] }),
3382
- targetCandidates.length === 0 ? /* @__PURE__ */ jsxs12("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3628
+ targetCandidates.length === 0 ? /* @__PURE__ */ jsxs13("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3383
3629
  "No available tracks in ",
3384
3630
  toLabel ?? "the target scene",
3385
3631
  " to crossfade into."
3386
- ] }) : /* @__PURE__ */ jsx16(
3632
+ ] }) : /* @__PURE__ */ jsx17(
3387
3633
  "div",
3388
3634
  {
3389
3635
  role: "radiogroup",
3390
3636
  "aria-label": "Target track",
3391
3637
  "data-testid": `${testIdPrefix}-target-list`,
3392
3638
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3393
- children: targetCandidates.map((t) => /* @__PURE__ */ jsx16(
3639
+ children: targetCandidates.map((t) => /* @__PURE__ */ jsx17(
3394
3640
  CandidateRow,
3395
3641
  {
3396
3642
  track: t,
@@ -3405,9 +3651,9 @@ function CrossfadeModal({
3405
3651
  )
3406
3652
  ] })
3407
3653
  ] })),
3408
- error && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3409
- /* @__PURE__ */ jsxs12("div", { className: "flex justify-end gap-2 pt-1", children: [
3410
- /* @__PURE__ */ jsx16(
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(
3411
3657
  "button",
3412
3658
  {
3413
3659
  ref: cancelRef,
@@ -3418,7 +3664,7 @@ function CrossfadeModal({
3418
3664
  children: "Cancel"
3419
3665
  }
3420
3666
  ),
3421
- /* @__PURE__ */ jsx16(
3667
+ /* @__PURE__ */ jsx17(
3422
3668
  "button",
3423
3669
  {
3424
3670
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3435,10 +3681,10 @@ function CrossfadeModal({
3435
3681
  }
3436
3682
 
3437
3683
  // src/components/TransitionDesigner.tsx
3438
- import { useCallback as useCallback8, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef10, useState as useState11 } from "react";
3684
+ import { useCallback as useCallback9, useEffect as useEffect10, useMemo as useMemo6, useRef as useRef11, useState as useState13 } from "react";
3439
3685
 
3440
3686
  // src/hooks/useTrackReorder.ts
3441
- import { useCallback as useCallback7, useRef as useRef9, useState as useState10 } from "react";
3687
+ import { useCallback as useCallback8, useRef as useRef10, useState as useState12 } from "react";
3442
3688
  function moveItem(arr, from, to) {
3443
3689
  const next = arr.slice();
3444
3690
  if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
@@ -3455,12 +3701,12 @@ function useTrackReorder({
3455
3701
  getId,
3456
3702
  onError
3457
3703
  }) {
3458
- const [draggingIndex, setDraggingIndex] = useState10(null);
3459
- const [dragOverIndex, setDragOverIndex] = useState10(null);
3460
- const fromRef = useRef9(null);
3461
- const itemsRef = useRef9(items);
3704
+ const [draggingIndex, setDraggingIndex] = useState12(null);
3705
+ const [dragOverIndex, setDragOverIndex] = useState12(null);
3706
+ const fromRef = useRef10(null);
3707
+ const itemsRef = useRef10(items);
3462
3708
  itemsRef.current = items;
3463
- const dragPropsFor = useCallback7(
3709
+ const dragPropsFor = useCallback8(
3464
3710
  (index) => ({
3465
3711
  handleProps: {
3466
3712
  draggable: true,
@@ -3642,7 +3888,7 @@ function dbIdsFromKeys(keys) {
3642
3888
  }
3643
3889
 
3644
3890
  // src/components/TransitionDesigner.tsx
3645
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
3891
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
3646
3892
  var CROSSFADE_ESTIMATE_MS = 15e3;
3647
3893
  var FADE_ESTIMATE_MS = 11e3;
3648
3894
  var CREATE_ALL_CONCURRENCY = 5;
@@ -3666,44 +3912,44 @@ function TransitionDesigner({
3666
3912
  familyLabel,
3667
3913
  testIdPrefix = "transition-designer"
3668
3914
  }) {
3669
- const [load, setLoad] = useState11({ status: "loading" });
3670
- const [fromName, setFromName] = useState11(null);
3671
- const [toName, setToName] = useState11(null);
3672
- const [originSlots, setOriginSlots] = useState11([]);
3673
- const [targetSlots, setTargetSlots] = useState11([]);
3674
- const [creatingKeys, setCreatingKeys] = useState11(() => /* @__PURE__ */ new Set());
3675
- const [rowErrors, setRowErrors] = useState11({});
3676
- const [rowEffects, setRowEffects] = useState11({});
3677
- const rowEffectsRef = useRef10(rowEffects);
3915
+ const [load, setLoad] = useState13({ status: "loading" });
3916
+ const [fromName, setFromName] = useState13(null);
3917
+ const [toName, setToName] = useState13(null);
3918
+ const [originSlots, setOriginSlots] = useState13([]);
3919
+ const [targetSlots, setTargetSlots] = useState13([]);
3920
+ const [creatingKeys, setCreatingKeys] = useState13(() => /* @__PURE__ */ new Set());
3921
+ const [rowErrors, setRowErrors] = useState13({});
3922
+ const [rowEffects, setRowEffects] = useState13({});
3923
+ const rowEffectsRef = useRef11(rowEffects);
3678
3924
  rowEffectsRef.current = rowEffects;
3679
3925
  const audioEffectsEnabled = !!onCreateAudioTransition;
3680
- const excludeRef = useRef10(excludeSourceDbIds);
3926
+ const excludeRef = useRef11(excludeSourceDbIds);
3681
3927
  excludeRef.current = excludeSourceDbIds;
3682
- const originSlotsRef = useRef10(originSlots);
3928
+ const originSlotsRef = useRef11(originSlots);
3683
3929
  originSlotsRef.current = originSlots;
3684
- const targetSlotsRef = useRef10(targetSlots);
3930
+ const targetSlotsRef = useRef11(targetSlots);
3685
3931
  targetSlotsRef.current = targetSlots;
3686
- const creatingKeysRef = useRef10(creatingKeys);
3932
+ const creatingKeysRef = useRef11(creatingKeys);
3687
3933
  creatingKeysRef.current = creatingKeys;
3688
- const dragRef = useRef10(null);
3689
- const [dragging, setDragging] = useState11(null);
3690
- const [dragOver, setDragOver] = useState11(null);
3691
- const excludeSet = useMemo5(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3692
- const originPool = useMemo5(
3934
+ const dragRef = useRef11(null);
3935
+ const [dragging, setDragging] = useState13(null);
3936
+ const [dragOver, setDragOver] = useState13(null);
3937
+ const excludeSet = useMemo6(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3938
+ const originPool = useMemo6(
3693
3939
  () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
3694
3940
  [load, excludeSet]
3695
3941
  );
3696
- const targetPool = useMemo5(
3942
+ const targetPool = useMemo6(
3697
3943
  () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
3698
3944
  [load, excludeSet]
3699
3945
  );
3700
- const originById = useMemo5(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
3701
- const targetById = useMemo5(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
3702
- const originByIdRef = useRef10(originById);
3946
+ const originById = useMemo6(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
3947
+ const targetById = useMemo6(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
3948
+ const originByIdRef = useRef11(originById);
3703
3949
  originByIdRef.current = originById;
3704
- const targetByIdRef = useRef10(targetById);
3950
+ const targetByIdRef = useRef11(targetById);
3705
3951
  targetByIdRef.current = targetById;
3706
- const refresh = useCallback8(async () => {
3952
+ const refresh = useCallback9(async () => {
3707
3953
  if (!host.listSceneFamilyTracks) {
3708
3954
  setLoad({ status: "error", message: "This host does not support transition tracks." });
3709
3955
  return;
@@ -3738,10 +3984,10 @@ function TransitionDesigner({
3738
3984
  });
3739
3985
  }
3740
3986
  }, [host, fromSceneId, toSceneId, transitionSceneId]);
3741
- useEffect9(() => {
3987
+ useEffect10(() => {
3742
3988
  void refresh();
3743
3989
  }, [refresh]);
3744
- useEffect9(() => {
3990
+ useEffect10(() => {
3745
3991
  if (load.status !== "ready") return;
3746
3992
  const [po, pt] = padPair(
3747
3993
  reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
@@ -3750,7 +3996,7 @@ function TransitionDesigner({
3750
3996
  if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
3751
3997
  if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
3752
3998
  }, [originPool, targetPool, load.status]);
3753
- const mutate = useCallback8(
3999
+ const mutate = useCallback9(
3754
4000
  (nextOrigin, nextTarget) => {
3755
4001
  const norm = normalizeSlots(nextOrigin, nextTarget);
3756
4002
  const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
@@ -3763,7 +4009,7 @@ function TransitionDesigner({
3763
4009
  },
3764
4010
  [host, transitionSceneId]
3765
4011
  );
3766
- const setRowEffect = useCallback8(
4012
+ const setRowEffect = useCallback9(
3767
4013
  (sourceDbId, effect) => {
3768
4014
  setRowEffects((prev) => {
3769
4015
  const next = { ...prev, [sourceDbId]: effect };
@@ -3777,7 +4023,7 @@ function TransitionDesigner({
3777
4023
  },
3778
4024
  [host, transitionSceneId]
3779
4025
  );
3780
- const insertGapAbove = useCallback8(
4026
+ const insertGapAbove = useCallback9(
3781
4027
  (col, index) => {
3782
4028
  const slots = col === "origin" ? originSlots : targetSlots;
3783
4029
  const next = [...slots.slice(0, index), null, ...slots.slice(index)];
@@ -3786,7 +4032,7 @@ function TransitionDesigner({
3786
4032
  },
3787
4033
  [originSlots, targetSlots, mutate]
3788
4034
  );
3789
- const removeGap = useCallback8(
4035
+ const removeGap = useCallback9(
3790
4036
  (col, index) => {
3791
4037
  const slots = col === "origin" ? originSlots : targetSlots;
3792
4038
  const next = slots.filter((_, i) => i !== index);
@@ -3795,7 +4041,7 @@ function TransitionDesigner({
3795
4041
  },
3796
4042
  [originSlots, targetSlots, mutate]
3797
4043
  );
3798
- const handleDrop = useCallback8(
4044
+ const handleDrop = useCallback9(
3799
4045
  (col, to) => {
3800
4046
  const from = dragRef.current;
3801
4047
  dragRef.current = null;
@@ -3807,16 +4053,16 @@ function TransitionDesigner({
3807
4053
  },
3808
4054
  [originSlots, targetSlots, mutate]
3809
4055
  );
3810
- const rows = useMemo5(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
3811
- const creatingDbIds = useMemo5(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
3812
- const eligibleCount = useMemo5(
4056
+ const rows = useMemo6(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
4057
+ const creatingDbIds = useMemo6(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
4058
+ const eligibleCount = useMemo6(
3813
4059
  () => rows.filter((r) => {
3814
4060
  const k = rowKey(r);
3815
4061
  return k !== null && !creatingKeys.has(k);
3816
4062
  }).length,
3817
4063
  [rows, creatingKeys]
3818
4064
  );
3819
- const createRow = useCallback8(
4065
+ const createRow = useCallback9(
3820
4066
  async (row) => {
3821
4067
  const key = rowKey(row);
3822
4068
  if (!key || !row.type || creatingKeysRef.current.has(key)) return;
@@ -3870,7 +4116,7 @@ function TransitionDesigner({
3870
4116
  },
3871
4117
  [onCreateCrossfade, onCreateFade, onCreateAudioTransition]
3872
4118
  );
3873
- const createAll = useCallback8(async () => {
4119
+ const createAll = useCallback9(async () => {
3874
4120
  const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
3875
4121
  const k = rowKey(r);
3876
4122
  return k !== null && !creatingKeysRef.current.has(k);
@@ -3938,15 +4184,15 @@ function TransitionDesigner({
3938
4184
  const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
3939
4185
  const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
3940
4186
  if (slotId === null) {
3941
- return /* @__PURE__ */ jsxs13(
4187
+ return /* @__PURE__ */ jsxs14(
3942
4188
  "div",
3943
4189
  {
3944
4190
  ...cellDragProps(col, index, false),
3945
4191
  "data-testid": `${testIdPrefix}-${col}-gap-${index}`,
3946
4192
  className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
3947
4193
  children: [
3948
- /* @__PURE__ */ jsx17("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
3949
- /* @__PURE__ */ jsx17(
4194
+ /* @__PURE__ */ jsx18("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4195
+ /* @__PURE__ */ jsx18(
3950
4196
  "button",
3951
4197
  {
3952
4198
  type: "button",
@@ -3963,7 +4209,7 @@ function TransitionDesigner({
3963
4209
  }
3964
4210
  const primary = track ? track.prompt?.trim() || track.name : slotId;
3965
4211
  const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
3966
- return /* @__PURE__ */ jsx17(
4212
+ return /* @__PURE__ */ jsx18(
3967
4213
  "div",
3968
4214
  {
3969
4215
  ...cellDragProps(col, index, locked),
@@ -3971,13 +4217,13 @@ function TransitionDesigner({
3971
4217
  "data-value": slotId,
3972
4218
  className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
3973
4219
  title: track ? track.dbId : "Track no longer available",
3974
- children: /* @__PURE__ */ jsxs13("div", { className: "flex items-start gap-1", children: [
3975
- /* @__PURE__ */ jsx17("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
3976
- /* @__PURE__ */ jsxs13("div", { className: "min-w-0 flex-1", children: [
3977
- /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-text truncate", children: primary }),
3978
- meta && /* @__PURE__ */ jsx17("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
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 })
3979
4225
  ] }),
3980
- /* @__PURE__ */ jsx17(
4226
+ /* @__PURE__ */ jsx18(
3981
4227
  "button",
3982
4228
  {
3983
4229
  type: "button",
@@ -3993,22 +4239,22 @@ function TransitionDesigner({
3993
4239
  }
3994
4240
  );
3995
4241
  };
3996
- return /* @__PURE__ */ jsxs13("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
3997
- /* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
3998
- /* @__PURE__ */ jsxs13("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
3999
- /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: fromLabel }),
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 }),
4000
4246
  " \u2192",
4001
4247
  " ",
4002
- /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: toLabel }),
4248
+ /* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: toLabel }),
4003
4249
  familyLabel ? ` \xB7 ${familyLabel}` : "",
4004
4250
  " \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
4005
4251
  ] }),
4006
- /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 shrink-0", children: [
4007
- creatingKeys.size > 0 && /* @__PURE__ */ jsxs13("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
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: [
4008
4254
  creatingKeys.size,
4009
4255
  " creating\u2026"
4010
4256
  ] }),
4011
- /* @__PURE__ */ jsxs13(
4257
+ /* @__PURE__ */ jsxs14(
4012
4258
  "button",
4013
4259
  {
4014
4260
  type: "button",
@@ -4025,49 +4271,49 @@ function TransitionDesigner({
4025
4271
  )
4026
4272
  ] })
4027
4273
  ] }),
4028
- /* @__PURE__ */ jsxs13("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4029
- /* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
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: [
4030
4276
  "Origin (",
4031
4277
  fromLabel,
4032
4278
  ")"
4033
4279
  ] }),
4034
- /* @__PURE__ */ jsx17("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4035
- /* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
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: [
4036
4282
  "Target (",
4037
4283
  toLabel,
4038
4284
  ")"
4039
4285
  ] })
4040
4286
  ] }),
4041
- load.status === "loading" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4042
- load.status === "error" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4043
- load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ jsxs13("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
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: [
4044
4290
  "No tracks to arrange in this panel for either scene. Add tracks to ",
4045
4291
  fromLabel,
4046
4292
  " or ",
4047
4293
  toLabel,
4048
4294
  " ",
4049
4295
  "first (or free one by deleting an existing crossfade/fade)."
4050
- ] }) : /* @__PURE__ */ jsx17("div", { className: "space-y-2", children: rows.map((row, i) => {
4296
+ ] }) : /* @__PURE__ */ jsx18("div", { className: "space-y-2", children: rows.map((row, i) => {
4051
4297
  const key = rowKey(row);
4052
4298
  const isCreatingThis = key !== null && creatingKeys.has(key);
4053
4299
  const errMsg = key !== null ? rowErrors[key] : void 0;
4054
- return /* @__PURE__ */ jsxs13(
4300
+ return /* @__PURE__ */ jsxs14(
4055
4301
  "div",
4056
4302
  {
4057
4303
  "data-testid": `${testIdPrefix}-row-${i}`,
4058
4304
  className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
4059
4305
  children: [
4060
4306
  renderCell("origin", i, row.originId),
4061
- /* @__PURE__ */ jsxs13("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4062
- !row.type ? /* @__PURE__ */ jsx17("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ jsx17(
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(
4063
4309
  "span",
4064
4310
  {
4065
4311
  "data-testid": `${testIdPrefix}-type-${i}`,
4066
4312
  className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
4067
4313
  children: TYPE_LABEL[row.type]
4068
4314
  }
4069
- ) : audioEffectsEnabled ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4070
- /* @__PURE__ */ jsx17(
4315
+ ) : audioEffectsEnabled ? /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4316
+ /* @__PURE__ */ jsx18(
4071
4317
  "select",
4072
4318
  {
4073
4319
  "data-testid": `${testIdPrefix}-effect-${i}`,
@@ -4077,11 +4323,11 @@ function TransitionDesigner({
4077
4323
  if (id) setRowEffect(id, e.target.value);
4078
4324
  },
4079
4325
  className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
4080
- children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx17("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4326
+ children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx18("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4081
4327
  }
4082
4328
  ),
4083
- /* @__PURE__ */ jsx17("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4084
- ] }) : /* @__PURE__ */ jsx17(
4329
+ /* @__PURE__ */ jsx18("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4330
+ ] }) : /* @__PURE__ */ jsx18(
4085
4331
  "span",
4086
4332
  {
4087
4333
  "data-testid": `${testIdPrefix}-type-${i}`,
@@ -4089,7 +4335,7 @@ function TransitionDesigner({
4089
4335
  children: TYPE_LABEL[row.type]
4090
4336
  }
4091
4337
  ),
4092
- isCreatingThis ? /* @__PURE__ */ jsx17("div", { className: "w-full", children: /* @__PURE__ */ jsx17(
4338
+ isCreatingThis ? /* @__PURE__ */ jsx18("div", { className: "w-full", children: /* @__PURE__ */ jsx18(
4093
4339
  SorceryProgressBar,
4094
4340
  {
4095
4341
  isLoading: true,
@@ -4097,7 +4343,7 @@ function TransitionDesigner({
4097
4343
  statusText: "CREATING",
4098
4344
  estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
4099
4345
  }
4100
- ) }) : /* @__PURE__ */ jsx17(
4346
+ ) }) : /* @__PURE__ */ jsx18(
4101
4347
  "button",
4102
4348
  {
4103
4349
  type: "button",
@@ -4108,7 +4354,7 @@ function TransitionDesigner({
4108
4354
  children: "Create"
4109
4355
  }
4110
4356
  ),
4111
- errMsg && /* @__PURE__ */ jsx17(
4357
+ errMsg && /* @__PURE__ */ jsx18(
4112
4358
  "span",
4113
4359
  {
4114
4360
  "data-testid": `${testIdPrefix}-row-error-${i}`,
@@ -4126,9 +4372,325 @@ function TransitionDesigner({
4126
4372
  ] });
4127
4373
  }
4128
4374
 
4375
+ // src/components/PanelMasterStrip.tsx
4376
+ import { useMemo as useMemo7, useState as useState14 } from "react";
4377
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
4378
+ function PanelMasterStrip({
4379
+ bus,
4380
+ levels = null,
4381
+ availableFx = [],
4382
+ fxLoading = false,
4383
+ soloedOut = false,
4384
+ disabled = false,
4385
+ fxPickerOpen,
4386
+ onToggleFxPicker,
4387
+ onRefreshFx,
4388
+ onVolumeChange,
4389
+ onMuteToggle,
4390
+ onSoloToggle,
4391
+ onAddFx,
4392
+ onRemoveFx,
4393
+ onToggleFxEnabled,
4394
+ onShowFxEditor
4395
+ }) {
4396
+ const [search, setSearch] = useState14("");
4397
+ const filtered = useMemo7(() => {
4398
+ const q = search.trim().toLowerCase();
4399
+ if (!q) return availableFx;
4400
+ return availableFx.filter(
4401
+ (fx) => fx.name.toLowerCase().includes(q) || fx.manufacturer.toLowerCase().includes(q)
4402
+ );
4403
+ }, [availableFx, search]);
4404
+ return /* @__PURE__ */ jsxs15(
4405
+ "div",
4406
+ {
4407
+ "data-testid": "panel-master-strip",
4408
+ 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
+ children: [
4410
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", children: [
4411
+ /* @__PURE__ */ jsx19(
4412
+ "span",
4413
+ {
4414
+ className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
4415
+ title: "Panel mix bus \u2014 volume, mute/solo and FX applied to this panel's summed output",
4416
+ children: "BUS"
4417
+ }
4418
+ ),
4419
+ /* @__PURE__ */ jsxs15("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
4420
+ /* @__PURE__ */ jsx19(
4421
+ VolumeSlider,
4422
+ {
4423
+ value: dbToSlider(bus.volume),
4424
+ onChange: (sliderValue) => onVolumeChange(sliderToDb(sliderValue)),
4425
+ disabled
4426
+ }
4427
+ ),
4428
+ /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
4429
+ /* @__PURE__ */ jsx19(
4430
+ LevelMeter,
4431
+ {
4432
+ peakDb: levels?.leftDb ?? -120,
4433
+ active: levels != null,
4434
+ clipped: levels?.clipped,
4435
+ compact: true,
4436
+ "data-testid": "bus-meter-left"
4437
+ }
4438
+ ),
4439
+ /* @__PURE__ */ jsx19(
4440
+ LevelMeter,
4441
+ {
4442
+ peakDb: levels?.rightDb ?? -120,
4443
+ active: levels != null,
4444
+ compact: true,
4445
+ "data-testid": "bus-meter-right"
4446
+ }
4447
+ )
4448
+ ] })
4449
+ ] }),
4450
+ /* @__PURE__ */ jsx19(
4451
+ "button",
4452
+ {
4453
+ "data-testid": "bus-mute-button",
4454
+ onClick: onMuteToggle,
4455
+ disabled,
4456
+ className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${bus.muted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"} disabled:opacity-50`,
4457
+ title: bus.muted ? "Unmute panel bus" : "Mute panel bus (silences the whole panel)",
4458
+ children: "M"
4459
+ }
4460
+ ),
4461
+ /* @__PURE__ */ jsx19(
4462
+ "button",
4463
+ {
4464
+ "data-testid": "bus-solo-button",
4465
+ onClick: onSoloToggle,
4466
+ disabled,
4467
+ className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${bus.soloed ? "bg-amber-500 text-black" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"} disabled:opacity-50`,
4468
+ title: bus.soloed ? "Unsolo panel bus" : "Solo this panel (silences other panels/tracks in scope)",
4469
+ children: "S"
4470
+ }
4471
+ ),
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(
4473
+ "span",
4474
+ {
4475
+ "data-testid": `bus-fx-chip-${fx.index}`,
4476
+ 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
+ title: `${fx.name}${fx.enabled ? "" : " (bypassed)"}`,
4478
+ children: [
4479
+ /* @__PURE__ */ jsx19(
4480
+ "button",
4481
+ {
4482
+ "data-testid": `bus-fx-toggle-${fx.index}`,
4483
+ onClick: () => onToggleFxEnabled(fx.index, !fx.enabled),
4484
+ disabled,
4485
+ className: "hover:opacity-70 disabled:opacity-50",
4486
+ title: fx.enabled ? `Bypass ${fx.name}` : `Enable ${fx.name}`,
4487
+ children: fx.enabled ? "\u25CF" : "\u25CB"
4488
+ }
4489
+ ),
4490
+ onShowFxEditor ? /* @__PURE__ */ jsx19(
4491
+ "button",
4492
+ {
4493
+ "data-testid": `bus-fx-edit-${fx.index}`,
4494
+ onClick: () => onShowFxEditor(fx.index),
4495
+ disabled,
4496
+ className: "max-w-[80px] truncate hover:underline disabled:opacity-50",
4497
+ title: `Open ${fx.name} editor`,
4498
+ children: fx.name
4499
+ }
4500
+ ) : /* @__PURE__ */ jsx19("span", { className: "max-w-[80px] truncate", children: fx.name }),
4501
+ /* @__PURE__ */ jsx19(
4502
+ "button",
4503
+ {
4504
+ "data-testid": `bus-fx-remove-${fx.index}`,
4505
+ onClick: () => onRemoveFx(fx.index),
4506
+ disabled,
4507
+ className: "text-sas-muted/60 hover:text-sas-danger disabled:opacity-50",
4508
+ title: `Remove ${fx.name} from the bus`,
4509
+ children: "\u2715"
4510
+ }
4511
+ )
4512
+ ]
4513
+ },
4514
+ `${fx.index}:${fx.pluginId}`
4515
+ )) }),
4516
+ /* @__PURE__ */ jsx19(
4517
+ "button",
4518
+ {
4519
+ "data-testid": "bus-fx-add-button",
4520
+ onClick: () => onToggleFxPicker(!fxPickerOpen),
4521
+ disabled,
4522
+ className: `px-1.5 py-0.5 rounded-sm border text-xs whitespace-nowrap transition-colors ${fxPickerOpen ? "border-sas-accent text-sas-accent bg-sas-accent/10" : "border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"} disabled:opacity-50`,
4523
+ title: fxPickerOpen ? "Close the FX picker" : "Add an FX plugin to the panel bus",
4524
+ children: fxPickerOpen ? "FX \u25B4" : "FX +"
4525
+ }
4526
+ )
4527
+ ] }),
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(
4531
+ "input",
4532
+ {
4533
+ type: "text",
4534
+ value: search,
4535
+ onChange: (e) => setSearch(e.target.value),
4536
+ placeholder: "Search FX...",
4537
+ className: "sas-input flex-1 px-2 py-1 text-xs"
4538
+ }
4539
+ ),
4540
+ onRefreshFx && /* @__PURE__ */ jsx19(
4541
+ "button",
4542
+ {
4543
+ onClick: () => onRefreshFx(),
4544
+ disabled: fxLoading,
4545
+ 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",
4547
+ children: fxLoading ? "..." : "Refresh"
4548
+ }
4549
+ )
4550
+ ] }),
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(
4553
+ "button",
4554
+ {
4555
+ "data-testid": `bus-fx-pick-${fx.pluginId}`,
4556
+ onClick: () => onAddFx(fx.pluginId),
4557
+ 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
+ title: `${fx.name} by ${fx.manufacturer} (${fx.type.toUpperCase()})`,
4559
+ 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() })
4562
+ ]
4563
+ },
4564
+ fx.pluginId
4565
+ )),
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" })
4567
+ ] })
4568
+ ] })
4569
+ ]
4570
+ }
4571
+ );
4572
+ }
4573
+
4574
+ // src/hooks/usePanelBus.ts
4575
+ import { useCallback as useCallback10, useEffect as useEffect11, useRef as useRef12, useState as useState15 } from "react";
4576
+ var LEVELS_POLL_MS = 66;
4577
+ function usePanelBus(host, activeSceneId) {
4578
+ const supported = typeof host.getPanelBusState === "function";
4579
+ const [bus, setBus] = useState15(null);
4580
+ const [levels, setLevels] = useState15(null);
4581
+ const [availableFx, setAvailableFx] = useState15([]);
4582
+ const [fxLoading, setFxLoading] = useState15(false);
4583
+ const [fxPickerOpen, setFxPickerOpen] = useState15(false);
4584
+ const fxLoadedRef = useRef12(false);
4585
+ const loadSeqRef = useRef12(0);
4586
+ const reload = useCallback10(async () => {
4587
+ if (!supported || !activeSceneId || !host.getPanelBusState) {
4588
+ setBus(null);
4589
+ return;
4590
+ }
4591
+ const seq = ++loadSeqRef.current;
4592
+ try {
4593
+ const state = await host.getPanelBusState(activeSceneId);
4594
+ if (loadSeqRef.current === seq) setBus(state);
4595
+ } catch {
4596
+ }
4597
+ }, [host, activeSceneId, supported]);
4598
+ useEffect11(() => {
4599
+ setBus(null);
4600
+ setFxPickerOpen(false);
4601
+ void reload();
4602
+ }, [reload]);
4603
+ useEffect11(() => {
4604
+ if (!supported || !activeSceneId || !bus?.engaged || !host.getPanelBusLevels) {
4605
+ setLevels(null);
4606
+ return;
4607
+ }
4608
+ let cancelled = false;
4609
+ const tick = async () => {
4610
+ if (typeof document !== "undefined" && document.hidden) return;
4611
+ try {
4612
+ const next = await host.getPanelBusLevels(activeSceneId);
4613
+ if (!cancelled) setLevels(next);
4614
+ } catch {
4615
+ if (!cancelled) setLevels(null);
4616
+ }
4617
+ };
4618
+ void tick();
4619
+ const id = setInterval(() => void tick(), LEVELS_POLL_MS);
4620
+ return () => {
4621
+ cancelled = true;
4622
+ clearInterval(id);
4623
+ };
4624
+ }, [supported, activeSceneId, bus?.engaged, host]);
4625
+ const loadFxList = useCallback10(
4626
+ async (force) => {
4627
+ if (!supported || !host.getAvailableFx) return;
4628
+ if (fxLoadedRef.current && !force) return;
4629
+ setFxLoading(true);
4630
+ try {
4631
+ const list = await host.getAvailableFx();
4632
+ setAvailableFx(list);
4633
+ fxLoadedRef.current = true;
4634
+ } catch {
4635
+ } finally {
4636
+ setFxLoading(false);
4637
+ }
4638
+ },
4639
+ [host, supported]
4640
+ );
4641
+ const openPicker = useCallback10(
4642
+ (open) => {
4643
+ setFxPickerOpen(open);
4644
+ if (open) void loadFxList(false);
4645
+ },
4646
+ [loadFxList]
4647
+ );
4648
+ const mutate = useCallback10(
4649
+ (fn) => {
4650
+ if (!fn || !activeSceneId) return;
4651
+ void (async () => {
4652
+ try {
4653
+ await fn();
4654
+ } catch {
4655
+ }
4656
+ await reload();
4657
+ })();
4658
+ },
4659
+ [activeSceneId, reload]
4660
+ );
4661
+ return {
4662
+ supported,
4663
+ bus,
4664
+ levels,
4665
+ availableFx,
4666
+ fxLoading,
4667
+ fxPickerOpen,
4668
+ setFxPickerOpen: openPicker,
4669
+ refreshFx: () => void loadFxList(true),
4670
+ reload,
4671
+ onVolumeChange: (volumeDb) => mutate(host.setPanelBusVolume && (() => host.setPanelBusVolume(activeSceneId, volumeDb))),
4672
+ onMuteToggle: () => mutate(
4673
+ host.setPanelBusMute && (() => host.setPanelBusMute(activeSceneId, !(bus?.muted ?? false)))
4674
+ ),
4675
+ onSoloToggle: () => mutate(
4676
+ host.setPanelBusSolo && (() => host.setPanelBusSolo(activeSceneId, !(bus?.soloed ?? false)))
4677
+ ),
4678
+ onAddFx: (pluginId) => mutate(host.loadPanelBusFx && (async () => {
4679
+ await host.loadPanelBusFx(activeSceneId, pluginId);
4680
+ })),
4681
+ onRemoveFx: (fxIndex) => mutate(host.removePanelBusFx && (() => host.removePanelBusFx(activeSceneId, fxIndex))),
4682
+ onToggleFxEnabled: (fxIndex, enabled) => mutate(
4683
+ host.setPanelBusFxEnabled && (() => host.setPanelBusFxEnabled(activeSceneId, fxIndex, enabled))
4684
+ ),
4685
+ onShowFxEditor: (fxIndex) => mutate(
4686
+ host.showPanelBusFxEditor && (() => host.showPanelBusFxEditor(activeSceneId, fxIndex))
4687
+ )
4688
+ };
4689
+ }
4690
+
4129
4691
  // src/components/DownloadPackButton.tsx
4130
- import { useCallback as useCallback9, useEffect as useEffect10, useState as useState12 } from "react";
4131
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
4692
+ import { useCallback as useCallback11, useEffect as useEffect12, useState as useState16 } from "react";
4693
+ import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
4132
4694
  function formatSize(bytes) {
4133
4695
  if (!bytes || bytes <= 0) return "";
4134
4696
  const gb = bytes / 1024 ** 3;
@@ -4144,10 +4706,10 @@ var DownloadPackButton = ({
4144
4706
  variant = "compact",
4145
4707
  onDownloadComplete
4146
4708
  }) => {
4147
- const [status, setStatus] = useState12("idle");
4148
- const [progress, setProgress] = useState12(0);
4149
- const [errorMessage, setErrorMessage] = useState12(null);
4150
- useEffect10(() => {
4709
+ const [status, setStatus] = useState16("idle");
4710
+ const [progress, setProgress] = useState16(0);
4711
+ const [errorMessage, setErrorMessage] = useState16(null);
4712
+ useEffect12(() => {
4151
4713
  const unsub = host.onSamplePackProgress(packId, (p) => {
4152
4714
  setStatus(p.status);
4153
4715
  setProgress(p.progress);
@@ -4162,7 +4724,7 @@ var DownloadPackButton = ({
4162
4724
  });
4163
4725
  return unsub;
4164
4726
  }, [host, packId, onDownloadComplete]);
4165
- const handleClick = useCallback9(async () => {
4727
+ const handleClick = useCallback11(async () => {
4166
4728
  if (status !== "idle" && status !== "error") return;
4167
4729
  try {
4168
4730
  setStatus("downloading");
@@ -4216,8 +4778,8 @@ var DownloadPackButton = ({
4216
4778
  } else {
4217
4779
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
4218
4780
  }
4219
- return /* @__PURE__ */ jsxs14("div", { children: [
4220
- /* @__PURE__ */ jsx18(
4781
+ return /* @__PURE__ */ jsxs16("div", { children: [
4782
+ /* @__PURE__ */ jsx20(
4221
4783
  "button",
4222
4784
  {
4223
4785
  "data-testid": `download-pack-button-${packId}`,
@@ -4228,12 +4790,12 @@ var DownloadPackButton = ({
4228
4790
  children: buttonLabel
4229
4791
  }
4230
4792
  ),
4231
- variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
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 })
4232
4794
  ] });
4233
4795
  };
4234
4796
 
4235
4797
  // src/components/SamplePackCTACard.tsx
4236
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
4798
+ import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
4237
4799
  var SamplePackCTACard = ({
4238
4800
  host,
4239
4801
  pack,
@@ -4241,7 +4803,7 @@ var SamplePackCTACard = ({
4241
4803
  onDownloadComplete
4242
4804
  }) => {
4243
4805
  if (status === "checking") {
4244
- return /* @__PURE__ */ jsx19(
4806
+ return /* @__PURE__ */ jsx21(
4245
4807
  "div",
4246
4808
  {
4247
4809
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -4252,16 +4814,16 @@ var SamplePackCTACard = ({
4252
4814
  }
4253
4815
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
4254
4816
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
4255
- return /* @__PURE__ */ jsxs15(
4817
+ return /* @__PURE__ */ jsxs17(
4256
4818
  "div",
4257
4819
  {
4258
4820
  "data-testid": `sample-pack-cta-${pack.packId}`,
4259
4821
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
4260
4822
  children: [
4261
- /* @__PURE__ */ jsx19("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
4262
- /* @__PURE__ */ jsx19("div", { className: "text-base text-sas-text mb-1", children: headline }),
4263
- /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
4264
- /* @__PURE__ */ jsx19(
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(
4265
4827
  DownloadPackButton,
4266
4828
  {
4267
4829
  host,
@@ -4278,7 +4840,7 @@ var SamplePackCTACard = ({
4278
4840
  };
4279
4841
 
4280
4842
  // src/components/WaveformView.tsx
4281
- import { useEffect as useEffect11, useRef as useRef11, useState as useState13 } from "react";
4843
+ import { useEffect as useEffect13, useRef as useRef13, useState as useState17 } from "react";
4282
4844
 
4283
4845
  // src/components/waveform.ts
4284
4846
  function computePeaks(audioBuffer, bins, targetSamples) {
@@ -4341,7 +4903,7 @@ function drawWaveform(canvas, peaks, options = {}) {
4341
4903
  }
4342
4904
 
4343
4905
  // src/components/WaveformView.tsx
4344
- import { jsx as jsx20 } from "react/jsx-runtime";
4906
+ import { jsx as jsx22 } from "react/jsx-runtime";
4345
4907
  var WaveformView = ({
4346
4908
  host,
4347
4909
  filePath,
@@ -4350,9 +4912,9 @@ var WaveformView = ({
4350
4912
  fillStyle,
4351
4913
  targetSamples
4352
4914
  }) => {
4353
- const canvasRef = useRef11(null);
4354
- const [peaks, setPeaks] = useState13(null);
4355
- useEffect11(() => {
4915
+ const canvasRef = useRef13(null);
4916
+ const [peaks, setPeaks] = useState17(null);
4917
+ useEffect13(() => {
4356
4918
  let cancelled = false;
4357
4919
  let audioContext = null;
4358
4920
  (async () => {
@@ -4378,7 +4940,7 @@ var WaveformView = ({
4378
4940
  cancelled = true;
4379
4941
  };
4380
4942
  }, [host, filePath, bins, targetSamples]);
4381
- useEffect11(() => {
4943
+ useEffect13(() => {
4382
4944
  if (!peaks) return;
4383
4945
  const canvas = canvasRef.current;
4384
4946
  if (!canvas) return;
@@ -4389,7 +4951,7 @@ var WaveformView = ({
4389
4951
  observer.observe(canvas);
4390
4952
  return () => observer.disconnect();
4391
4953
  }, [peaks, fillStyle]);
4392
- return /* @__PURE__ */ jsx20(
4954
+ return /* @__PURE__ */ jsx22(
4393
4955
  "canvas",
4394
4956
  {
4395
4957
  ref: canvasRef,
@@ -4400,8 +4962,8 @@ var WaveformView = ({
4400
4962
  };
4401
4963
 
4402
4964
  // src/components/ScrollingWaveform.tsx
4403
- import { useEffect as useEffect12, useRef as useRef12 } from "react";
4404
- import { jsx as jsx21 } from "react/jsx-runtime";
4965
+ import { useEffect as useEffect14, useRef as useRef14 } from "react";
4966
+ import { jsx as jsx23 } from "react/jsx-runtime";
4405
4967
  var ScrollingWaveform = ({
4406
4968
  getPeakDb,
4407
4969
  active,
@@ -4409,11 +4971,11 @@ var ScrollingWaveform = ({
4409
4971
  className,
4410
4972
  fillStyle
4411
4973
  }) => {
4412
- const canvasRef = useRef12(null);
4413
- const ringRef = useRef12(new Float32Array(columns));
4414
- const writeIdxRef = useRef12(0);
4415
- const rafRef = useRef12(null);
4416
- useEffect12(() => {
4974
+ const canvasRef = useRef14(null);
4975
+ const ringRef = useRef14(new Float32Array(columns));
4976
+ const writeIdxRef = useRef14(0);
4977
+ const rafRef = useRef14(null);
4978
+ useEffect14(() => {
4417
4979
  if (ringRef.current.length !== columns) {
4418
4980
  const next = new Float32Array(columns);
4419
4981
  const prev = ringRef.current;
@@ -4425,7 +4987,7 @@ var ScrollingWaveform = ({
4425
4987
  writeIdxRef.current = writeIdxRef.current % columns;
4426
4988
  }
4427
4989
  }, [columns]);
4428
- useEffect12(() => {
4990
+ useEffect14(() => {
4429
4991
  if (!active) {
4430
4992
  if (rafRef.current !== null) {
4431
4993
  cancelAnimationFrame(rafRef.current);
@@ -4477,7 +5039,7 @@ var ScrollingWaveform = ({
4477
5039
  }
4478
5040
  };
4479
5041
  }, [active, getPeakDb, fillStyle]);
4480
- return /* @__PURE__ */ jsx21(
5042
+ return /* @__PURE__ */ jsx23(
4481
5043
  "canvas",
4482
5044
  {
4483
5045
  ref: canvasRef,
@@ -4488,8 +5050,8 @@ var ScrollingWaveform = ({
4488
5050
  };
4489
5051
 
4490
5052
  // src/components/OffsetScrubber.tsx
4491
- import { useCallback as useCallback10, useEffect as useEffect13, useMemo as useMemo6, useRef as useRef13, useState as useState14 } from "react";
4492
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
5053
+ 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";
4493
5055
  var SLIDER_HEIGHT_PX = 28;
4494
5056
  var TICK_HEIGHT_PX = 14;
4495
5057
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -4502,40 +5064,40 @@ function OffsetScrubber({
4502
5064
  onChange,
4503
5065
  disabled = false
4504
5066
  }) {
4505
- const trackRef = useRef13(null);
4506
- const [draftOffset, setDraftOffset] = useState14(offsetSamples);
4507
- const [isDragging, setIsDragging] = useState14(false);
4508
- useEffect13(() => {
5067
+ const trackRef = useRef15(null);
5068
+ const [draftOffset, setDraftOffset] = useState18(offsetSamples);
5069
+ const [isDragging, setIsDragging] = useState18(false);
5070
+ useEffect15(() => {
4509
5071
  if (!isDragging) setDraftOffset(offsetSamples);
4510
5072
  }, [offsetSamples, isDragging]);
4511
5073
  const sampleRate = cuePoints?.sample_rate ?? 44100;
4512
5074
  const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
4513
- const beatsForRange = useMemo6(() => {
5075
+ const beatsForRange = useMemo8(() => {
4514
5076
  return Math.round(60 / projectBpm * sampleRate);
4515
5077
  }, [projectBpm, sampleRate]);
4516
5078
  const rangeSamples = beatsForRange * meter;
4517
- const sampleToFraction = useCallback10(
5079
+ const sampleToFraction = useCallback12(
4518
5080
  (sample) => {
4519
5081
  const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
4520
5082
  return (clamped + rangeSamples) / (2 * rangeSamples);
4521
5083
  },
4522
5084
  [rangeSamples]
4523
5085
  );
4524
- const fractionToSample = useCallback10(
5086
+ const fractionToSample = useCallback12(
4525
5087
  (fraction) => {
4526
5088
  const clamped = Math.max(0, Math.min(1, fraction));
4527
5089
  return Math.round(clamped * 2 * rangeSamples - rangeSamples);
4528
5090
  },
4529
5091
  [rangeSamples]
4530
5092
  );
4531
- const snapTargets = useMemo6(() => {
5093
+ const snapTargets = useMemo8(() => {
4532
5094
  if (!cuePoints || cuePoints.beats.length === 0) return [];
4533
5095
  const downbeat = cuePoints.beats[0];
4534
5096
  const positives = cuePoints.beats.map((b) => b - downbeat);
4535
5097
  const negatives = positives.slice(1).map((p) => -p);
4536
5098
  return [...negatives, ...positives].sort((a, b) => a - b);
4537
5099
  }, [cuePoints]);
4538
- const snapToBeat = useCallback10(
5100
+ const snapToBeat = useCallback12(
4539
5101
  (sample) => {
4540
5102
  if (snapTargets.length === 0) return sample;
4541
5103
  let best = snapTargets[0];
@@ -4551,7 +5113,7 @@ function OffsetScrubber({
4551
5113
  },
4552
5114
  [snapTargets]
4553
5115
  );
4554
- const handlePointerDown = useCallback10(
5116
+ const handlePointerDown = useCallback12(
4555
5117
  (e) => {
4556
5118
  if (disabled || !cuePoints) return;
4557
5119
  e.preventDefault();
@@ -4585,7 +5147,7 @@ function OffsetScrubber({
4585
5147
  },
4586
5148
  [disabled, cuePoints, fractionToSample, onChange, snapToBeat]
4587
5149
  );
4588
- const handleResetToZero = useCallback10(() => {
5150
+ const handleResetToZero = useCallback12(() => {
4589
5151
  if (disabled) return;
4590
5152
  setDraftOffset(0);
4591
5153
  onChange(0);
@@ -4593,7 +5155,7 @@ function OffsetScrubber({
4593
5155
  const thumbFraction = sampleToFraction(draftOffset);
4594
5156
  const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
4595
5157
  const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
4596
- const ticks = useMemo6(() => {
5158
+ const ticks = useMemo8(() => {
4597
5159
  if (!cuePoints) return [];
4598
5160
  const downbeat = cuePoints.beats[0] ?? 0;
4599
5161
  return cuePoints.beats.map((b, i) => {
@@ -4604,9 +5166,9 @@ function OffsetScrubber({
4604
5166
  });
4605
5167
  }, [cuePoints, sampleToFraction]);
4606
5168
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
4607
- return /* @__PURE__ */ jsxs16("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
4608
- /* @__PURE__ */ jsx22("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
4609
- /* @__PURE__ */ jsxs16(
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(
4610
5172
  "div",
4611
5173
  {
4612
5174
  ref: trackRef,
@@ -4622,7 +5184,7 @@ function OffsetScrubber({
4622
5184
  "aria-valuenow": draftOffset,
4623
5185
  "aria-disabled": isDisabled,
4624
5186
  children: [
4625
- /* @__PURE__ */ jsx22(
5187
+ /* @__PURE__ */ jsx24(
4626
5188
  "div",
4627
5189
  {
4628
5190
  "aria-hidden": "true",
@@ -4630,7 +5192,7 @@ function OffsetScrubber({
4630
5192
  style: { left: "50%" }
4631
5193
  }
4632
5194
  ),
4633
- ticks.map((t) => /* @__PURE__ */ jsx22(
5195
+ ticks.map((t) => /* @__PURE__ */ jsx24(
4634
5196
  "div",
4635
5197
  {
4636
5198
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -4645,7 +5207,7 @@ function OffsetScrubber({
4645
5207
  },
4646
5208
  t.i
4647
5209
  )),
4648
- /* @__PURE__ */ jsx22(
5210
+ /* @__PURE__ */ jsx24(
4649
5211
  "div",
4650
5212
  {
4651
5213
  "data-testid": "offset-scrubber-thumb",
@@ -4662,7 +5224,7 @@ function OffsetScrubber({
4662
5224
  ]
4663
5225
  }
4664
5226
  ),
4665
- /* @__PURE__ */ jsx22(
5227
+ /* @__PURE__ */ jsx24(
4666
5228
  "span",
4667
5229
  {
4668
5230
  "data-testid": "offset-scrubber-readout",
@@ -4670,7 +5232,7 @@ function OffsetScrubber({
4670
5232
  children: formatOffset(draftOffset, sampleRate)
4671
5233
  }
4672
5234
  ),
4673
- /* @__PURE__ */ jsx22(
5235
+ /* @__PURE__ */ jsx24(
4674
5236
  "button",
4675
5237
  {
4676
5238
  type: "button",
@@ -4682,7 +5244,7 @@ function OffsetScrubber({
4682
5244
  children: "\u2316"
4683
5245
  }
4684
5246
  ),
4685
- bpmMismatch && /* @__PURE__ */ jsx22(
5247
+ bpmMismatch && /* @__PURE__ */ jsx24(
4686
5248
  "span",
4687
5249
  {
4688
5250
  "data-testid": "offset-bpm-mismatch",
@@ -4754,16 +5316,16 @@ function synthesizeCuePoints({
4754
5316
  }
4755
5317
 
4756
5318
  // src/panel-core/useGeneratorPanelCore.tsx
4757
- import { useState as useState19, useEffect as useEffect16, useCallback as useCallback14, useRef as useRef17, useMemo as useMemo8 } from "react";
5319
+ import { useState as useState23, useEffect as useEffect18, useCallback as useCallback16, useRef as useRef19, useMemo as useMemo10 } from "react";
4758
5320
 
4759
5321
  // src/hooks/useSceneState.ts
4760
- import { useState as useState15, useCallback as useCallback11, useRef as useRef14 } from "react";
5322
+ import { useState as useState19, useCallback as useCallback13, useRef as useRef16 } from "react";
4761
5323
  function useSceneState(activeSceneId, initialValue) {
4762
- const [stateMap, setStateMap] = useState15(() => /* @__PURE__ */ new Map());
4763
- const activeSceneIdRef = useRef14(activeSceneId);
5324
+ const [stateMap, setStateMap] = useState19(() => /* @__PURE__ */ new Map());
5325
+ const activeSceneIdRef = useRef16(activeSceneId);
4764
5326
  activeSceneIdRef.current = activeSceneId;
4765
5327
  const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
4766
- const setForCurrentScene = useCallback11((value) => {
5328
+ const setForCurrentScene = useCallback13((value) => {
4767
5329
  const sid = activeSceneIdRef.current;
4768
5330
  if (sid === null) return;
4769
5331
  setStateMap((prev) => {
@@ -4774,7 +5336,7 @@ function useSceneState(activeSceneId, initialValue) {
4774
5336
  return newMap;
4775
5337
  });
4776
5338
  }, [initialValue]);
4777
- const setForScene = useCallback11((sceneId, value) => {
5339
+ const setForScene = useCallback13((sceneId, value) => {
4778
5340
  setStateMap((prev) => {
4779
5341
  const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
4780
5342
  const next = typeof value === "function" ? value(current) : value;
@@ -4787,10 +5349,10 @@ function useSceneState(activeSceneId, initialValue) {
4787
5349
  }
4788
5350
 
4789
5351
  // src/hooks/useAnySolo.ts
4790
- import { useEffect as useEffect14, useState as useState16 } from "react";
5352
+ import { useEffect as useEffect16, useState as useState20 } from "react";
4791
5353
  function useAnySolo(host) {
4792
- const [anySolo, setAnySolo] = useState16(false);
4793
- useEffect14(() => {
5354
+ const [anySolo, setAnySolo] = useState20(false);
5355
+ useEffect16(() => {
4794
5356
  let active = true;
4795
5357
  const refresh = () => {
4796
5358
  host.isAnySoloActive().then((v) => {
@@ -4809,7 +5371,7 @@ function useAnySolo(host) {
4809
5371
  }
4810
5372
 
4811
5373
  // src/hooks/useSoundHistory.ts
4812
- import { useCallback as useCallback12, useMemo as useMemo7, useRef as useRef15, useState as useState17 } from "react";
5374
+ import { useCallback as useCallback14, useMemo as useMemo9, useRef as useRef17, useState as useState21 } from "react";
4813
5375
  var EMPTY = { entries: [], cursor: -1 };
4814
5376
  function sameDescriptor(a, b) {
4815
5377
  if (a === b) return true;
@@ -4821,14 +5383,14 @@ function sameDescriptor(a, b) {
4821
5383
  }
4822
5384
  function useSoundHistory(applySound, opts = {}) {
4823
5385
  const max = Math.max(2, opts.max ?? 24);
4824
- const applyRef = useRef15(applySound);
5386
+ const applyRef = useRef17(applySound);
4825
5387
  applyRef.current = applySound;
4826
- const onChangeRef = useRef15(opts.onChange);
5388
+ const onChangeRef = useRef17(opts.onChange);
4827
5389
  onChangeRef.current = opts.onChange;
4828
- const dataRef = useRef15({});
4829
- const [, setVersion] = useState17(0);
4830
- const bump = useCallback12(() => setVersion((v) => v + 1), []);
4831
- const commit = useCallback12(
5390
+ const dataRef = useRef17({});
5391
+ const [, setVersion] = useState21(0);
5392
+ const bump = useCallback14(() => setVersion((v) => v + 1), []);
5393
+ const commit = useCallback14(
4832
5394
  (trackId, next, notify) => {
4833
5395
  dataRef.current = { ...dataRef.current, [trackId]: next };
4834
5396
  bump();
@@ -4836,7 +5398,7 @@ function useSoundHistory(applySound, opts = {}) {
4836
5398
  },
4837
5399
  [bump]
4838
5400
  );
4839
- const record = useCallback12(
5401
+ const record = useCallback14(
4840
5402
  (trackId, descriptor, label) => {
4841
5403
  const h = dataRef.current[trackId];
4842
5404
  const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
@@ -4851,7 +5413,7 @@ function useSoundHistory(applySound, opts = {}) {
4851
5413
  },
4852
5414
  [max, commit]
4853
5415
  );
4854
- const restoreTo = useCallback12(
5416
+ const restoreTo = useCallback14(
4855
5417
  async (trackId, index) => {
4856
5418
  const h = dataRef.current[trackId];
4857
5419
  if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
@@ -4861,7 +5423,7 @@ function useSoundHistory(applySound, opts = {}) {
4861
5423
  },
4862
5424
  [commit]
4863
5425
  );
4864
- const undo = useCallback12(
5426
+ const undo = useCallback14(
4865
5427
  (trackId) => {
4866
5428
  const h = dataRef.current[trackId];
4867
5429
  if (!h || h.cursor <= 0) return Promise.resolve(false);
@@ -4869,7 +5431,7 @@ function useSoundHistory(applySound, opts = {}) {
4869
5431
  },
4870
5432
  [restoreTo]
4871
5433
  );
4872
- const toggleFavorite = useCallback12(
5434
+ const toggleFavorite = useCallback14(
4873
5435
  (trackId, index) => {
4874
5436
  const h = dataRef.current[trackId];
4875
5437
  if (!h || index < 0 || index >= h.entries.length) return;
@@ -4878,7 +5440,7 @@ function useSoundHistory(applySound, opts = {}) {
4878
5440
  },
4879
5441
  [commit]
4880
5442
  );
4881
- const restore = useCallback12(
5443
+ const restore = useCallback14(
4882
5444
  (trackId, state) => {
4883
5445
  const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
4884
5446
  const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
@@ -4887,15 +5449,15 @@ function useSoundHistory(applySound, opts = {}) {
4887
5449
  },
4888
5450
  [commit]
4889
5451
  );
4890
- const list = useCallback12(
5452
+ const list = useCallback14(
4891
5453
  (trackId) => dataRef.current[trackId] ?? EMPTY,
4892
5454
  []
4893
5455
  );
4894
- const canUndo = useCallback12((trackId) => {
5456
+ const canUndo = useCallback14((trackId) => {
4895
5457
  const h = dataRef.current[trackId];
4896
5458
  return !!h && h.cursor > 0;
4897
5459
  }, []);
4898
- const clear = useCallback12(
5460
+ const clear = useCallback14(
4899
5461
  (trackId) => {
4900
5462
  if (dataRef.current[trackId]) {
4901
5463
  const next = { ...dataRef.current };
@@ -4907,11 +5469,11 @@ function useSoundHistory(applySound, opts = {}) {
4907
5469
  },
4908
5470
  [bump]
4909
5471
  );
4910
- const reset = useCallback12(() => {
5472
+ const reset = useCallback14(() => {
4911
5473
  dataRef.current = {};
4912
5474
  bump();
4913
5475
  }, [bump]);
4914
- return useMemo7(
5476
+ return useMemo9(
4915
5477
  () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
4916
5478
  [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
4917
5479
  );
@@ -5044,7 +5606,7 @@ function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
5044
5606
  }
5045
5607
 
5046
5608
  // src/panel-core/useTransitionOps.ts
5047
- import { useCallback as useCallback13, useEffect as useEffect15, useRef as useRef16, useState as useState18 } from "react";
5609
+ import { useCallback as useCallback15, useEffect as useEffect17, useRef as useRef18, useState as useState22 } from "react";
5048
5610
  function useTransitionOps({
5049
5611
  host,
5050
5612
  adapter,
@@ -5061,8 +5623,8 @@ function useTransitionOps({
5061
5623
  resolvedFades
5062
5624
  }) {
5063
5625
  const { identity } = adapter;
5064
- const appliedFadeAutomationRef = useRef16(/* @__PURE__ */ new Set());
5065
- const applyCrossfadeAutomation = useCallback13(
5626
+ const appliedFadeAutomationRef = useRef18(/* @__PURE__ */ new Set());
5627
+ const applyCrossfadeAutomation = useCallback15(
5066
5628
  async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
5067
5629
  if (host.setTrackVolumeAutomation) {
5068
5630
  const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
@@ -5079,7 +5641,7 @@ function useTransitionOps({
5079
5641
  },
5080
5642
  [host]
5081
5643
  );
5082
- const applyFadeAutomation = useCallback13(
5644
+ const applyFadeAutomation = useCallback15(
5083
5645
  async (trackId, direction, bars, bpm, sliderPos, gesture) => {
5084
5646
  if (!host.setTrackVolumeAutomation) return;
5085
5647
  const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
@@ -5088,8 +5650,8 @@ function useTransitionOps({
5088
5650
  },
5089
5651
  [host]
5090
5652
  );
5091
- const [isCreatingCrossfade, setIsCreatingCrossfade] = useState18(false);
5092
- const handleCreateCrossfade = useCallback13(
5653
+ const [isCreatingCrossfade, setIsCreatingCrossfade] = useState22(false);
5654
+ const handleCreateCrossfade = useCallback15(
5093
5655
  async (origin, target) => {
5094
5656
  const scene = activeSceneId;
5095
5657
  const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
@@ -5217,8 +5779,8 @@ function useTransitionOps({
5217
5779
  loadTracks
5218
5780
  ]
5219
5781
  );
5220
- const [isCreatingFade, setIsCreatingFade] = useState18(false);
5221
- const handleCreateFade = useCallback13(
5782
+ const [isCreatingFade, setIsCreatingFade] = useState22(false);
5783
+ const handleCreateFade = useCallback15(
5222
5784
  async (selection, direction, gesture) => {
5223
5785
  const scene = activeSceneId;
5224
5786
  const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
@@ -5328,7 +5890,7 @@ function useTransitionOps({
5328
5890
  loadTracks
5329
5891
  ]
5330
5892
  );
5331
- const handleCrossfadeMute = useCallback13(
5893
+ const handleCrossfadeMute = useCallback15(
5332
5894
  (pair) => {
5333
5895
  const newMuted = !pair.origin.runtimeState.muted;
5334
5896
  for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
@@ -5343,7 +5905,7 @@ function useTransitionOps({
5343
5905
  },
5344
5906
  [host, setTracks]
5345
5907
  );
5346
- const handleCrossfadeSolo = useCallback13(
5908
+ const handleCrossfadeSolo = useCallback15(
5347
5909
  (pair) => {
5348
5910
  const newSolo = !pair.origin.runtimeState.solo;
5349
5911
  for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
@@ -5358,7 +5920,7 @@ function useTransitionOps({
5358
5920
  },
5359
5921
  [host, setTracks]
5360
5922
  );
5361
- const handleCrossfadeDelete = useCallback13(
5923
+ const handleCrossfadeDelete = useCallback15(
5362
5924
  async (pair) => {
5363
5925
  try {
5364
5926
  for (const member of [pair.origin, pair.target]) {
@@ -5384,8 +5946,8 @@ function useTransitionOps({
5384
5946
  },
5385
5947
  [host, activeSceneId, setCrossfadePairsMeta, setTracks]
5386
5948
  );
5387
- const crossfadeSliderTimers = useRef16({});
5388
- const handleCrossfadeSlider = useCallback13(
5949
+ const crossfadeSliderTimers = useRef18({});
5950
+ const handleCrossfadeSlider = useCallback15(
5389
5951
  (pair, pos) => {
5390
5952
  setCrossfadePairsMeta(
5391
5953
  (prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
@@ -5418,7 +5980,7 @@ function useTransitionOps({
5418
5980
  },
5419
5981
  [host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
5420
5982
  );
5421
- const handleFadeDelete = useCallback13(
5983
+ const handleFadeDelete = useCallback15(
5422
5984
  async (fade) => {
5423
5985
  try {
5424
5986
  await host.deleteTrack(fade.track.handle.id);
@@ -5438,8 +6000,8 @@ function useTransitionOps({
5438
6000
  },
5439
6001
  [host, activeSceneId, setFadesMeta, setTracks]
5440
6002
  );
5441
- const fadeSliderTimers = useRef16({});
5442
- const handleFadeSlider = useCallback13(
6003
+ const fadeSliderTimers = useRef18({});
6004
+ const handleFadeSlider = useCallback15(
5443
6005
  (fade, pos) => {
5444
6006
  setFadesMeta(
5445
6007
  (prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
@@ -5469,8 +6031,8 @@ function useTransitionOps({
5469
6031
  },
5470
6032
  [host, activeSceneId, applyFadeAutomation, setFadesMeta]
5471
6033
  );
5472
- const lastResyncKeyRef = useRef16("");
5473
- useEffect15(() => {
6034
+ const lastResyncKeyRef = useRef18("");
6035
+ useEffect17(() => {
5474
6036
  if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
5475
6037
  return;
5476
6038
  }
@@ -5511,7 +6073,7 @@ function useTransitionOps({
5511
6073
  cancelled = true;
5512
6074
  };
5513
6075
  }, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
5514
- useEffect15(() => {
6076
+ useEffect17(() => {
5515
6077
  if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
5516
6078
  void (async () => {
5517
6079
  const mc = await host.getMusicalContext();
@@ -5545,7 +6107,7 @@ function useTransitionOps({
5545
6107
  }
5546
6108
 
5547
6109
  // src/panel-core/useGeneratorPanelCore.tsx
5548
- import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
6110
+ import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
5549
6111
  var EMPTY_PLACEHOLDERS = [];
5550
6112
  function useGeneratorPanelCore({
5551
6113
  ui,
@@ -5565,8 +6127,8 @@ function useGeneratorPanelCore({
5565
6127
  } = ui;
5566
6128
  const { identity, features } = adapter;
5567
6129
  const logTag = identity.logTag;
5568
- const adapterRef = useRef17(adapter);
5569
- useEffect16(() => {
6130
+ const adapterRef = useRef19(adapter);
6131
+ useEffect18(() => {
5570
6132
  if (adapterRef.current !== adapter) {
5571
6133
  adapterRef.current = adapter;
5572
6134
  console.warn(
@@ -5576,27 +6138,27 @@ function useGeneratorPanelCore({
5576
6138
  }, [adapter, logTag]);
5577
6139
  const supportsMeters = typeof host.getTrackLevels === "function";
5578
6140
  const trackLevels = useTrackLevels(host, isExpanded);
5579
- const [tracks, setTracks] = useState19([]);
5580
- const [isLoadingTracks, setIsLoadingTracks] = useState19(false);
5581
- const [importOpen, setImportOpen] = useState19(false);
5582
- const [soundImportTarget, setSoundImportTarget] = useState19(null);
5583
- const [designerView, setDesignerView] = useState19(false);
5584
- const [transitionSourceTotal, setTransitionSourceTotal] = useState19(0);
5585
- const [crossfadePairsMeta, setCrossfadePairsMeta] = useState19([]);
5586
- const [fadesMeta, setFadesMeta] = useState19([]);
5587
- const [genericGroupMetas, setGenericGroupMetas] = useState19({});
6141
+ const [tracks, setTracks] = useState23([]);
6142
+ const [isLoadingTracks, setIsLoadingTracks] = useState23(false);
6143
+ const [importOpen, setImportOpen] = useState23(false);
6144
+ const [soundImportTarget, setSoundImportTarget] = useState23(null);
6145
+ const [designerView, setDesignerView] = useState23(false);
6146
+ const [transitionSourceTotal, setTransitionSourceTotal] = useState23(0);
6147
+ const [crossfadePairsMeta, setCrossfadePairsMeta] = useState23([]);
6148
+ const [fadesMeta, setFadesMeta] = useState23([]);
6149
+ const [genericGroupMetas, setGenericGroupMetas] = useState23({});
5588
6150
  const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
5589
6151
  const [placeholders, , setPlaceholdersForScene] = useSceneState(
5590
6152
  activeSceneId,
5591
6153
  EMPTY_PLACEHOLDERS
5592
6154
  );
5593
- const saveTimeoutRefs = useRef17({});
5594
- const editLoadStartedRef = useRef17(/* @__PURE__ */ new Set());
5595
- const [availableInstruments, setAvailableInstruments] = useState19([]);
5596
- const [instrumentsLoading, setInstrumentsLoading] = useState19(false);
5597
- const engineToDbIdRef = useRef17(/* @__PURE__ */ new Map());
5598
- const tracksLoadedForSceneRef = useRef17(null);
5599
- const persistSoundHistory = useCallback14(
6155
+ const saveTimeoutRefs = useRef19({});
6156
+ const editLoadStartedRef = useRef19(/* @__PURE__ */ new Set());
6157
+ const [availableInstruments, setAvailableInstruments] = useState23([]);
6158
+ const [instrumentsLoading, setInstrumentsLoading] = useState23(false);
6159
+ const engineToDbIdRef = useRef19(/* @__PURE__ */ new Map());
6160
+ const tracksLoadedForSceneRef = useRef19(null);
6161
+ const persistSoundHistory = useCallback16(
5600
6162
  (trackId, state) => {
5601
6163
  if (!activeSceneId) return;
5602
6164
  const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
@@ -5616,7 +6178,7 @@ function useGeneratorPanelCore({
5616
6178
  setItems: setTracks,
5617
6179
  getId: (t) => t.handle.dbId
5618
6180
  });
5619
- const loadTracks = useCallback14(
6181
+ const loadTracks = useCallback16(
5620
6182
  async (incremental = false) => {
5621
6183
  const sceneAtStart = activeSceneId;
5622
6184
  if (!sceneAtStart) {
@@ -5741,18 +6303,18 @@ function useGeneratorPanelCore({
5741
6303
  },
5742
6304
  [host, activeSceneId, soundHistory, adapter, logTag]
5743
6305
  );
5744
- useEffect16(() => {
6306
+ useEffect18(() => {
5745
6307
  loadTracks();
5746
6308
  }, [loadTracks]);
5747
- useEffect16(() => {
6309
+ useEffect18(() => {
5748
6310
  const map = /* @__PURE__ */ new Map();
5749
6311
  for (const t of tracks) {
5750
6312
  map.set(t.handle.id, t.handle.dbId);
5751
6313
  }
5752
6314
  engineToDbIdRef.current = map;
5753
6315
  }, [tracks]);
5754
- const loadedCompletedIdsRef = useRef17(/* @__PURE__ */ new Set());
5755
- useEffect16(() => {
6316
+ const loadedCompletedIdsRef = useRef19(/* @__PURE__ */ new Set());
6317
+ useEffect18(() => {
5756
6318
  if (placeholders.length === 0) {
5757
6319
  loadedCompletedIdsRef.current.clear();
5758
6320
  return;
@@ -5771,16 +6333,16 @@ function useGeneratorPanelCore({
5771
6333
  loadTracks(true);
5772
6334
  }
5773
6335
  }, [placeholders, loadTracks, logTag]);
5774
- const adoptAndLoad = useCallback14(() => {
6336
+ const adoptAndLoad = useCallback16(() => {
5775
6337
  loadTracks(true);
5776
6338
  }, [loadTracks]);
5777
- useEffect16(() => {
6339
+ useEffect18(() => {
5778
6340
  const unsub = host.onEngineReady(() => {
5779
6341
  adoptAndLoad();
5780
6342
  });
5781
6343
  return unsub;
5782
6344
  }, [host, adoptAndLoad]);
5783
- useEffect16(() => {
6345
+ useEffect18(() => {
5784
6346
  if (typeof host.onAfterAgentMutation !== "function") return;
5785
6347
  let timer = null;
5786
6348
  const unsub = host.onAfterAgentMutation(() => {
@@ -5795,13 +6357,13 @@ function useGeneratorPanelCore({
5795
6357
  if (timer) clearTimeout(timer);
5796
6358
  };
5797
6359
  }, [host, loadTracks]);
5798
- useEffect16(() => {
6360
+ useEffect18(() => {
5799
6361
  const unsub = host.onTrackStateChange((trackId, state) => {
5800
6362
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
5801
6363
  });
5802
6364
  return unsub;
5803
6365
  }, [host]);
5804
- useEffect16(() => {
6366
+ useEffect18(() => {
5805
6367
  if (!features.bulkComposePlaceholders) return;
5806
6368
  console.log(`[${logTag}] Subscribing to composeProgress`);
5807
6369
  const unsub = host.onComposeProgress((event) => {
@@ -5835,7 +6397,7 @@ function useGeneratorPanelCore({
5835
6397
  });
5836
6398
  return unsub;
5837
6399
  }, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
5838
- useEffect16(() => {
6400
+ useEffect18(() => {
5839
6401
  const refs = saveTimeoutRefs;
5840
6402
  return () => {
5841
6403
  for (const timeout of Object.values(refs.current)) {
@@ -5843,9 +6405,9 @@ function useGeneratorPanelCore({
5843
6405
  }
5844
6406
  };
5845
6407
  }, []);
5846
- const isAddingTrackRef = useRef17(false);
5847
- const [isAddingTrack, setIsAddingTrack] = useState19(false);
5848
- const handleAddTrack = useCallback14(async () => {
6408
+ const isAddingTrackRef = useRef19(false);
6409
+ const [isAddingTrack, setIsAddingTrack] = useState23(false);
6410
+ const handleAddTrack = useCallback16(async () => {
5849
6411
  if (isAddingTrackRef.current) return;
5850
6412
  if (!activeSceneId) {
5851
6413
  host.showToast("warning", "Select SCENE");
@@ -5885,7 +6447,7 @@ function useGeneratorPanelCore({
5885
6447
  setIsAddingTrack(false);
5886
6448
  }
5887
6449
  }, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
5888
- const handlePortTrack = useCallback14(
6450
+ const handlePortTrack = useCallback16(
5889
6451
  async (sel) => {
5890
6452
  if (!activeSceneId) {
5891
6453
  host.showToast("warning", "Select SCENE");
@@ -5942,7 +6504,7 @@ function useGeneratorPanelCore({
5942
6504
  },
5943
6505
  [host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
5944
6506
  );
5945
- const handleSoundImportPick = useCallback14(
6507
+ const handleSoundImportPick = useCallback16(
5946
6508
  async (sel) => {
5947
6509
  const target = soundImportTarget;
5948
6510
  if (!target || !host.getTrackSound) {
@@ -5973,8 +6535,8 @@ function useGeneratorPanelCore({
5973
6535
  },
5974
6536
  [soundImportTarget, host, adapter, identity.familyKey, soundHistory]
5975
6537
  );
5976
- const [isExportingMidi, setIsExportingMidi] = useState19(false);
5977
- const handleExportMidi = useCallback14(async () => {
6538
+ const [isExportingMidi, setIsExportingMidi] = useState23(false);
6539
+ const handleExportMidi = useCallback16(async () => {
5978
6540
  if (isExportingMidi) return;
5979
6541
  setIsExportingMidi(true);
5980
6542
  try {
@@ -6005,10 +6567,10 @@ function useGeneratorPanelCore({
6005
6567
  const xfFromId = sceneContext?.transitionFromSceneId ?? null;
6006
6568
  const xfToId = sceneContext?.transitionToSceneId ?? null;
6007
6569
  const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
6008
- useEffect16(() => {
6570
+ useEffect18(() => {
6009
6571
  if (!canCrossfade) setDesignerView(false);
6010
6572
  }, [canCrossfade]);
6011
- useEffect16(() => {
6573
+ useEffect18(() => {
6012
6574
  if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
6013
6575
  setTransitionSourceTotal(0);
6014
6576
  return;
@@ -6024,12 +6586,12 @@ function useGeneratorPanelCore({
6024
6586
  };
6025
6587
  }, [canCrossfade, xfFromId, xfToId, host]);
6026
6588
  const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
6027
- useEffect16(() => {
6589
+ useEffect18(() => {
6028
6590
  if (!onHeaderContent) return;
6029
6591
  const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
6030
6592
  onHeaderContent(
6031
- /* @__PURE__ */ jsxs17("div", { className: "flex gap-1 items-center", children: [
6032
- features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx23(
6593
+ /* @__PURE__ */ jsxs19("div", { className: "flex gap-1 items-center", children: [
6594
+ features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx25(
6033
6595
  "button",
6034
6596
  {
6035
6597
  "data-testid": `import-from-scene-${identity.familyKey}-button`,
@@ -6043,7 +6605,7 @@ function useGeneratorPanelCore({
6043
6605
  children: identity.importTrackLabel ?? "Import Track"
6044
6606
  }
6045
6607
  ),
6046
- (!canCrossfade || !designerView) && /* @__PURE__ */ jsx23(
6608
+ (!canCrossfade || !designerView) && /* @__PURE__ */ jsx25(
6047
6609
  "button",
6048
6610
  {
6049
6611
  "data-testid": `add-${identity.familyKey}-track-button`,
@@ -6059,7 +6621,7 @@ function useGeneratorPanelCore({
6059
6621
  children: identity.addTrackLabel ?? "Add Track"
6060
6622
  }
6061
6623
  ),
6062
- canCrossfade && /* @__PURE__ */ jsxs17(
6624
+ canCrossfade && /* @__PURE__ */ jsxs19(
6063
6625
  "button",
6064
6626
  {
6065
6627
  "data-testid": `${identity.familyKey}-view-toggle`,
@@ -6078,7 +6640,7 @@ function useGeneratorPanelCore({
6078
6640
  title: designerView ? "Back to the track list" : "Open the transition designer",
6079
6641
  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",
6080
6642
  children: [
6081
- transitionSourceTotal > 0 && /* @__PURE__ */ jsx23(
6643
+ transitionSourceTotal > 0 && /* @__PURE__ */ jsx25(
6082
6644
  "span",
6083
6645
  {
6084
6646
  className: "absolute inset-y-0 left-0 bg-sas-accent/25",
@@ -6086,7 +6648,7 @@ function useGeneratorPanelCore({
6086
6648
  "aria-hidden": true
6087
6649
  }
6088
6650
  ),
6089
- /* @__PURE__ */ jsxs17("span", { className: "relative", children: [
6651
+ /* @__PURE__ */ jsxs19("span", { className: "relative", children: [
6090
6652
  "\u21C4 ",
6091
6653
  designerView ? "Transition" : "Tracks",
6092
6654
  transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
@@ -6117,7 +6679,7 @@ function useGeneratorPanelCore({
6117
6679
  identity,
6118
6680
  features.importTracks
6119
6681
  ]);
6120
- useEffect16(() => {
6682
+ useEffect18(() => {
6121
6683
  if (!onLoading) return;
6122
6684
  const anyGenerating = tracks.some((t) => t.isGenerating);
6123
6685
  onLoading(isLoadingTracks || anyGenerating || isBulkActive);
@@ -6125,7 +6687,7 @@ function useGeneratorPanelCore({
6125
6687
  onLoading(false);
6126
6688
  };
6127
6689
  }, [onLoading, isLoadingTracks, tracks, isBulkActive]);
6128
- const handleDeleteTrack = useCallback14(
6690
+ const handleDeleteTrack = useCallback16(
6129
6691
  async (trackId) => {
6130
6692
  try {
6131
6693
  await host.deleteTrack(trackId);
@@ -6141,7 +6703,7 @@ function useGeneratorPanelCore({
6141
6703
  },
6142
6704
  [host, activeSceneId]
6143
6705
  );
6144
- const handlePromptChange = useCallback14(
6706
+ const handlePromptChange = useCallback16(
6145
6707
  (trackId, prompt) => {
6146
6708
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
6147
6709
  const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
@@ -6157,7 +6719,7 @@ function useGeneratorPanelCore({
6157
6719
  },
6158
6720
  [host, activeSceneId]
6159
6721
  );
6160
- const resolvedGenericGroups = useMemo8(() => {
6722
+ const resolvedGenericGroups = useMemo10(() => {
6161
6723
  const out = {};
6162
6724
  for (const ext of adapter.groupExtensions ?? []) {
6163
6725
  out[ext.metaKey] = resolveTrackGroups(
@@ -6171,18 +6733,18 @@ function useGeneratorPanelCore({
6171
6733
  }
6172
6734
  return out;
6173
6735
  }, [adapter, genericGroupMetas, tracks]);
6174
- const genericGroupMemberDbIds = useMemo8(() => {
6736
+ const genericGroupMemberDbIds = useMemo10(() => {
6175
6737
  const s = /* @__PURE__ */ new Set();
6176
6738
  for (const r of Object.values(resolvedGenericGroups)) {
6177
6739
  for (const dbId of r.memberDbIds) s.add(dbId);
6178
6740
  }
6179
6741
  return s;
6180
6742
  }, [resolvedGenericGroups]);
6181
- const engineToDbId = useCallback14(
6743
+ const engineToDbId = useCallback16(
6182
6744
  (trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
6183
6745
  []
6184
6746
  );
6185
- const updateTrack = useCallback14(
6747
+ const updateTrack = useCallback16(
6186
6748
  (trackId, patch) => {
6187
6749
  setTracks(
6188
6750
  (prev) => prev.map(
@@ -6192,18 +6754,18 @@ function useGeneratorPanelCore({
6192
6754
  },
6193
6755
  []
6194
6756
  );
6195
- const markEditLoaded = useCallback14((trackId) => {
6757
+ const markEditLoaded = useCallback16((trackId) => {
6196
6758
  editLoadStartedRef.current.add(trackId);
6197
6759
  }, []);
6198
- const tracksRef = useRef17(tracks);
6199
- useEffect16(() => {
6760
+ const tracksRef = useRef19(tracks);
6761
+ useEffect18(() => {
6200
6762
  tracksRef.current = tracks;
6201
6763
  }, [tracks]);
6202
- const resolvedGenericGroupsRef = useRef17(resolvedGenericGroups);
6203
- useEffect16(() => {
6764
+ const resolvedGenericGroupsRef = useRef19(resolvedGenericGroups);
6765
+ useEffect18(() => {
6204
6766
  resolvedGenericGroupsRef.current = resolvedGenericGroups;
6205
6767
  }, [resolvedGenericGroups]);
6206
- const makeServices = useCallback14(() => {
6768
+ const makeServices = useCallback16(() => {
6207
6769
  return {
6208
6770
  host,
6209
6771
  activeSceneId,
@@ -6222,7 +6784,7 @@ function useGeneratorPanelCore({
6222
6784
  resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
6223
6785
  };
6224
6786
  }, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
6225
- const handleGenerate = useCallback14(
6787
+ const handleGenerate = useCallback16(
6226
6788
  async (trackId) => {
6227
6789
  const track = tracks.find((t) => t.handle.id === trackId);
6228
6790
  if (!track || !track.prompt.trim()) return;
@@ -6249,7 +6811,7 @@ function useGeneratorPanelCore({
6249
6811
  },
6250
6812
  [host, adapter, tracks, isAuthenticated, makeServices]
6251
6813
  );
6252
- const handleMuteToggle = useCallback14(
6814
+ const handleMuteToggle = useCallback16(
6253
6815
  (trackId) => {
6254
6816
  const track = tracks.find((t) => t.handle.id === trackId);
6255
6817
  if (!track) return;
@@ -6269,7 +6831,7 @@ function useGeneratorPanelCore({
6269
6831
  },
6270
6832
  [host, tracks]
6271
6833
  );
6272
- const handleSoloToggle = useCallback14(
6834
+ const handleSoloToggle = useCallback16(
6273
6835
  (trackId) => {
6274
6836
  const track = tracks.find((t) => t.handle.id === trackId);
6275
6837
  if (!track) return;
@@ -6289,7 +6851,7 @@ function useGeneratorPanelCore({
6289
6851
  },
6290
6852
  [host, tracks]
6291
6853
  );
6292
- const handleVolumeChange = useCallback14(
6854
+ const handleVolumeChange = useCallback16(
6293
6855
  (trackId, volume) => {
6294
6856
  setTracks(
6295
6857
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
@@ -6299,7 +6861,7 @@ function useGeneratorPanelCore({
6299
6861
  },
6300
6862
  [host]
6301
6863
  );
6302
- const handlePanChange = useCallback14(
6864
+ const handlePanChange = useCallback16(
6303
6865
  (trackId, pan) => {
6304
6866
  setTracks(
6305
6867
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
@@ -6309,7 +6871,7 @@ function useGeneratorPanelCore({
6309
6871
  },
6310
6872
  [host]
6311
6873
  );
6312
- const handleShuffle = useCallback14(
6874
+ const handleShuffle = useCallback16(
6313
6875
  async (trackId) => {
6314
6876
  const track = tracks.find((t) => t.handle.id === trackId);
6315
6877
  if (!track) return;
@@ -6351,7 +6913,7 @@ function useGeneratorPanelCore({
6351
6913
  },
6352
6914
  [host, adapter, tracks, soundHistory, logTag]
6353
6915
  );
6354
- const handleCopy = useCallback14(
6916
+ const handleCopy = useCallback16(
6355
6917
  async (trackId) => {
6356
6918
  try {
6357
6919
  const newHandle = await host.duplicateTrack(trackId);
@@ -6364,7 +6926,7 @@ function useGeneratorPanelCore({
6364
6926
  },
6365
6927
  [host, loadTracks]
6366
6928
  );
6367
- const handleFxToggle = useCallback14(
6929
+ const handleFxToggle = useCallback16(
6368
6930
  (trackId, category, enabled) => {
6369
6931
  setTracks(
6370
6932
  (prev) => prev.map(
@@ -6387,7 +6949,7 @@ function useGeneratorPanelCore({
6387
6949
  },
6388
6950
  [host]
6389
6951
  );
6390
- const handleFxPresetChange = useCallback14(
6952
+ const handleFxPresetChange = useCallback16(
6391
6953
  (trackId, category, presetIndex) => {
6392
6954
  setTracks(
6393
6955
  (prev) => prev.map(
@@ -6413,7 +6975,7 @@ function useGeneratorPanelCore({
6413
6975
  },
6414
6976
  [host]
6415
6977
  );
6416
- const handleFxDryWetChange = useCallback14(
6978
+ const handleFxDryWetChange = useCallback16(
6417
6979
  (trackId, category, value) => {
6418
6980
  setTracks(
6419
6981
  (prev) => prev.map(
@@ -6425,7 +6987,7 @@ function useGeneratorPanelCore({
6425
6987
  },
6426
6988
  [host]
6427
6989
  );
6428
- const toggleFxDrawer = useCallback14(
6990
+ const toggleFxDrawer = useCallback16(
6429
6991
  (trackId) => {
6430
6992
  setTracks(
6431
6993
  (prev) => prev.map((t) => {
@@ -6447,7 +7009,7 @@ function useGeneratorPanelCore({
6447
7009
  },
6448
7010
  [host, tracks]
6449
7011
  );
6450
- const loadEditNotes = useCallback14(
7012
+ const loadEditNotes = useCallback16(
6451
7013
  async (trackId) => {
6452
7014
  try {
6453
7015
  const mc = await host.getMusicalContext();
@@ -6465,7 +7027,7 @@ function useGeneratorPanelCore({
6465
7027
  },
6466
7028
  [host, logTag]
6467
7029
  );
6468
- const handleNotesChange = useCallback14(
7030
+ const handleNotesChange = useCallback16(
6469
7031
  (trackId, notes) => {
6470
7032
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
6471
7033
  const key = `edit:${trackId}`;
@@ -6495,7 +7057,7 @@ function useGeneratorPanelCore({
6495
7057
  },
6496
7058
  [host]
6497
7059
  );
6498
- const handleTabChange = useCallback14(
7060
+ const handleTabChange = useCallback16(
6499
7061
  (trackId, tab) => {
6500
7062
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
6501
7063
  if (tab === "fx") {
@@ -6520,10 +7082,10 @@ function useGeneratorPanelCore({
6520
7082
  },
6521
7083
  [host, availableInstruments.length, instrumentsLoading, loadEditNotes]
6522
7084
  );
6523
- const handleProgressChange = useCallback14((trackId, pct) => {
7085
+ const handleProgressChange = useCallback16((trackId, pct) => {
6524
7086
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
6525
7087
  }, []);
6526
- const handleToggleDrawer = useCallback14((trackId) => {
7088
+ const handleToggleDrawer = useCallback16((trackId) => {
6527
7089
  setTracks(
6528
7090
  (prev) => prev.map((t) => {
6529
7091
  if (t.handle.id !== trackId) return t;
@@ -6532,7 +7094,7 @@ function useGeneratorPanelCore({
6532
7094
  })
6533
7095
  );
6534
7096
  }, []);
6535
- const handleInstrumentSelect = useCallback14(
7097
+ const handleInstrumentSelect = useCallback16(
6536
7098
  async (trackId, pluginId) => {
6537
7099
  const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
6538
7100
  if (isDefaultInstrument) {
@@ -6585,7 +7147,7 @@ function useGeneratorPanelCore({
6585
7147
  },
6586
7148
  [host, identity.defaultInstrumentPluginId, logTag]
6587
7149
  );
6588
- const handleShowEditor = useCallback14(
7150
+ const handleShowEditor = useCallback16(
6589
7151
  async (trackId) => {
6590
7152
  try {
6591
7153
  await host.showInstrumentEditor(trackId);
@@ -6596,12 +7158,12 @@ function useGeneratorPanelCore({
6596
7158
  },
6597
7159
  [host]
6598
7160
  );
6599
- const handleBackToInstruments = useCallback14((trackId) => {
7161
+ const handleBackToInstruments = useCallback16((trackId) => {
6600
7162
  setTracks(
6601
7163
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
6602
7164
  );
6603
7165
  }, []);
6604
- const handleRefreshInstruments = useCallback14(() => {
7166
+ const handleRefreshInstruments = useCallback16(() => {
6605
7167
  setInstrumentsLoading(true);
6606
7168
  host.getAvailableInstruments().then((instruments) => {
6607
7169
  setAvailableInstruments(instruments);
@@ -6610,13 +7172,13 @@ function useGeneratorPanelCore({
6610
7172
  setInstrumentsLoading(false);
6611
7173
  });
6612
7174
  }, [host]);
6613
- const onAuditionNote = useCallback14(
7175
+ const onAuditionNote = useCallback16(
6614
7176
  (trackId, pitch, velocity, ms) => {
6615
7177
  void host.auditionNote(trackId, pitch, velocity, ms);
6616
7178
  },
6617
7179
  [host]
6618
7180
  );
6619
- const { resolvedCrossfadePairs, crossfadeMemberDbIds } = useMemo8(() => {
7181
+ const { resolvedCrossfadePairs, crossfadeMemberDbIds } = useMemo10(() => {
6620
7182
  const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
6621
7183
  const pairs = [];
6622
7184
  const members = /* @__PURE__ */ new Set();
@@ -6631,7 +7193,7 @@ function useGeneratorPanelCore({
6631
7193
  }
6632
7194
  return { resolvedCrossfadePairs: pairs, crossfadeMemberDbIds: members };
6633
7195
  }, [tracks, crossfadePairsMeta]);
6634
- const { resolvedFades, fadeMemberDbIds } = useMemo8(() => {
7196
+ const { resolvedFades, fadeMemberDbIds } = useMemo10(() => {
6635
7197
  const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
6636
7198
  const list = [];
6637
7199
  const members = /* @__PURE__ */ new Set();
@@ -6659,7 +7221,7 @@ function useGeneratorPanelCore({
6659
7221
  resolvedCrossfadePairs,
6660
7222
  resolvedFades
6661
7223
  });
6662
- const setGroupMute = useCallback14(
7224
+ const setGroupMute = useCallback16(
6663
7225
  (trackIds, muted) => {
6664
7226
  for (const id of trackIds) {
6665
7227
  setTracks(
@@ -6671,7 +7233,7 @@ function useGeneratorPanelCore({
6671
7233
  },
6672
7234
  [host]
6673
7235
  );
6674
- const setGroupSolo = useCallback14(
7236
+ const setGroupSolo = useCallback16(
6675
7237
  (trackIds, solo) => {
6676
7238
  for (const id of trackIds) {
6677
7239
  setTracks(
@@ -6683,7 +7245,7 @@ function useGeneratorPanelCore({
6683
7245
  },
6684
7246
  [host]
6685
7247
  );
6686
- const deleteGroup = useCallback14(
7248
+ const deleteGroup = useCallback16(
6687
7249
  async (members, cleanupKeySuffixes) => {
6688
7250
  for (const member of members) {
6689
7251
  try {
@@ -6703,7 +7265,7 @@ function useGeneratorPanelCore({
6703
7265
  },
6704
7266
  [host, activeSceneId, loadTracks]
6705
7267
  );
6706
- const handlers = useMemo8(
7268
+ const handlers = useMemo10(
6707
7269
  () => ({
6708
7270
  promptChange: handlePromptChange,
6709
7271
  generate: (trackId) => {
@@ -6817,8 +7379,8 @@ function useGeneratorPanelCore({
6817
7379
  }
6818
7380
 
6819
7381
  // src/panel-core/GeneratorPanelShell.tsx
6820
- import React20, { useCallback as useCallback15 } from "react";
6821
- import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
7382
+ import React22, { useCallback as useCallback17 } from "react";
7383
+ import { Fragment as Fragment6, jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
6822
7384
  function GeneratorPanelShell({ core, slots }) {
6823
7385
  const {
6824
7386
  ui,
@@ -6871,8 +7433,9 @@ function GeneratorPanelShell({ core, slots }) {
6871
7433
  deleteGroup
6872
7434
  } = core;
6873
7435
  const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
7436
+ const panelBus = usePanelBus(host, activeSceneId);
6874
7437
  const { identity, features } = adapter;
6875
- const buildRowProps = useCallback15(
7438
+ const buildRowProps = useCallback17(
6876
7439
  (track, drag) => {
6877
7440
  const id = track.handle.id;
6878
7441
  const pickerProps = features.instrumentPicker ? {
@@ -6924,6 +7487,7 @@ function GeneratorPanelShell({ core, slots }) {
6924
7487
  onVolumeChange: (vol) => handlers.volumeChange(id, vol),
6925
7488
  onPanChange: (pan) => handlers.panChange(id, pan),
6926
7489
  onFxToggle: (cat, enabled) => handleFxToggle(id, cat, enabled),
7490
+ externalFxHost: host,
6927
7491
  onFxPresetChange: (cat, idx) => handleFxPresetChange(id, cat, idx),
6928
7492
  onFxDryWetChange: (cat, val) => handleFxDryWetChange(id, cat, val),
6929
7493
  onToggleFxDrawer: () => handlers.toggleFxDrawer(id),
@@ -6971,12 +7535,12 @@ function GeneratorPanelShell({ core, slots }) {
6971
7535
  ]
6972
7536
  );
6973
7537
  if (!activeSceneId) {
6974
- return /* @__PURE__ */ jsx24(
7538
+ return /* @__PURE__ */ jsx26(
6975
7539
  "div",
6976
7540
  {
6977
7541
  "data-testid": `no-scene-placeholder-${identity.familyKey}`,
6978
7542
  className: "flex items-center justify-center py-8",
6979
- children: /* @__PURE__ */ jsx24(
7543
+ children: /* @__PURE__ */ jsx26(
6980
7544
  "button",
6981
7545
  {
6982
7546
  onClick: () => onSelectScene?.(),
@@ -6988,12 +7552,12 @@ function GeneratorPanelShell({ core, slots }) {
6988
7552
  );
6989
7553
  }
6990
7554
  if (!sceneContext?.hasContract) {
6991
- return /* @__PURE__ */ jsx24(
7555
+ return /* @__PURE__ */ jsx26(
6992
7556
  "div",
6993
7557
  {
6994
7558
  "data-testid": `no-contract-placeholder-${identity.familyKey}`,
6995
7559
  className: "flex items-center justify-center py-8",
6996
- children: /* @__PURE__ */ jsx24(
7560
+ children: /* @__PURE__ */ jsx26(
6997
7561
  "button",
6998
7562
  {
6999
7563
  onClick: () => onOpenContract?.(),
@@ -7005,7 +7569,7 @@ function GeneratorPanelShell({ core, slots }) {
7005
7569
  );
7006
7570
  }
7007
7571
  if (features.bulkComposePlaceholders && isComposing) {
7008
- return /* @__PURE__ */ jsx24("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx24(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
7572
+ return /* @__PURE__ */ jsx26("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx26(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
7009
7573
  }
7010
7574
  const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
7011
7575
  if (activePlaceholders.length > 0) {
@@ -7016,18 +7580,18 @@ function GeneratorPanelShell({ core, slots }) {
7016
7580
  tracksByDbId.set(t.handle.id, t);
7017
7581
  }
7018
7582
  }
7019
- return /* @__PURE__ */ jsx24("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
7583
+ return /* @__PURE__ */ jsx26("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
7020
7584
  const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
7021
7585
  if (loadedTrack) {
7022
- return /* @__PURE__ */ jsx24(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
7586
+ return /* @__PURE__ */ jsx26(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
7023
7587
  }
7024
- return /* @__PURE__ */ jsx24(
7588
+ return /* @__PURE__ */ jsx26(
7025
7589
  "div",
7026
7590
  {
7027
7591
  "data-testid": "bulk-placeholder-track",
7028
7592
  className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
7029
7593
  style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
7030
- children: /* @__PURE__ */ jsx24(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
7594
+ children: /* @__PURE__ */ jsx26(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
7031
7595
  },
7032
7596
  ph.id
7033
7597
  );
@@ -7039,13 +7603,13 @@ function GeneratorPanelShell({ core, slots }) {
7039
7603
  supportsMeters,
7040
7604
  levels: supportsMeters ? trackLevels : void 0,
7041
7605
  handlers,
7042
- renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx24(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
7606
+ renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx26(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
7043
7607
  setGroupMute,
7044
7608
  setGroupSolo,
7045
7609
  deleteGroup
7046
7610
  };
7047
- return /* @__PURE__ */ jsxs18("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
7048
- features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx24(
7611
+ return /* @__PURE__ */ jsxs20("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
7612
+ features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx26(
7049
7613
  ImportTrackModal,
7050
7614
  {
7051
7615
  host,
@@ -7058,7 +7622,7 @@ function GeneratorPanelShell({ core, slots }) {
7058
7622
  testIdPrefix: `${identity.familyKey}-import`
7059
7623
  }
7060
7624
  ),
7061
- features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx24(
7625
+ features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx26(
7062
7626
  ImportTrackModal,
7063
7627
  {
7064
7628
  host,
@@ -7073,7 +7637,7 @@ function GeneratorPanelShell({ core, slots }) {
7073
7637
  }
7074
7638
  ),
7075
7639
  slots?.modals,
7076
- canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx24("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx24(
7640
+ canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx26("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx26(
7077
7641
  TransitionDesigner,
7078
7642
  {
7079
7643
  host,
@@ -7090,9 +7654,29 @@ function GeneratorPanelShell({ core, slots }) {
7090
7654
  testIdPrefix: `${identity.familyKey}-transition-designer`
7091
7655
  }
7092
7656
  ) }),
7093
- !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx24("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs18(Fragment6, { children: [
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(
7659
+ PanelMasterStrip,
7660
+ {
7661
+ bus: panelBus.bus,
7662
+ levels: panelBus.levels,
7663
+ availableFx: panelBus.availableFx,
7664
+ fxLoading: panelBus.fxLoading,
7665
+ soloedOut: anySolo && !panelBus.bus.soloed,
7666
+ fxPickerOpen: panelBus.fxPickerOpen,
7667
+ onToggleFxPicker: panelBus.setFxPickerOpen,
7668
+ onRefreshFx: panelBus.refreshFx,
7669
+ onVolumeChange: panelBus.onVolumeChange,
7670
+ onMuteToggle: panelBus.onMuteToggle,
7671
+ onSoloToggle: panelBus.onSoloToggle,
7672
+ onAddFx: panelBus.onAddFx,
7673
+ onRemoveFx: panelBus.onRemoveFx,
7674
+ onToggleFxEnabled: panelBus.onToggleFxEnabled,
7675
+ onShowFxEditor: panelBus.onShowFxEditor
7676
+ }
7677
+ ),
7094
7678
  slots?.beforeRows,
7095
- resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx24(
7679
+ resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx26(
7096
7680
  CrossfadeTrackRow,
7097
7681
  {
7098
7682
  accentColor: identity.transitionAccentColor,
@@ -7129,7 +7713,7 @@ function GeneratorPanelShell({ core, slots }) {
7129
7713
  },
7130
7714
  pair.groupId
7131
7715
  )),
7132
- resolvedFades.map((fade) => /* @__PURE__ */ jsx24(
7716
+ resolvedFades.map((fade) => /* @__PURE__ */ jsx26(
7133
7717
  FadeTrackRow,
7134
7718
  {
7135
7719
  accentColor: identity.transitionAccentColor,
@@ -7155,20 +7739,20 @@ function GeneratorPanelShell({ core, slots }) {
7155
7739
  fade.dbId
7156
7740
  )),
7157
7741
  (adapter.groupExtensions ?? []).flatMap(
7158
- (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ jsx24(React20.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
7742
+ (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ jsx26(React22.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
7159
7743
  ),
7160
7744
  tracks.map((track, index) => {
7161
7745
  if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
7162
7746
  return null;
7163
7747
  }
7164
- return /* @__PURE__ */ jsx24(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
7748
+ return /* @__PURE__ */ jsx26(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
7165
7749
  }),
7166
7750
  slots?.afterRows
7167
7751
  ] })),
7168
7752
  features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
7169
7753
  const hasAnyMidi = tracks.some((t) => t.hasMidi);
7170
7754
  const exportDisabled = isExportingMidi || !hasAnyMidi;
7171
- return /* @__PURE__ */ jsx24("div", { className: "pt-2", children: /* @__PURE__ */ jsx24(
7755
+ return /* @__PURE__ */ jsx26("div", { className: "pt-2", children: /* @__PURE__ */ jsx26(
7172
7756
  "button",
7173
7757
  {
7174
7758
  "data-testid": "export-midi-tracks-button",
@@ -7236,7 +7820,7 @@ function createSurgeSoundAdapter(host, overrides = {}) {
7236
7820
  }
7237
7821
 
7238
7822
  // src/constants/sdk-version.ts
7239
- var PLUGIN_SDK_VERSION = "2.35.0";
7823
+ var PLUGIN_SDK_VERSION = "2.39.0";
7240
7824
 
7241
7825
  // src/utils/format-concurrent-tracks.ts
7242
7826
  function formatConcurrentTracks(ctx) {
@@ -7411,6 +7995,7 @@ export {
7411
7995
  PLUGIN_SDK_VERSION,
7412
7996
  PX_PER_BEAT,
7413
7997
  PanSlider,
7998
+ PanelMasterStrip,
7414
7999
  PianoRollEditor,
7415
8000
  PluginError,
7416
8001
  RESIZE_HANDLE_PX,
@@ -7422,6 +8007,7 @@ export {
7422
8007
  TEXTURAL_ROLES,
7423
8008
  TRANSITION_DESIGNER_DRAFT_KEY,
7424
8009
  TrackDrawer,
8010
+ TrackExternalFxSection,
7425
8011
  TrackMeterStrip,
7426
8012
  TrackRow,
7427
8013
  TransitionDesigner,
@@ -7475,8 +8061,10 @@ export {
7475
8061
  transposeNotes,
7476
8062
  useAnySolo,
7477
8063
  useGeneratorPanelCore,
8064
+ usePanelBus,
7478
8065
  useSceneState,
7479
8066
  useSoundHistory,
8067
+ useTrackExternalFx,
7480
8068
  useTrackLevel,
7481
8069
  useTrackLevels,
7482
8070
  useTrackMeter,