@signalsandsorcery/plugin-sdk 2.36.2 → 2.40.1

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
@@ -69,7 +69,7 @@ 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 useState4, useMemo as useMemo3 } from "react";
72
+ import { useState as useState5, useMemo as useMemo3 } from "react";
73
73
 
74
74
  // src/constants/fx-presets.ts
75
75
  var EQ_PRESETS = {
@@ -1051,14 +1051,132 @@ function TrackExternalFxSection({
1051
1051
  );
1052
1052
  }
1053
1053
 
1054
- // src/components/TrackDrawer.tsx
1054
+ // src/components/TrackFreezeSection.tsx
1055
1055
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1056
+ var STALE_LABELS = {
1057
+ midi: "MIDI changed",
1058
+ preset: "preset changed",
1059
+ instrument: "instrument changed",
1060
+ "external-fx": "FX changed",
1061
+ role: "role changed"
1062
+ };
1063
+ function TrackFreezeSection({
1064
+ state,
1065
+ busy,
1066
+ error,
1067
+ onFreeze,
1068
+ onUnfreeze
1069
+ }) {
1070
+ const frozen = state?.frozen === true;
1071
+ const stale = frozen && state?.stale === true;
1072
+ const missing = state?.missingDeps ?? [];
1073
+ const unfreezeBlocked = frozen && missing.length > 0;
1074
+ const statusLine = !state ? "Reading freeze state\u2026" : !frozen ? state.latentFreshFreeze ? "Live \u2014 a fresh stem is cached, freezing is instant." : "Live \u2014 playing through the instrument and FX chain." : stale ? `\u26A0\uFE0F\u2744 Frozen (stale) \u2014 ${state.staleReasons.map((r) => STALE_LABELS[r] ?? r).join(", ")} since the stem rendered. Playback keeps the old stem until you re-freeze.` : "\u2744 Frozen \u2014 playing the rendered stem. Mixer (volume \xB7 pan \xB7 mute \xB7 solo) stays live.";
1075
+ const primaryLabel = busy === "freeze" ? "Rendering stem\u2026" : busy === "unfreeze" ? "Restoring chain\u2026" : !frozen ? "\u2744 Freeze track" : stale ? "\u2744 Re-freeze (render new stem)" : "Unfreeze track";
1076
+ const primaryAction = !frozen || stale ? onFreeze : onUnfreeze;
1077
+ const primaryDisabled = busy !== null || !state || frozen && !stale && unfreezeBlocked;
1078
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", "data-testid": "sdk-freeze-section", children: [
1079
+ /* @__PURE__ */ jsx4("p", { className: "text-[11px] text-sas-muted/80 leading-snug", "data-testid": "sdk-freeze-status", children: statusLine }),
1080
+ frozen && missing.length > 0 && /* @__PURE__ */ jsxs4(
1081
+ "div",
1082
+ {
1083
+ className: "text-[11px] leading-snug rounded-sm border border-sas-border bg-sas-panel-alt px-2 py-1.5 text-sas-muted",
1084
+ "data-testid": "sdk-freeze-missing-deps",
1085
+ children: [
1086
+ "Missing on this machine:",
1087
+ " ",
1088
+ /* @__PURE__ */ jsx4("span", { className: "text-sas-accent", children: missing.map((d) => d.name).join(", ") }),
1089
+ ". ",
1090
+ "Install (or rescan plugins if blacklisted) to unfreeze \u2014 the frozen sound keeps playing meanwhile."
1091
+ ]
1092
+ }
1093
+ ),
1094
+ /* @__PURE__ */ jsx4(
1095
+ "button",
1096
+ {
1097
+ type: "button",
1098
+ "data-testid": "sdk-freeze-primary",
1099
+ disabled: primaryDisabled,
1100
+ onClick: primaryAction,
1101
+ className: `w-full py-2 text-xs font-medium rounded-sm border transition-colors ${primaryDisabled ? "border-sas-border text-sas-muted/40 cursor-not-allowed" : "border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40"}`,
1102
+ title: !frozen ? "Render this track to audio and disable its plugins (mix controls stay live)" : stale ? "Sound inputs changed \u2014 render a fresh stem" : unfreezeBlocked ? "Unfreeze needs the missing plugins installed" : "Remove the stem and re-enable the instrument + FX chain",
1103
+ children: primaryLabel
1104
+ }
1105
+ ),
1106
+ frozen && stale && !unfreezeBlocked && /* @__PURE__ */ jsx4(
1107
+ "button",
1108
+ {
1109
+ type: "button",
1110
+ "data-testid": "sdk-freeze-unfreeze-secondary",
1111
+ disabled: busy !== null,
1112
+ onClick: onUnfreeze,
1113
+ className: "w-full py-1.5 text-[11px] rounded-sm border border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent transition-colors",
1114
+ children: "Unfreeze instead (back to live editing)"
1115
+ }
1116
+ ),
1117
+ error && /* @__PURE__ */ jsx4("p", { className: "text-[11px] text-red-400 leading-snug", "data-testid": "sdk-freeze-error", children: error }),
1118
+ /* @__PURE__ */ jsx4("p", { className: "text-[10px] text-sas-muted/50 leading-snug", children: "Frozen tracks survive missing plugins and travel inside project backups. The stem is kept after unfreezing, so re-freezing an unchanged track is instant." })
1119
+ ] });
1120
+ }
1121
+
1122
+ // src/hooks/useTrackFreeze.ts
1123
+ import { useState as useState4, useEffect as useEffect2, useCallback as useCallback3 } from "react";
1124
+ function useTrackFreeze(host, trackId) {
1125
+ const enabled = !!host && typeof host.getTrackFreezeState === "function";
1126
+ const [state, setState] = useState4(null);
1127
+ const [busy, setBusy] = useState4(null);
1128
+ const [error, setError] = useState4(null);
1129
+ const refresh = useCallback3(async () => {
1130
+ if (!enabled || !host?.getTrackFreezeState) return;
1131
+ try {
1132
+ setState(await host.getTrackFreezeState(trackId));
1133
+ } catch {
1134
+ setState(null);
1135
+ }
1136
+ }, [enabled, host, trackId]);
1137
+ useEffect2(() => {
1138
+ void refresh();
1139
+ }, [refresh]);
1140
+ useEffect2(() => {
1141
+ if (!enabled) return void 0;
1142
+ const onFreezeChanged = () => {
1143
+ void refresh();
1144
+ };
1145
+ window.addEventListener("sas:freeze-changed", onFreezeChanged);
1146
+ return () => window.removeEventListener("sas:freeze-changed", onFreezeChanged);
1147
+ }, [enabled, refresh]);
1148
+ const run = useCallback3(
1149
+ async (action) => {
1150
+ if (!host) return;
1151
+ const method = action === "freeze" ? host.freezeTrack : host.unfreezeTrack;
1152
+ if (typeof method !== "function") return;
1153
+ setBusy(action);
1154
+ setError(null);
1155
+ try {
1156
+ setState(await method.call(host, trackId));
1157
+ } catch (e) {
1158
+ setError(e instanceof Error ? e.message : String(e));
1159
+ void refresh();
1160
+ } finally {
1161
+ setBusy(null);
1162
+ }
1163
+ },
1164
+ [host, trackId, refresh]
1165
+ );
1166
+ const freeze = useCallback3(() => run("freeze"), [run]);
1167
+ const unfreeze = useCallback3(() => run("unfreeze"), [run]);
1168
+ return { enabled, state, busy, error, refresh, freeze, unfreeze };
1169
+ }
1170
+
1171
+ // src/components/TrackDrawer.tsx
1172
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1056
1173
  var TAB_LABELS = {
1057
1174
  fx: "FX",
1058
1175
  pick: "Pick",
1059
1176
  history: "History",
1060
1177
  import: "Import",
1061
- edit: "Edit"
1178
+ edit: "Edit",
1179
+ freeze: "\u2744 Freeze"
1062
1180
  };
1063
1181
  function TrackDrawer({
1064
1182
  activeTab,
@@ -1070,6 +1188,7 @@ function TrackDrawer({
1070
1188
  onFxDryWetChange,
1071
1189
  fxDisabled = false,
1072
1190
  externalFxHost,
1191
+ freeze,
1073
1192
  instruments = [],
1074
1193
  currentPluginId = null,
1075
1194
  isLoading = false,
@@ -1085,6 +1204,7 @@ function TrackDrawer({
1085
1204
  onToggleFavorite,
1086
1205
  onImportSound,
1087
1206
  importSoundLabel,
1207
+ linkedSoundHint,
1088
1208
  editNotes,
1089
1209
  onNotesChange,
1090
1210
  editBars,
@@ -1092,12 +1212,15 @@ function TrackDrawer({
1092
1212
  editSnap,
1093
1213
  onAuditionNote
1094
1214
  }) {
1095
- const [search, setSearch] = useState4("");
1215
+ const [search, setSearch] = useState5("");
1096
1216
  const fxEnabled = !!onFxToggle;
1097
1217
  const pickEnabled = !!onSelect;
1098
1218
  const historyEnabled = !!onRestoreSound;
1099
1219
  const importEnabled = !!onImportSound;
1100
1220
  const editEnabled = !!onNotesChange;
1221
+ const internalFreeze = useTrackFreeze(freeze ? void 0 : externalFxHost, trackId);
1222
+ const fz = freeze ?? internalFreeze;
1223
+ const freezeEnabled = fz.enabled;
1101
1224
  const enabledTabs = useMemo3(() => {
1102
1225
  const tabs = [];
1103
1226
  if (fxEnabled) tabs.push("fx");
@@ -1105,8 +1228,9 @@ function TrackDrawer({
1105
1228
  if (historyEnabled) tabs.push("history");
1106
1229
  if (importEnabled) tabs.push("import");
1107
1230
  if (editEnabled) tabs.push("edit");
1231
+ if (freezeEnabled) tabs.push("freeze");
1108
1232
  return tabs;
1109
- }, [fxEnabled, pickEnabled, historyEnabled, importEnabled, editEnabled]);
1233
+ }, [fxEnabled, pickEnabled, historyEnabled, importEnabled, editEnabled, freezeEnabled]);
1110
1234
  const SURGE_XT_DEFAULT_ID = "Surge XT";
1111
1235
  const filtered = useMemo3(() => {
1112
1236
  let all = instruments.filter((i) => i.name !== "Surge XT");
@@ -1128,12 +1252,12 @@ function TrackDrawer({
1128
1252
  const history = soundHistory ?? [];
1129
1253
  const effectiveTab = enabledTabs.includes(activeTab) ? activeTab : enabledTabs[0] ?? "fx";
1130
1254
  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"}`;
1131
- const strip = enabledTabs.length > 1 ? /* @__PURE__ */ jsx4(
1255
+ const strip = enabledTabs.length > 1 ? /* @__PURE__ */ jsx5(
1132
1256
  "div",
1133
1257
  {
1134
1258
  className: "flex items-center gap-1 border-b border-sas-border pb-1",
1135
1259
  "data-testid": "sdk-drawer-tabs",
1136
- children: enabledTabs.map((tab) => /* @__PURE__ */ jsx4(
1260
+ children: enabledTabs.map((tab) => /* @__PURE__ */ jsx5(
1137
1261
  "button",
1138
1262
  {
1139
1263
  type: "button",
@@ -1147,21 +1271,60 @@ function TrackDrawer({
1147
1271
  }
1148
1272
  ) : null;
1149
1273
  const currentSound = soundHistoryCursor >= 0 && soundHistoryCursor < history.length ? history[soundHistoryCursor].label : null;
1150
- const header = strip || currentSound ? /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1", "data-testid": "sdk-drawer-header", children: [
1274
+ const header = strip || currentSound || linkedSoundHint ? /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-1", "data-testid": "sdk-drawer-header", children: [
1151
1275
  strip,
1152
- currentSound && /* @__PURE__ */ jsx4(
1276
+ currentSound && /* @__PURE__ */ jsx5(
1153
1277
  "span",
1154
1278
  {
1155
1279
  className: "text-[10px] text-sas-muted/60 truncate px-0.5",
1156
1280
  title: currentSound,
1157
1281
  children: currentSound
1158
1282
  }
1283
+ ),
1284
+ linkedSoundHint && /* @__PURE__ */ jsx5(
1285
+ "span",
1286
+ {
1287
+ "data-testid": "sdk-drawer-linked-hint",
1288
+ className: "text-[10px] text-sas-accent/80 px-0.5",
1289
+ children: linkedSoundHint
1290
+ }
1159
1291
  )
1160
1292
  ] }) : null;
1293
+ if (effectiveTab === "freeze") {
1294
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-freeze", children: [
1295
+ header,
1296
+ /* @__PURE__ */ jsx5(
1297
+ TrackFreezeSection,
1298
+ {
1299
+ state: fz.state,
1300
+ busy: fz.busy,
1301
+ error: fz.error,
1302
+ onFreeze: () => void fz.freeze(),
1303
+ onUnfreeze: () => void fz.unfreeze()
1304
+ }
1305
+ )
1306
+ ] });
1307
+ }
1308
+ if (fz.state?.frozen === true) {
1309
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-frozen-lock", children: [
1310
+ header,
1311
+ /* @__PURE__ */ jsx5("div", { className: "text-[11px] text-sas-muted/80 leading-snug rounded-sm border border-sas-border bg-sas-panel-alt px-2 py-2", children: "\u2744 This track is frozen \u2014 it plays its rendered stem, so sound editing here would be inaudible. Volume, pan, mute and solo stay live." }),
1312
+ /* @__PURE__ */ jsx5(
1313
+ "button",
1314
+ {
1315
+ type: "button",
1316
+ "data-testid": "sdk-drawer-frozen-goto",
1317
+ onClick: () => onTabChange?.("freeze"),
1318
+ className: "w-full py-2 text-xs font-medium rounded-sm border border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40 transition-colors",
1319
+ children: "Open the Freeze tab to unfreeze"
1320
+ }
1321
+ )
1322
+ ] });
1323
+ }
1161
1324
  if (effectiveTab === "edit") {
1162
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-edit", children: [
1325
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-edit", children: [
1163
1326
  header,
1164
- /* @__PURE__ */ jsx4(
1327
+ /* @__PURE__ */ jsx5(
1165
1328
  PianoRollEditor,
1166
1329
  {
1167
1330
  notes: editNotes ?? [],
@@ -1176,9 +1339,9 @@ function TrackDrawer({
1176
1339
  ] });
1177
1340
  }
1178
1341
  if (effectiveTab === "fx") {
1179
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-fx", children: [
1342
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-fx", children: [
1180
1343
  header,
1181
- /* @__PURE__ */ jsx4(
1344
+ /* @__PURE__ */ jsx5(
1182
1345
  FxToggleBar,
1183
1346
  {
1184
1347
  trackId,
@@ -1189,20 +1352,20 @@ function TrackDrawer({
1189
1352
  disabled: fxDisabled
1190
1353
  }
1191
1354
  ),
1192
- externalFxHost && /* @__PURE__ */ jsx4(TrackExternalFxSection, { host: externalFxHost, trackId, disabled: fxDisabled })
1355
+ externalFxHost && /* @__PURE__ */ jsx5(TrackExternalFxSection, { host: externalFxHost, trackId, disabled: fxDisabled })
1193
1356
  ] });
1194
1357
  }
1195
1358
  if (effectiveTab === "import") {
1196
1359
  const soundNoun = /preset/i.test(importSoundLabel ?? "") ? "preset" : /sample/i.test(importSoundLabel ?? "") ? "sample" : "sound";
1197
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-import", children: [
1360
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", "data-testid": "sdk-drawer-import", children: [
1198
1361
  header,
1199
- /* @__PURE__ */ jsxs4("p", { className: "text-[11px] text-sas-muted/70 leading-snug", children: [
1362
+ /* @__PURE__ */ jsxs5("p", { className: "text-[11px] text-sas-muted/70 leading-snug", children: [
1200
1363
  "Copy the sound from a matching track in another scene \u2014 your MIDI stays, only the",
1201
1364
  " ",
1202
1365
  soundNoun,
1203
1366
  " changes."
1204
1367
  ] }),
1205
- /* @__PURE__ */ jsxs4(
1368
+ /* @__PURE__ */ jsxs5(
1206
1369
  "button",
1207
1370
  {
1208
1371
  type: "button",
@@ -1220,16 +1383,16 @@ function TrackDrawer({
1220
1383
  }
1221
1384
  if (effectiveTab === "history") {
1222
1385
  const order = history.map((_, i) => i).reverse();
1223
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
1386
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1224
1387
  header,
1225
- history.length === 0 ? /* @__PURE__ */ jsx4(
1388
+ history.length === 0 ? /* @__PURE__ */ jsx5(
1226
1389
  "div",
1227
1390
  {
1228
1391
  className: "text-xs text-sas-muted/60 text-center py-3",
1229
1392
  "data-testid": "sdk-history-empty",
1230
1393
  children: "No sounds yet \u2014 shuffle to build history."
1231
1394
  }
1232
- ) : /* @__PURE__ */ jsx4(
1395
+ ) : /* @__PURE__ */ jsx5(
1233
1396
  "ul",
1234
1397
  {
1235
1398
  className: "flex flex-col gap-1 max-h-[160px] overflow-y-auto",
@@ -1237,8 +1400,8 @@ function TrackDrawer({
1237
1400
  children: order.map((i) => {
1238
1401
  const entry = history[i];
1239
1402
  const isCurrent = i === soundHistoryCursor;
1240
- return /* @__PURE__ */ jsxs4("li", { className: "flex items-center gap-1", children: [
1241
- /* @__PURE__ */ jsxs4(
1403
+ return /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-1", children: [
1404
+ /* @__PURE__ */ jsxs5(
1242
1405
  "button",
1243
1406
  {
1244
1407
  type: "button",
@@ -1248,12 +1411,12 @@ function TrackDrawer({
1248
1411
  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"}`,
1249
1412
  title: isCurrent ? "Current sound" : `Restore: ${entry.label}`,
1250
1413
  children: [
1251
- /* @__PURE__ */ jsx4("span", { className: "truncate", children: entry.label }),
1252
- /* @__PURE__ */ jsx4("span", { className: "text-[10px] text-sas-muted/60 flex-shrink-0 ml-2", children: isCurrent ? "\u25CF current" : "restore" })
1414
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: entry.label }),
1415
+ /* @__PURE__ */ jsx5("span", { className: "text-[10px] text-sas-muted/60 flex-shrink-0 ml-2", children: isCurrent ? "\u25CF current" : "restore" })
1253
1416
  ]
1254
1417
  }
1255
1418
  ),
1256
- onToggleFavorite && /* @__PURE__ */ jsx4(
1419
+ onToggleFavorite && /* @__PURE__ */ jsx5(
1257
1420
  "button",
1258
1421
  {
1259
1422
  type: "button",
@@ -1271,10 +1434,10 @@ function TrackDrawer({
1271
1434
  ] });
1272
1435
  }
1273
1436
  if (effectiveTab === "pick" && editorStage) {
1274
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
1437
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1275
1438
  header,
1276
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
1277
- /* @__PURE__ */ jsx4(
1439
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
1440
+ /* @__PURE__ */ jsx5(
1278
1441
  "button",
1279
1442
  {
1280
1443
  onClick: () => onBackToInstruments?.(),
@@ -1282,9 +1445,9 @@ function TrackDrawer({
1282
1445
  children: "\u2190 Back"
1283
1446
  }
1284
1447
  ),
1285
- /* @__PURE__ */ jsx4("span", { className: "text-xs text-sas-muted font-medium truncate flex-1", children: selectedInstrumentName ?? "Plugin" })
1448
+ /* @__PURE__ */ jsx5("span", { className: "text-xs text-sas-muted font-medium truncate flex-1", children: selectedInstrumentName ?? "Plugin" })
1286
1449
  ] }),
1287
- /* @__PURE__ */ jsx4(
1450
+ /* @__PURE__ */ jsx5(
1288
1451
  "button",
1289
1452
  {
1290
1453
  onClick: () => onShowEditor?.(),
@@ -1296,10 +1459,10 @@ function TrackDrawer({
1296
1459
  }
1297
1460
  const isDefaultSelected = currentPluginId === null;
1298
1461
  const isSelected = (pluginId) => pluginId === currentPluginId;
1299
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
1462
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1300
1463
  header,
1301
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
1302
- /* @__PURE__ */ jsx4(
1464
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
1465
+ /* @__PURE__ */ jsx5(
1303
1466
  "input",
1304
1467
  {
1305
1468
  type: "text",
@@ -1309,7 +1472,7 @@ function TrackDrawer({
1309
1472
  className: "sas-input flex-1 px-2 py-1 text-xs"
1310
1473
  }
1311
1474
  ),
1312
- /* @__PURE__ */ jsx4(
1475
+ /* @__PURE__ */ jsx5(
1313
1476
  "button",
1314
1477
  {
1315
1478
  onClick: () => onRefresh?.(),
@@ -1320,43 +1483,43 @@ function TrackDrawer({
1320
1483
  }
1321
1484
  )
1322
1485
  ] }),
1323
- 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: [
1324
- /* @__PURE__ */ jsxs4(
1486
+ isLoading && instruments.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
1487
+ /* @__PURE__ */ jsxs5(
1325
1488
  "button",
1326
1489
  {
1327
1490
  onClick: () => onSelect?.(SURGE_XT_DEFAULT_ID),
1328
1491
  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"}`,
1329
1492
  title: "Surge XT \u2014 Default instrument",
1330
1493
  children: [
1331
- /* @__PURE__ */ jsxs4("span", { className: "text-xs font-medium truncate w-full", children: [
1494
+ /* @__PURE__ */ jsxs5("span", { className: "text-xs font-medium truncate w-full", children: [
1332
1495
  isDefaultSelected && "\u2713 ",
1333
1496
  "Surge XT"
1334
1497
  ] }),
1335
- /* @__PURE__ */ jsx4("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: "Default" })
1498
+ /* @__PURE__ */ jsx5("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: "Default" })
1336
1499
  ]
1337
1500
  },
1338
1501
  "__surge-xt-default__"
1339
1502
  ),
1340
1503
  filtered.map((inst) => {
1341
1504
  const selected = isSelected(inst.pluginId);
1342
- return /* @__PURE__ */ jsxs4(
1505
+ return /* @__PURE__ */ jsxs5(
1343
1506
  "button",
1344
1507
  {
1345
1508
  onClick: () => onSelect?.(inst.pluginId),
1346
1509
  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"}`,
1347
1510
  title: `${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? " \u2014 MISSING" : ""}`,
1348
1511
  children: [
1349
- /* @__PURE__ */ jsxs4("span", { className: "text-xs font-medium truncate w-full", children: [
1512
+ /* @__PURE__ */ jsxs5("span", { className: "text-xs font-medium truncate w-full", children: [
1350
1513
  selected && "\u2713 ",
1351
1514
  inst.name
1352
1515
  ] }),
1353
- /* @__PURE__ */ jsx4("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: inst.manufacturer || inst.type.toUpperCase() })
1516
+ /* @__PURE__ */ jsx5("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: inst.manufacturer || inst.type.toUpperCase() })
1354
1517
  ]
1355
1518
  },
1356
1519
  inst.pluginId
1357
1520
  );
1358
1521
  }),
1359
- 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" })
1522
+ filtered.length === 0 && /* @__PURE__ */ jsx5("div", { className: "col-span-2 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No other plugins found" })
1360
1523
  ] })
1361
1524
  ] });
1362
1525
  }
@@ -1365,9 +1528,9 @@ function TrackDrawer({
1365
1528
  import { useRef as useRef3 } from "react";
1366
1529
 
1367
1530
  // src/components/Modal.tsx
1368
- import { useEffect as useEffect2 } from "react";
1531
+ import { useEffect as useEffect3 } from "react";
1369
1532
  import { createPortal } from "react-dom";
1370
- import { jsx as jsx5 } from "react/jsx-runtime";
1533
+ import { jsx as jsx6 } from "react/jsx-runtime";
1371
1534
  function Modal({
1372
1535
  open,
1373
1536
  onClose,
@@ -1377,7 +1540,7 @@ function Modal({
1377
1540
  closeOnEscape = true,
1378
1541
  initialFocusRef
1379
1542
  }) {
1380
- useEffect2(() => {
1543
+ useEffect3(() => {
1381
1544
  if (!open) return void 0;
1382
1545
  const onKey = (e) => {
1383
1546
  if (closeOnEscape && e.key === "Escape") {
@@ -1391,7 +1554,7 @@ function Modal({
1391
1554
  }, [open, onClose, closeOnEscape, initialFocusRef]);
1392
1555
  if (!open) return null;
1393
1556
  return createPortal(
1394
- /* @__PURE__ */ jsx5(
1557
+ /* @__PURE__ */ jsx6(
1395
1558
  "div",
1396
1559
  {
1397
1560
  className: "fixed inset-0 z-[1000] flex items-center justify-center bg-black/60",
@@ -1405,7 +1568,7 @@ function Modal({
1405
1568
  }
1406
1569
 
1407
1570
  // src/components/ConfirmDialog.tsx
1408
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1571
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1409
1572
  function ConfirmDialog({
1410
1573
  open,
1411
1574
  title,
@@ -1418,7 +1581,7 @@ function ConfirmDialog({
1418
1581
  testIdPrefix = "confirm-dialog"
1419
1582
  }) {
1420
1583
  const cancelRef = useRef3(null);
1421
- return /* @__PURE__ */ jsx6(Modal, { open, onClose: onCancel, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs5(
1584
+ return /* @__PURE__ */ jsx7(Modal, { open, onClose: onCancel, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs6(
1422
1585
  "div",
1423
1586
  {
1424
1587
  className: "w-[360px] max-w-[90vw] flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
@@ -1428,8 +1591,8 @@ function ConfirmDialog({
1428
1591
  "aria-label": title,
1429
1592
  "data-testid": `${testIdPrefix}-modal`,
1430
1593
  children: [
1431
- /* @__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 }) }),
1432
- /* @__PURE__ */ jsx6(
1594
+ /* @__PURE__ */ jsx7("div", { className: "px-4 py-3 border-b border-sas-border", children: /* @__PURE__ */ jsx7("span", { className: "text-sm font-medium text-sas-text", "data-testid": `${testIdPrefix}-title`, children: title }) }),
1595
+ /* @__PURE__ */ jsx7(
1433
1596
  "div",
1434
1597
  {
1435
1598
  className: "px-4 py-3 text-xs text-sas-muted leading-relaxed break-words",
@@ -1437,8 +1600,8 @@ function ConfirmDialog({
1437
1600
  children: message
1438
1601
  }
1439
1602
  ),
1440
- /* @__PURE__ */ jsxs5("div", { className: "flex justify-end gap-2 px-4 py-3 border-t border-sas-border", children: [
1441
- /* @__PURE__ */ jsx6(
1603
+ /* @__PURE__ */ jsxs6("div", { className: "flex justify-end gap-2 px-4 py-3 border-t border-sas-border", children: [
1604
+ /* @__PURE__ */ jsx7(
1442
1605
  "button",
1443
1606
  {
1444
1607
  ref: cancelRef,
@@ -1449,7 +1612,7 @@ function ConfirmDialog({
1449
1612
  children: cancelLabel
1450
1613
  }
1451
1614
  ),
1452
- /* @__PURE__ */ jsx6(
1615
+ /* @__PURE__ */ jsx7(
1453
1616
  "button",
1454
1617
  {
1455
1618
  type: "button",
@@ -1466,7 +1629,7 @@ function ConfirmDialog({
1466
1629
  }
1467
1630
 
1468
1631
  // src/components/LevelMeter.tsx
1469
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1632
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1470
1633
  var COLOR_GREEN = "#2BD576";
1471
1634
  var COLOR_ORANGE = "#F5A623";
1472
1635
  var COLOR_RED = "#FF4D5E";
@@ -1494,7 +1657,7 @@ var LevelMeter = ({
1494
1657
  const widthPct = active ? dbToPct(peakDb) : 0;
1495
1658
  const showPeak = peakHoldDb != null && active && peakHoldDb > -60;
1496
1659
  const peakHoldPct = showPeak ? dbToPct(peakHoldDb) : 0;
1497
- return /* @__PURE__ */ jsxs6(
1660
+ return /* @__PURE__ */ jsxs7(
1498
1661
  "div",
1499
1662
  {
1500
1663
  className: `sas-level-meter ${className ?? ""}`,
@@ -1505,7 +1668,7 @@ var LevelMeter = ({
1505
1668
  gap: compact ? 0 : 6
1506
1669
  },
1507
1670
  children: [
1508
- /* @__PURE__ */ jsxs6(
1671
+ /* @__PURE__ */ jsxs7(
1509
1672
  "div",
1510
1673
  {
1511
1674
  style: {
@@ -1519,8 +1682,8 @@ var LevelMeter = ({
1519
1682
  minWidth: compact ? 0 : 60
1520
1683
  },
1521
1684
  children: [
1522
- /* @__PURE__ */ jsx7("div", { style: { position: "absolute", inset: 0, background: METER_GRADIENT } }),
1523
- /* @__PURE__ */ jsx7(
1685
+ /* @__PURE__ */ jsx8("div", { style: { position: "absolute", inset: 0, background: METER_GRADIENT } }),
1686
+ /* @__PURE__ */ jsx8(
1524
1687
  "div",
1525
1688
  {
1526
1689
  style: {
@@ -1534,7 +1697,7 @@ var LevelMeter = ({
1534
1697
  }
1535
1698
  }
1536
1699
  ),
1537
- /* @__PURE__ */ jsx7(
1700
+ /* @__PURE__ */ jsx8(
1538
1701
  "div",
1539
1702
  {
1540
1703
  "data-testid": `${id}-segments`,
@@ -1547,7 +1710,7 @@ var LevelMeter = ({
1547
1710
  }
1548
1711
  }
1549
1712
  ),
1550
- showPeak && /* @__PURE__ */ jsx7(
1713
+ showPeak && /* @__PURE__ */ jsx8(
1551
1714
  "div",
1552
1715
  {
1553
1716
  "data-testid": `${id}-peak`,
@@ -1568,7 +1731,7 @@ var LevelMeter = ({
1568
1731
  ]
1569
1732
  }
1570
1733
  ),
1571
- !compact && /* @__PURE__ */ jsx7(
1734
+ !compact && /* @__PURE__ */ jsx8(
1572
1735
  "span",
1573
1736
  {
1574
1737
  style: {
@@ -1581,7 +1744,7 @@ var LevelMeter = ({
1581
1744
  children: active && peakDb > -120 ? `${peakDb.toFixed(0)} dB` : "\u2014"
1582
1745
  }
1583
1746
  ),
1584
- clipped && /* @__PURE__ */ jsx7(
1747
+ clipped && /* @__PURE__ */ jsx8(
1585
1748
  "span",
1586
1749
  {
1587
1750
  "data-testid": `${id}-clip`,
@@ -1606,7 +1769,7 @@ var LevelMeter = ({
1606
1769
  };
1607
1770
 
1608
1771
  // src/hooks/useTrackLevels.ts
1609
- import { useEffect as useEffect3, useRef as useRef4, useState as useState5 } from "react";
1772
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState6 } from "react";
1610
1773
  var meterDiagRLast = /* @__PURE__ */ new Map();
1611
1774
  var POLL_INTERVAL_MS = 33;
1612
1775
  var HIDDEN_RECHECK_MS = 250;
@@ -1631,7 +1794,7 @@ function useTrackLevels(host, enabled = true) {
1631
1794
  }
1632
1795
  };
1633
1796
  }
1634
- useEffect3(() => {
1797
+ useEffect4(() => {
1635
1798
  const notify = () => {
1636
1799
  listenersRef.current.forEach((l) => l());
1637
1800
  };
@@ -1701,8 +1864,8 @@ function sameLevel(a, b) {
1701
1864
  return a.peakDb === b.peakDb && a.clipped === b.clipped;
1702
1865
  }
1703
1866
  function useTrackLevel(handle, trackId) {
1704
- const [level, setLevel] = useState5(null);
1705
- useEffect3(() => {
1867
+ const [level, setLevel] = useState6(null);
1868
+ useEffect4(() => {
1706
1869
  if (!handle) {
1707
1870
  setLevel(null);
1708
1871
  return;
@@ -1726,11 +1889,11 @@ function sameMeter(a, b) {
1726
1889
  return a.active === b.active && a.clipped === b.clipped && a.peakDb === b.peakDb && Math.round(a.peakHoldDb * 2) === Math.round(b.peakHoldDb * 2);
1727
1890
  }
1728
1891
  function useTrackMeter(handle, trackId) {
1729
- const [view, setView] = useState5(IDLE_METER_VIEW);
1892
+ const [view, setView] = useState6(IDLE_METER_VIEW);
1730
1893
  const heldDbRef = useRef4(METER_FLOOR_DB);
1731
1894
  const heldAtRef = useRef4(0);
1732
1895
  const lastTickRef = useRef4(0);
1733
- useEffect3(() => {
1896
+ useEffect4(() => {
1734
1897
  if (!handle) {
1735
1898
  heldDbRef.current = METER_FLOOR_DB;
1736
1899
  lastTickRef.current = 0;
@@ -1773,8 +1936,8 @@ function useTrackMeter(handle, trackId) {
1773
1936
  return view;
1774
1937
  }
1775
1938
  function useTransportPlaying(host) {
1776
- const [playing, setPlaying] = useState5(false);
1777
- useEffect3(() => {
1939
+ const [playing, setPlaying] = useState6(false);
1940
+ useEffect4(() => {
1778
1941
  if (!host) {
1779
1942
  setPlaying(false);
1780
1943
  return;
@@ -1802,7 +1965,7 @@ function useTransportPlaying(host) {
1802
1965
  }
1803
1966
 
1804
1967
  // src/components/TrackMeterStrip.tsx
1805
- import { jsx as jsx8 } from "react/jsx-runtime";
1968
+ import { jsx as jsx9 } from "react/jsx-runtime";
1806
1969
  var TrackMeterStrip = ({
1807
1970
  levels,
1808
1971
  trackId,
@@ -1810,12 +1973,12 @@ var TrackMeterStrip = ({
1810
1973
  className
1811
1974
  }) => {
1812
1975
  const meter = useTrackMeter(levels, trackId);
1813
- return /* @__PURE__ */ jsx8(
1976
+ return /* @__PURE__ */ jsx9(
1814
1977
  "div",
1815
1978
  {
1816
1979
  "data-testid": "sdk-track-meter",
1817
1980
  className: `w-full px-2 py-1 bg-sas-panel-alt border border-t-0 border-sas-border ${roundBottom ? "rounded-b-sm" : ""} ${className ?? ""}`,
1818
- children: /* @__PURE__ */ jsx8(
1981
+ children: /* @__PURE__ */ jsx9(
1819
1982
  LevelMeter,
1820
1983
  {
1821
1984
  compact: true,
@@ -1831,7 +1994,7 @@ var TrackMeterStrip = ({
1831
1994
  };
1832
1995
 
1833
1996
  // src/components/VolumeSlider.tsx
1834
- import { useCallback as useCallback3, useState as useState6, useRef as useRef5, useEffect as useEffect4 } from "react";
1997
+ import { useCallback as useCallback4, useState as useState7, useRef as useRef5, useEffect as useEffect5 } from "react";
1835
1998
 
1836
1999
  // src/utils/volume-conversion.ts
1837
2000
  var SLIDER_UNITY = 0.75;
@@ -1853,7 +2016,7 @@ function dbToSlider(db) {
1853
2016
  }
1854
2017
 
1855
2018
  // src/components/VolumeSlider.tsx
1856
- import { jsx as jsx9 } from "react/jsx-runtime";
2019
+ import { jsx as jsx10 } from "react/jsx-runtime";
1857
2020
  function formatDb(value) {
1858
2021
  const db = sliderToDb(value);
1859
2022
  if (db <= -60) return "-\u221E dB";
@@ -1863,10 +2026,10 @@ function formatDb(value) {
1863
2026
  function useDebouncedCallback(callback, delay) {
1864
2027
  const timeoutRef = useRef5(null);
1865
2028
  const callbackRef = useRef5(callback);
1866
- useEffect4(() => {
2029
+ useEffect5(() => {
1867
2030
  callbackRef.current = callback;
1868
2031
  }, [callback]);
1869
- const debouncedCallback = useCallback3(
2032
+ const debouncedCallback = useCallback4(
1870
2033
  (...args) => {
1871
2034
  if (timeoutRef.current) {
1872
2035
  clearTimeout(timeoutRef.current);
@@ -1877,7 +2040,7 @@ function useDebouncedCallback(callback, delay) {
1877
2040
  },
1878
2041
  [delay]
1879
2042
  );
1880
- useEffect4(() => {
2043
+ useEffect5(() => {
1881
2044
  return () => {
1882
2045
  if (timeoutRef.current) {
1883
2046
  clearTimeout(timeoutRef.current);
@@ -1892,15 +2055,15 @@ var VolumeSlider = ({
1892
2055
  disabled = false,
1893
2056
  className = ""
1894
2057
  }) => {
1895
- const [localValue, setLocalValue] = useState6(value);
1896
- const [isDragging, setIsDragging] = useState6(false);
1897
- useEffect4(() => {
2058
+ const [localValue, setLocalValue] = useState7(value);
2059
+ const [isDragging, setIsDragging] = useState7(false);
2060
+ useEffect5(() => {
1898
2061
  if (!isDragging) {
1899
2062
  setLocalValue(value);
1900
2063
  }
1901
2064
  }, [value, isDragging]);
1902
2065
  const debouncedOnChange = useDebouncedCallback(onChange, 50);
1903
- const handleChange = useCallback3(
2066
+ const handleChange = useCallback4(
1904
2067
  (e) => {
1905
2068
  const newValue = parseFloat(e.target.value);
1906
2069
  setLocalValue(newValue);
@@ -1908,19 +2071,19 @@ var VolumeSlider = ({
1908
2071
  },
1909
2072
  [debouncedOnChange]
1910
2073
  );
1911
- const handleMouseDown = useCallback3(() => {
2074
+ const handleMouseDown = useCallback4(() => {
1912
2075
  setIsDragging(true);
1913
2076
  }, []);
1914
- const handleMouseUp = useCallback3(() => {
2077
+ const handleMouseUp = useCallback4(() => {
1915
2078
  setIsDragging(false);
1916
2079
  onChange(localValue);
1917
2080
  }, [localValue, onChange]);
1918
- return /* @__PURE__ */ jsx9(
2081
+ return /* @__PURE__ */ jsx10(
1919
2082
  "div",
1920
2083
  {
1921
2084
  className: `flex items-center ${className}`,
1922
2085
  title: `Volume: ${formatDb(localValue)}`,
1923
- children: /* @__PURE__ */ jsx9(
2086
+ children: /* @__PURE__ */ jsx10(
1924
2087
  "input",
1925
2088
  {
1926
2089
  type: "range",
@@ -1960,8 +2123,8 @@ var VolumeSlider = ({
1960
2123
  };
1961
2124
 
1962
2125
  // src/components/PanSlider.tsx
1963
- import { useCallback as useCallback4, useState as useState7, useRef as useRef6, useEffect as useEffect5 } from "react";
1964
- import { jsx as jsx10 } from "react/jsx-runtime";
2126
+ import { useCallback as useCallback5, useState as useState8, useRef as useRef6, useEffect as useEffect6 } from "react";
2127
+ import { jsx as jsx11 } from "react/jsx-runtime";
1965
2128
  function toPanDisplay(value) {
1966
2129
  if (Math.abs(value) < 0.02) {
1967
2130
  return "Center";
@@ -1972,10 +2135,10 @@ function toPanDisplay(value) {
1972
2135
  function useDebouncedCallback2(callback, delay) {
1973
2136
  const timeoutRef = useRef6(null);
1974
2137
  const callbackRef = useRef6(callback);
1975
- useEffect5(() => {
2138
+ useEffect6(() => {
1976
2139
  callbackRef.current = callback;
1977
2140
  }, [callback]);
1978
- const debouncedCallback = useCallback4(
2141
+ const debouncedCallback = useCallback5(
1979
2142
  (...args) => {
1980
2143
  if (timeoutRef.current) {
1981
2144
  clearTimeout(timeoutRef.current);
@@ -1986,7 +2149,7 @@ function useDebouncedCallback2(callback, delay) {
1986
2149
  },
1987
2150
  [delay]
1988
2151
  );
1989
- useEffect5(() => {
2152
+ useEffect6(() => {
1990
2153
  return () => {
1991
2154
  if (timeoutRef.current) {
1992
2155
  clearTimeout(timeoutRef.current);
@@ -2001,15 +2164,15 @@ var PanSlider = ({
2001
2164
  disabled = false,
2002
2165
  className = ""
2003
2166
  }) => {
2004
- const [localValue, setLocalValue] = useState7(value);
2005
- const [isDragging, setIsDragging] = useState7(false);
2006
- useEffect5(() => {
2167
+ const [localValue, setLocalValue] = useState8(value);
2168
+ const [isDragging, setIsDragging] = useState8(false);
2169
+ useEffect6(() => {
2007
2170
  if (!isDragging) {
2008
2171
  setLocalValue(value);
2009
2172
  }
2010
2173
  }, [value, isDragging]);
2011
2174
  const debouncedOnChange = useDebouncedCallback2(onChange, 50);
2012
- const handleChange = useCallback4(
2175
+ const handleChange = useCallback5(
2013
2176
  (e) => {
2014
2177
  const newValue = parseFloat(e.target.value);
2015
2178
  setLocalValue(newValue);
@@ -2017,23 +2180,23 @@ var PanSlider = ({
2017
2180
  },
2018
2181
  [debouncedOnChange]
2019
2182
  );
2020
- const handleMouseDown = useCallback4(() => {
2183
+ const handleMouseDown = useCallback5(() => {
2021
2184
  setIsDragging(true);
2022
2185
  }, []);
2023
- const handleMouseUp = useCallback4(() => {
2186
+ const handleMouseUp = useCallback5(() => {
2024
2187
  setIsDragging(false);
2025
2188
  onChange(localValue);
2026
2189
  }, [localValue, onChange]);
2027
- const handleDoubleClick = useCallback4(() => {
2190
+ const handleDoubleClick = useCallback5(() => {
2028
2191
  setLocalValue(0);
2029
2192
  onChange(0);
2030
2193
  }, [onChange]);
2031
- return /* @__PURE__ */ jsx10(
2194
+ return /* @__PURE__ */ jsx11(
2032
2195
  "div",
2033
2196
  {
2034
2197
  className: `flex items-center ${className}`,
2035
2198
  title: `Pan: ${toPanDisplay(localValue)}`,
2036
- children: /* @__PURE__ */ jsx10(
2199
+ children: /* @__PURE__ */ jsx11(
2037
2200
  "input",
2038
2201
  {
2039
2202
  type: "range",
@@ -2074,8 +2237,8 @@ var PanSlider = ({
2074
2237
  };
2075
2238
 
2076
2239
  // src/components/SorceryProgressBar.tsx
2077
- import { useState as useState8, useEffect as useEffect6, useRef as useRef7 } from "react";
2078
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2240
+ import { useState as useState9, useEffect as useEffect7, useRef as useRef7 } from "react";
2241
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2079
2242
  function calculateTimeBasedTarget(elapsedMs, estimatedDurationMs) {
2080
2243
  const t = elapsedMs / estimatedDurationMs;
2081
2244
  if (t <= 0) return 0;
@@ -2120,7 +2283,7 @@ function SorceryProgressBar({
2120
2283
  onProgressChange,
2121
2284
  estimatedDurationMs
2122
2285
  }) {
2123
- const [progress, setProgress] = useState8(initialProgress);
2286
+ const [progress, setProgress] = useState9(initialProgress);
2124
2287
  const timerRef = useRef7(null);
2125
2288
  const isLoadingRef = useRef7(false);
2126
2289
  const hasStartedRef = useRef7(false);
@@ -2133,7 +2296,7 @@ function SorceryProgressBar({
2133
2296
  initialProgressRef.current = initialProgress;
2134
2297
  const estimatedDurationMsRef = useRef7(estimatedDurationMs);
2135
2298
  estimatedDurationMsRef.current = estimatedDurationMs;
2136
- useEffect6(() => {
2299
+ useEffect7(() => {
2137
2300
  const wasLoading = isLoadingRef.current;
2138
2301
  isLoadingRef.current = isLoading;
2139
2302
  if (isLoading && !wasLoading) {
@@ -2195,12 +2358,12 @@ function SorceryProgressBar({
2195
2358
  const displayProgress = Math.floor(progress);
2196
2359
  const isComplete = !isLoading && progress === 100;
2197
2360
  const transitionDuration = progress < 50 ? "300ms" : progress < 80 ? "500ms" : "700ms";
2198
- return /* @__PURE__ */ jsxs7(
2361
+ return /* @__PURE__ */ jsxs8(
2199
2362
  "div",
2200
2363
  {
2201
2364
  className: `relative w-full ${heightClass} bg-sas-panel-alt border border-sas-border rounded-sm overflow-hidden shadow-inner`,
2202
2365
  children: [
2203
- /* @__PURE__ */ jsx11(
2366
+ /* @__PURE__ */ jsx12(
2204
2367
  "div",
2205
2368
  {
2206
2369
  className: `
@@ -2218,13 +2381,13 @@ function SorceryProgressBar({
2218
2381
  }
2219
2382
  }
2220
2383
  ),
2221
- /* @__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: [
2384
+ /* @__PURE__ */ jsx12("div", { className: "absolute inset-0 flex items-center justify-center", children: isLoading && progress < 100 ? /* @__PURE__ */ jsxs8("span", { className: "font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider", children: [
2222
2385
  statusText,
2223
2386
  " ",
2224
2387
  displayProgress,
2225
2388
  "%"
2226
- ] }) : isComplete ? /* @__PURE__ */ jsx11("span", { className: "font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider", children: completeText }) : null }),
2227
- /* @__PURE__ */ jsx11(
2389
+ ] }) : isComplete ? /* @__PURE__ */ jsx12("span", { className: "font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider", children: completeText }) : null }),
2390
+ /* @__PURE__ */ jsx12(
2228
2391
  "div",
2229
2392
  {
2230
2393
  className: "absolute inset-0 pointer-events-none opacity-10",
@@ -2310,7 +2473,7 @@ function parseLLMNoteResponse(content) {
2310
2473
  }
2311
2474
 
2312
2475
  // src/components/TrackRow.tsx
2313
- import { Fragment, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2476
+ import { Fragment, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2314
2477
  function TrackRow({
2315
2478
  track,
2316
2479
  prompt,
@@ -2360,6 +2523,7 @@ function TrackRow({
2360
2523
  onToggleFavorite,
2361
2524
  onImportSound,
2362
2525
  importSoundLabel,
2526
+ linkedSoundHint,
2363
2527
  editNotes,
2364
2528
  onNotesChange,
2365
2529
  editBars,
@@ -2371,6 +2535,8 @@ function TrackRow({
2371
2535
  }) {
2372
2536
  const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;
2373
2537
  const [confirmDelete, setConfirmDelete] = React9.useState(false);
2538
+ const freeze = useTrackFreeze(externalFxHost, track.id);
2539
+ const freezeBadge = freeze.state?.frozen === true ? freeze.state.missingDeps.length > 0 ? "missing" : freeze.state.stale ? "stale" : "frozen" : null;
2374
2540
  const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);
2375
2541
  const hasFxActive = Object.values(fxDetailState).some(
2376
2542
  (d) => d.enabled
@@ -2380,8 +2546,8 @@ function TrackRow({
2380
2546
  const handleKeyDown = promptEnterToGenerate(() => onGenerate?.(), !onGenerate);
2381
2547
  const borderColorStyle = needsGeneration ? void 0 : accentColor;
2382
2548
  const borderClass = needsGeneration ? "border-amber-400 animate-pulse" : "border-sas-border";
2383
- return /* @__PURE__ */ jsxs8("div", { "data-testid": "sdk-track-row-wrapper", className: "w-full", ...drag?.rowProps ?? {}, children: [
2384
- /* @__PURE__ */ jsxs8(
2549
+ return /* @__PURE__ */ jsxs9("div", { "data-testid": "sdk-track-row-wrapper", className: "w-full", ...drag?.rowProps ?? {}, children: [
2550
+ /* @__PURE__ */ jsxs9(
2385
2551
  "div",
2386
2552
  {
2387
2553
  "data-testid": "sdk-track-row",
@@ -2391,7 +2557,7 @@ function TrackRow({
2391
2557
  borderLeftWidth: "3px"
2392
2558
  },
2393
2559
  children: [
2394
- drag && /* @__PURE__ */ jsx12(
2560
+ drag && /* @__PURE__ */ jsx13(
2395
2561
  "div",
2396
2562
  {
2397
2563
  "data-testid": "sdk-drag-handle",
@@ -2399,10 +2565,10 @@ function TrackRow({
2399
2565
  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",
2400
2566
  title: "Drag to reorder",
2401
2567
  "aria-label": "Drag to reorder track",
2402
- children: /* @__PURE__ */ jsx12(GripVertical, { className: "w-3.5 h-3.5", strokeWidth: 2 })
2568
+ children: /* @__PURE__ */ jsx13(GripVertical, { className: "w-3.5 h-3.5", strokeWidth: 2 })
2403
2569
  }
2404
2570
  ),
2405
- isGenerating && /* @__PURE__ */ jsx12("div", { className: "absolute left-0 top-0 bottom-0 right-44 z-20", children: /* @__PURE__ */ jsx12(
2571
+ isGenerating && /* @__PURE__ */ jsx13("div", { className: "absolute left-0 top-0 bottom-0 right-44 z-20", children: /* @__PURE__ */ jsx13(
2406
2572
  SorceryProgressBar,
2407
2573
  {
2408
2574
  isLoading: true,
@@ -2413,14 +2579,14 @@ function TrackRow({
2413
2579
  estimatedDurationMs: estimatedGenerationMs
2414
2580
  }
2415
2581
  ) }),
2416
- /* @__PURE__ */ jsxs8(
2582
+ /* @__PURE__ */ jsxs9(
2417
2583
  "div",
2418
2584
  {
2419
2585
  "data-testid": "sdk-track-content",
2420
2586
  className: `flex flex-col flex-1 min-w-0 relative z-10 transition-opacity ${soloedOut ? "opacity-40" : ""}`,
2421
2587
  title: soloedOut ? "Silenced \u2014 another track is soloed" : void 0,
2422
2588
  children: [
2423
- contentSlot ? contentSlot : onPromptChange ? /* @__PURE__ */ jsx12(
2589
+ contentSlot ? contentSlot : onPromptChange ? /* @__PURE__ */ jsx13(
2424
2590
  "input",
2425
2591
  {
2426
2592
  type: "text",
@@ -2433,10 +2599,10 @@ function TrackRow({
2433
2599
  className: "sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed"
2434
2600
  }
2435
2601
  ) : null,
2436
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 mt-1", children: [
2437
- 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 }),
2438
- /* @__PURE__ */ jsx12("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "vol:" }),
2439
- /* @__PURE__ */ jsx12(
2602
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 mt-1", children: [
2603
+ track.name && /* @__PURE__ */ jsx13("span", { className: "text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]", title: track.name, children: track.name }),
2604
+ /* @__PURE__ */ jsx13("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "vol:" }),
2605
+ /* @__PURE__ */ jsx13(
2440
2606
  VolumeSlider,
2441
2607
  {
2442
2608
  value: currentVolume,
@@ -2445,8 +2611,8 @@ function TrackRow({
2445
2611
  className: "flex-1 min-w-[40px]"
2446
2612
  }
2447
2613
  ),
2448
- /* @__PURE__ */ jsx12("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "pan:" }),
2449
- /* @__PURE__ */ jsx12(
2614
+ /* @__PURE__ */ jsx13("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "pan:" }),
2615
+ /* @__PURE__ */ jsx13(
2450
2616
  PanSlider,
2451
2617
  {
2452
2618
  value: currentPan,
@@ -2459,27 +2625,27 @@ function TrackRow({
2459
2625
  ]
2460
2626
  }
2461
2627
  ),
2462
- error && /* @__PURE__ */ jsx12(
2628
+ error && /* @__PURE__ */ jsx13(
2463
2629
  "div",
2464
2630
  {
2465
2631
  "data-testid": "sdk-error-indicator",
2466
2632
  className: "flex-shrink-0 relative z-10 self-stretch flex items-center px-1 group cursor-help",
2467
2633
  title: error,
2468
- children: /* @__PURE__ */ jsxs8("div", { className: "relative", children: [
2469
- /* @__PURE__ */ jsx12(
2634
+ children: /* @__PURE__ */ jsxs9("div", { className: "relative", children: [
2635
+ /* @__PURE__ */ jsx13(
2470
2636
  AlertCircle,
2471
2637
  {
2472
2638
  className: "w-5 h-5 text-red-500 animate-pulse",
2473
2639
  strokeWidth: 2.5
2474
2640
  }
2475
2641
  ),
2476
- /* @__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 })
2642
+ /* @__PURE__ */ jsx13("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 })
2477
2643
  ] })
2478
2644
  }
2479
2645
  ),
2480
- /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center", children: [
2481
- /* @__PURE__ */ jsxs8("div", { className: "flex gap-1 items-center", children: [
2482
- onGenerate && /* @__PURE__ */ jsx12(
2646
+ /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center", children: [
2647
+ /* @__PURE__ */ jsxs9("div", { className: "flex gap-1 items-center", children: [
2648
+ onGenerate && /* @__PURE__ */ jsx13(
2483
2649
  "button",
2484
2650
  {
2485
2651
  "data-testid": "sdk-generate-button",
@@ -2490,7 +2656,7 @@ function TrackRow({
2490
2656
  children: "Create"
2491
2657
  }
2492
2658
  ),
2493
- onCopy && /* @__PURE__ */ jsx12(
2659
+ onCopy && /* @__PURE__ */ jsx13(
2494
2660
  "button",
2495
2661
  {
2496
2662
  "data-testid": "sdk-copy-button",
@@ -2501,7 +2667,7 @@ function TrackRow({
2501
2667
  children: "Copy"
2502
2668
  }
2503
2669
  ),
2504
- /* @__PURE__ */ jsx12(
2670
+ /* @__PURE__ */ jsx13(
2505
2671
  "button",
2506
2672
  {
2507
2673
  "data-testid": "sdk-mute-button",
@@ -2511,7 +2677,7 @@ function TrackRow({
2511
2677
  children: "M"
2512
2678
  }
2513
2679
  ),
2514
- onDelete && /* @__PURE__ */ jsx12(
2680
+ onDelete && /* @__PURE__ */ jsx13(
2515
2681
  "button",
2516
2682
  {
2517
2683
  "data-testid": "sdk-delete-button",
@@ -2522,19 +2688,39 @@ function TrackRow({
2522
2688
  }
2523
2689
  )
2524
2690
  ] }),
2525
- /* @__PURE__ */ jsxs8("div", { className: "flex gap-1 items-center", children: [
2526
- onShuffle && /* @__PURE__ */ jsx12(
2691
+ /* @__PURE__ */ jsxs9("div", { className: "flex gap-1 items-center", children: [
2692
+ onShuffle && /* @__PURE__ */ jsxs9(
2527
2693
  "button",
2528
2694
  {
2529
2695
  "data-testid": "sdk-shuffle-button",
2530
2696
  onClick: onShuffle,
2531
2697
  disabled: !hasMidi || isGenerating || !!currentInstrumentPluginId,
2532
- className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating || !!currentInstrumentPluginId ? "bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
2533
- title: currentInstrumentPluginId ? "Shuffle only works with default Surge XT" : hasMidi ? "Re-roll sound (keep MIDI)" : "Generate MIDI first",
2534
- children: "Shuffle"
2698
+ className: `relative w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating || !!currentInstrumentPluginId ? "bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
2699
+ title: currentInstrumentPluginId ? "Shuffle only works with default Surge XT" : hasMidi ? linkedSoundHint ? `Re-roll sound (keep MIDI) \u2014 ${linkedSoundHint}` : "Re-roll sound (keep MIDI)" : "Generate MIDI first",
2700
+ children: [
2701
+ "Shuffle",
2702
+ linkedSoundHint && /* @__PURE__ */ jsx13(
2703
+ "span",
2704
+ {
2705
+ "aria-hidden": true,
2706
+ "data-testid": "sdk-shuffle-linked-marker",
2707
+ className: "absolute -top-1 -right-1 text-[8px] leading-none",
2708
+ children: "\u{1F517}"
2709
+ }
2710
+ )
2711
+ ]
2535
2712
  }
2536
2713
  ),
2537
- onToggleFxDrawer && /* @__PURE__ */ jsx12(
2714
+ freezeBadge && /* @__PURE__ */ jsx13(
2715
+ "span",
2716
+ {
2717
+ "data-testid": "sdk-track-freeze-badge",
2718
+ className: `px-1 py-0.5 text-xs leading-none rounded-sm self-center ${freezeBadge === "frozen" ? "text-sas-accent" : "text-amber-400"}`,
2719
+ title: freezeBadge === "frozen" ? "Frozen \u2014 playing the rendered stem (mixer stays live)" : freezeBadge === "stale" ? "Frozen (stale) \u2014 sound edited since the stem rendered; re-freeze in the drawer" : `Frozen \u2014 plugin(s) missing on this machine: ${freeze.state?.missingDeps.map((d) => d.name).join(", ")}`,
2720
+ children: freezeBadge === "frozen" ? "\u2744" : freezeBadge === "stale" ? "\u26A0\u2744" : "\u2744!"
2721
+ }
2722
+ ),
2723
+ onToggleFxDrawer && /* @__PURE__ */ jsx13(
2538
2724
  "button",
2539
2725
  {
2540
2726
  "data-testid": "sdk-fx-button",
@@ -2545,7 +2731,7 @@ function TrackRow({
2545
2731
  children: "FX"
2546
2732
  }
2547
2733
  ),
2548
- /* @__PURE__ */ jsx12(
2734
+ /* @__PURE__ */ jsx13(
2549
2735
  "button",
2550
2736
  {
2551
2737
  "data-testid": "sdk-solo-button",
@@ -2556,7 +2742,7 @@ function TrackRow({
2556
2742
  children: "S"
2557
2743
  }
2558
2744
  ),
2559
- onToggleDrawer && /* @__PURE__ */ jsx12(
2745
+ onToggleDrawer && /* @__PURE__ */ jsx13(
2560
2746
  "button",
2561
2747
  {
2562
2748
  "data-testid": "sdk-plugin-button",
@@ -2564,7 +2750,7 @@ function TrackRow({
2564
2750
  disabled: isGenerating,
2565
2751
  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"}`,
2566
2752
  title: `Sound \u2014 presets & history${instrumentMissing ? " (instrument missing)" : ""}`,
2567
- children: /* @__PURE__ */ jsx12(ChevronDown, { className: "w-3 h-3", strokeWidth: 2.5 })
2753
+ children: /* @__PURE__ */ jsx13(ChevronDown, { className: "w-3 h-3", strokeWidth: 2.5 })
2568
2754
  }
2569
2755
  )
2570
2756
  ] })
@@ -2572,13 +2758,13 @@ function TrackRow({
2572
2758
  ]
2573
2759
  }
2574
2760
  ),
2575
- levels && /* @__PURE__ */ jsx12(TrackMeterStrip, { levels, trackId: track.id, roundBottom: !drawerOpen }),
2576
- drawerOpen && /* @__PURE__ */ jsx12(
2761
+ levels && /* @__PURE__ */ jsx13(TrackMeterStrip, { levels, trackId: track.id, roundBottom: !drawerOpen }),
2762
+ drawerOpen && /* @__PURE__ */ jsx13(
2577
2763
  "div",
2578
2764
  {
2579
2765
  "data-testid": "sdk-track-drawer",
2580
2766
  className: "border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2 max-h-[260px] overflow-y-auto",
2581
- children: /* @__PURE__ */ jsx12(
2767
+ children: /* @__PURE__ */ jsx13(
2582
2768
  TrackDrawer,
2583
2769
  {
2584
2770
  activeTab: drawerTab,
@@ -2589,6 +2775,7 @@ function TrackRow({
2589
2775
  onFxPresetChange,
2590
2776
  onFxDryWetChange,
2591
2777
  externalFxHost,
2778
+ freeze,
2592
2779
  fxDisabled: isGenerating,
2593
2780
  instruments: availableInstruments,
2594
2781
  currentPluginId: currentInstrumentPluginId ?? null,
@@ -2605,6 +2792,7 @@ function TrackRow({
2605
2792
  onToggleFavorite,
2606
2793
  onImportSound,
2607
2794
  importSoundLabel,
2795
+ linkedSoundHint,
2608
2796
  editNotes,
2609
2797
  onNotesChange,
2610
2798
  editBars,
@@ -2615,13 +2803,13 @@ function TrackRow({
2615
2803
  )
2616
2804
  }
2617
2805
  ),
2618
- /* @__PURE__ */ jsx12(
2806
+ /* @__PURE__ */ jsx13(
2619
2807
  ConfirmDialog,
2620
2808
  {
2621
2809
  open: confirmDelete,
2622
2810
  title: "Delete track?",
2623
- message: /* @__PURE__ */ jsxs8(Fragment, { children: [
2624
- /* @__PURE__ */ jsx12("span", { className: "text-sas-text", children: track.name?.trim() || "This track" }),
2811
+ message: /* @__PURE__ */ jsxs9(Fragment, { children: [
2812
+ /* @__PURE__ */ jsx13("span", { className: "text-sas-text", children: track.name?.trim() || "This track" }),
2625
2813
  " will be permanently removed from this scene. This cannot be undone."
2626
2814
  ] }),
2627
2815
  confirmLabel: "Delete",
@@ -2638,12 +2826,12 @@ function TrackRow({
2638
2826
 
2639
2827
  // src/components/CrossfadeTrackRow.tsx
2640
2828
  import React10 from "react";
2641
- import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2829
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
2642
2830
  function LayerCaption({ tag, layer }) {
2643
- return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
2644
- /* @__PURE__ */ jsx13("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
2645
- /* @__PURE__ */ jsx13("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
2646
- layer.soundLabel && /* @__PURE__ */ jsxs9("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
2831
+ return /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
2832
+ /* @__PURE__ */ jsx14("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
2833
+ /* @__PURE__ */ jsx14("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
2834
+ layer.soundLabel && /* @__PURE__ */ jsxs10("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
2647
2835
  "\xB7 ",
2648
2836
  layer.soundLabel
2649
2837
  ] })
@@ -2663,7 +2851,7 @@ function CrossfadeTrackRow({
2663
2851
  accentColor = "#9333EA"
2664
2852
  }) {
2665
2853
  const [confirmDelete, setConfirmDelete] = React10.useState(false);
2666
- const renderLayer = (layer, slot, tag) => /* @__PURE__ */ jsx13(
2854
+ const renderLayer = (layer, slot, tag) => /* @__PURE__ */ jsx14(
2667
2855
  TrackRow,
2668
2856
  {
2669
2857
  track: { id: layer.trackId, name: "", role: layer.role },
@@ -2673,23 +2861,23 @@ function CrossfadeTrackRow({
2673
2861
  drawerTab: "fx",
2674
2862
  levels,
2675
2863
  accentColor,
2676
- contentSlot: /* @__PURE__ */ jsx13(LayerCaption, { tag, layer }),
2864
+ contentSlot: /* @__PURE__ */ jsx14(LayerCaption, { tag, layer }),
2677
2865
  onMuteToggle,
2678
2866
  onSoloToggle,
2679
2867
  onVolumeChange: (v) => onVolumeChange(slot, v),
2680
2868
  onPanChange: (p) => onPanChange(slot, p)
2681
2869
  }
2682
2870
  );
2683
- return /* @__PURE__ */ jsxs9(
2871
+ return /* @__PURE__ */ jsxs10(
2684
2872
  "div",
2685
2873
  {
2686
2874
  "data-testid": "crossfade-track-row",
2687
2875
  className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
2688
2876
  style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
2689
2877
  children: [
2690
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
2691
- /* @__PURE__ */ jsx13("span", { className: "text-[10px] font-bold uppercase tracking-wide", style: { color: accentColor }, children: "\u21C4 Crossfade" }),
2692
- /* @__PURE__ */ jsx13(
2878
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
2879
+ /* @__PURE__ */ jsx14("span", { className: "text-[10px] font-bold uppercase tracking-wide", style: { color: accentColor }, children: "\u21C4 Crossfade" }),
2880
+ /* @__PURE__ */ jsx14(
2693
2881
  "button",
2694
2882
  {
2695
2883
  "data-testid": "crossfade-delete-button",
@@ -2702,8 +2890,8 @@ function CrossfadeTrackRow({
2702
2890
  )
2703
2891
  ] }),
2704
2892
  renderLayer(origin, "origin", "Origin"),
2705
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "crossfade-slider-row", children: [
2706
- /* @__PURE__ */ jsx13(
2893
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "crossfade-slider-row", children: [
2894
+ /* @__PURE__ */ jsx14(
2707
2895
  "span",
2708
2896
  {
2709
2897
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
@@ -2711,7 +2899,7 @@ function CrossfadeTrackRow({
2711
2899
  children: origin.sourceName ?? origin.name
2712
2900
  }
2713
2901
  ),
2714
- /* @__PURE__ */ jsx13(
2902
+ /* @__PURE__ */ jsx14(
2715
2903
  "input",
2716
2904
  {
2717
2905
  type: "range",
@@ -2727,7 +2915,7 @@ function CrossfadeTrackRow({
2727
2915
  "aria-label": "Crossfade position"
2728
2916
  }
2729
2917
  ),
2730
- /* @__PURE__ */ jsx13(
2918
+ /* @__PURE__ */ jsx14(
2731
2919
  "span",
2732
2920
  {
2733
2921
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
@@ -2737,12 +2925,12 @@ function CrossfadeTrackRow({
2737
2925
  )
2738
2926
  ] }),
2739
2927
  renderLayer(target, "target", "Target"),
2740
- /* @__PURE__ */ jsx13(
2928
+ /* @__PURE__ */ jsx14(
2741
2929
  ConfirmDialog,
2742
2930
  {
2743
2931
  open: confirmDelete,
2744
2932
  title: "Delete crossfade?",
2745
- message: /* @__PURE__ */ jsx13(Fragment2, { children: "This crossfade pair (both layers) will be permanently removed from this scene. This cannot be undone." }),
2933
+ message: /* @__PURE__ */ jsx14(Fragment2, { children: "This crossfade pair (both layers) will be permanently removed from this scene. This cannot be undone." }),
2746
2934
  confirmLabel: "Delete",
2747
2935
  onConfirm: () => {
2748
2936
  setConfirmDelete(false);
@@ -3016,21 +3204,21 @@ function defaultFadeGesture(role) {
3016
3204
 
3017
3205
  // src/components/FadeTrackRow.tsx
3018
3206
  import React11 from "react";
3019
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
3207
+ import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
3020
3208
  function FadeCaption({
3021
3209
  layer,
3022
3210
  direction,
3023
3211
  gesture
3024
3212
  }) {
3025
3213
  const tag = direction === "in" ? "Fade in" : "Fade out";
3026
- return /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
3027
- /* @__PURE__ */ jsx14("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
3028
- /* @__PURE__ */ jsx14("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
3029
- layer.soundLabel && /* @__PURE__ */ jsxs10("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
3214
+ return /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
3215
+ /* @__PURE__ */ jsx15("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
3216
+ /* @__PURE__ */ jsx15("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
3217
+ layer.soundLabel && /* @__PURE__ */ jsxs11("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
3030
3218
  "\xB7 ",
3031
3219
  layer.soundLabel
3032
3220
  ] }),
3033
- /* @__PURE__ */ jsxs10("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", title: `Fade gesture: ${gesture}`, children: [
3221
+ /* @__PURE__ */ jsxs11("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", title: `Fade gesture: ${gesture}`, children: [
3034
3222
  "\xB7 ",
3035
3223
  gesture
3036
3224
  ] })
@@ -3056,15 +3244,15 @@ function FadeTrackRow({
3056
3244
  const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
3057
3245
  const verb = effect && effect !== "fade" ? effect.charAt(0).toUpperCase() + effect.slice(1) : "Fade";
3058
3246
  const badge = direction === "in" ? `\u2197 ${verb} in` : `\u2198 ${verb} out`;
3059
- return /* @__PURE__ */ jsxs10(
3247
+ return /* @__PURE__ */ jsxs11(
3060
3248
  "div",
3061
3249
  {
3062
3250
  "data-testid": "fade-track-row",
3063
3251
  className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
3064
3252
  style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
3065
3253
  children: [
3066
- /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
3067
- /* @__PURE__ */ jsx14(
3254
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
3255
+ /* @__PURE__ */ jsx15(
3068
3256
  "span",
3069
3257
  {
3070
3258
  "data-testid": "fade-direction-badge",
@@ -3073,7 +3261,7 @@ function FadeTrackRow({
3073
3261
  children: badge
3074
3262
  }
3075
3263
  ),
3076
- /* @__PURE__ */ jsx14(
3264
+ /* @__PURE__ */ jsx15(
3077
3265
  "button",
3078
3266
  {
3079
3267
  "data-testid": "fade-delete-button",
@@ -3085,7 +3273,7 @@ function FadeTrackRow({
3085
3273
  }
3086
3274
  )
3087
3275
  ] }),
3088
- /* @__PURE__ */ jsx14(
3276
+ /* @__PURE__ */ jsx15(
3089
3277
  TrackRow,
3090
3278
  {
3091
3279
  track: { id: layer.trackId, name: "", role: layer.role },
@@ -3095,15 +3283,15 @@ function FadeTrackRow({
3095
3283
  drawerTab: "fx",
3096
3284
  levels,
3097
3285
  accentColor,
3098
- contentSlot: /* @__PURE__ */ jsx14(FadeCaption, { layer, direction, gesture }),
3286
+ contentSlot: /* @__PURE__ */ jsx15(FadeCaption, { layer, direction, gesture }),
3099
3287
  onMuteToggle,
3100
3288
  onSoloToggle,
3101
3289
  onVolumeChange,
3102
3290
  onPanChange
3103
3291
  }
3104
3292
  ),
3105
- /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "fade-slider-row", children: [
3106
- /* @__PURE__ */ jsx14(
3293
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "fade-slider-row", children: [
3294
+ /* @__PURE__ */ jsx15(
3107
3295
  "span",
3108
3296
  {
3109
3297
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
@@ -3111,7 +3299,7 @@ function FadeTrackRow({
3111
3299
  children: leftLabel
3112
3300
  }
3113
3301
  ),
3114
- /* @__PURE__ */ jsx14(
3302
+ /* @__PURE__ */ jsx15(
3115
3303
  "input",
3116
3304
  {
3117
3305
  type: "range",
@@ -3127,7 +3315,7 @@ function FadeTrackRow({
3127
3315
  "aria-label": "Fade position"
3128
3316
  }
3129
3317
  ),
3130
- /* @__PURE__ */ jsx14(
3318
+ /* @__PURE__ */ jsx15(
3131
3319
  "span",
3132
3320
  {
3133
3321
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
@@ -3136,12 +3324,12 @@ function FadeTrackRow({
3136
3324
  }
3137
3325
  )
3138
3326
  ] }),
3139
- /* @__PURE__ */ jsx14(
3327
+ /* @__PURE__ */ jsx15(
3140
3328
  ConfirmDialog,
3141
3329
  {
3142
3330
  open: confirmDelete,
3143
3331
  title: "Delete fade?",
3144
- message: /* @__PURE__ */ jsx14(Fragment3, { children: "This fade track will be permanently removed from this scene. This cannot be undone." }),
3332
+ message: /* @__PURE__ */ jsx15(Fragment3, { children: "This fade track will be permanently removed from this scene. This cannot be undone." }),
3145
3333
  confirmLabel: "Delete",
3146
3334
  onConfirm: () => {
3147
3335
  setConfirmDelete(false);
@@ -3158,20 +3346,20 @@ function FadeTrackRow({
3158
3346
 
3159
3347
  // src/components/GroupFadeTrackRow.tsx
3160
3348
  import React12 from "react";
3161
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
3349
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
3162
3350
  function MemberCaption({
3163
3351
  member,
3164
3352
  direction
3165
3353
  }) {
3166
3354
  const tag = direction === "in" ? "Fade in" : "Fade out";
3167
- return /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
3168
- member.memberLabel && /* @__PURE__ */ jsx15("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: member.memberLabel }),
3169
- /* @__PURE__ */ jsx15("span", { className: "text-[11px] text-sas-text truncate", title: member.sourceName ?? member.name, children: member.sourceName ?? member.name }),
3170
- member.soundLabel && /* @__PURE__ */ jsxs11("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: member.soundLabel, children: [
3355
+ return /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
3356
+ member.memberLabel && /* @__PURE__ */ jsx16("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: member.memberLabel }),
3357
+ /* @__PURE__ */ jsx16("span", { className: "text-[11px] text-sas-text truncate", title: member.sourceName ?? member.name, children: member.sourceName ?? member.name }),
3358
+ member.soundLabel && /* @__PURE__ */ jsxs12("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: member.soundLabel, children: [
3171
3359
  "\xB7 ",
3172
3360
  member.soundLabel
3173
3361
  ] }),
3174
- !member.memberLabel && /* @__PURE__ */ jsxs11("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: [
3362
+ !member.memberLabel && /* @__PURE__ */ jsxs12("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: [
3175
3363
  "\xB7 ",
3176
3364
  tag
3177
3365
  ] })
@@ -3200,16 +3388,16 @@ function GroupFadeTrackRow({
3200
3388
  const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
3201
3389
  const leftLabel = direction === "in" ? "(silent)" : groupLabel;
3202
3390
  const rightLabel = direction === "in" ? groupLabel : "(silent)";
3203
- return /* @__PURE__ */ jsxs11(
3391
+ return /* @__PURE__ */ jsxs12(
3204
3392
  "div",
3205
3393
  {
3206
3394
  "data-testid": "group-fade-track-row",
3207
3395
  className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
3208
3396
  style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
3209
3397
  children: [
3210
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60 gap-2", children: [
3211
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 min-w-0", children: [
3212
- /* @__PURE__ */ jsx15(
3398
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60 gap-2", children: [
3399
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2 min-w-0", children: [
3400
+ /* @__PURE__ */ jsx16(
3213
3401
  "span",
3214
3402
  {
3215
3403
  "data-testid": "group-fade-direction-badge",
@@ -3218,7 +3406,7 @@ function GroupFadeTrackRow({
3218
3406
  children: badge
3219
3407
  }
3220
3408
  ),
3221
- /* @__PURE__ */ jsx15(
3409
+ /* @__PURE__ */ jsx16(
3222
3410
  "span",
3223
3411
  {
3224
3412
  "data-testid": "group-fade-label",
@@ -3228,8 +3416,8 @@ function GroupFadeTrackRow({
3228
3416
  }
3229
3417
  )
3230
3418
  ] }),
3231
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1 flex-shrink-0", children: [
3232
- /* @__PURE__ */ jsx15(
3419
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1 flex-shrink-0", children: [
3420
+ /* @__PURE__ */ jsx16(
3233
3421
  "button",
3234
3422
  {
3235
3423
  "data-testid": "group-fade-mute-button",
@@ -3239,7 +3427,7 @@ function GroupFadeTrackRow({
3239
3427
  children: "M"
3240
3428
  }
3241
3429
  ),
3242
- /* @__PURE__ */ jsx15(
3430
+ /* @__PURE__ */ jsx16(
3243
3431
  "button",
3244
3432
  {
3245
3433
  "data-testid": "group-fade-solo-button",
@@ -3249,7 +3437,7 @@ function GroupFadeTrackRow({
3249
3437
  children: "S"
3250
3438
  }
3251
3439
  ),
3252
- /* @__PURE__ */ jsx15(
3440
+ /* @__PURE__ */ jsx16(
3253
3441
  "button",
3254
3442
  {
3255
3443
  "data-testid": "group-fade-delete-button",
@@ -3262,7 +3450,7 @@ function GroupFadeTrackRow({
3262
3450
  )
3263
3451
  ] })
3264
3452
  ] }),
3265
- members.map((member) => /* @__PURE__ */ jsx15(
3453
+ members.map((member) => /* @__PURE__ */ jsx16(
3266
3454
  TrackRow,
3267
3455
  {
3268
3456
  track: { id: member.trackId, name: "", role: member.role },
@@ -3272,7 +3460,7 @@ function GroupFadeTrackRow({
3272
3460
  drawerTab: "fx",
3273
3461
  levels,
3274
3462
  accentColor,
3275
- contentSlot: /* @__PURE__ */ jsx15(MemberCaption, { member, direction }),
3463
+ contentSlot: /* @__PURE__ */ jsx16(MemberCaption, { member, direction }),
3276
3464
  onMuteToggle: () => onMemberMuteToggle(member.trackId),
3277
3465
  onSoloToggle: () => onMemberSoloToggle(member.trackId),
3278
3466
  onVolumeChange: (vol) => onMemberVolumeChange(member.trackId, vol),
@@ -3280,8 +3468,8 @@ function GroupFadeTrackRow({
3280
3468
  },
3281
3469
  member.trackId
3282
3470
  )),
3283
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "group-fade-slider-row", children: [
3284
- /* @__PURE__ */ jsx15(
3471
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "group-fade-slider-row", children: [
3472
+ /* @__PURE__ */ jsx16(
3285
3473
  "span",
3286
3474
  {
3287
3475
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
@@ -3289,7 +3477,7 @@ function GroupFadeTrackRow({
3289
3477
  children: leftLabel
3290
3478
  }
3291
3479
  ),
3292
- /* @__PURE__ */ jsx15(
3480
+ /* @__PURE__ */ jsx16(
3293
3481
  "input",
3294
3482
  {
3295
3483
  type: "range",
@@ -3305,7 +3493,7 @@ function GroupFadeTrackRow({
3305
3493
  "aria-label": "Group fade position"
3306
3494
  }
3307
3495
  ),
3308
- /* @__PURE__ */ jsx15(
3496
+ /* @__PURE__ */ jsx16(
3309
3497
  "span",
3310
3498
  {
3311
3499
  className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
@@ -3314,12 +3502,12 @@ function GroupFadeTrackRow({
3314
3502
  }
3315
3503
  )
3316
3504
  ] }),
3317
- /* @__PURE__ */ jsx15(
3505
+ /* @__PURE__ */ jsx16(
3318
3506
  ConfirmDialog,
3319
3507
  {
3320
3508
  open: confirmDelete,
3321
3509
  title: "Delete group fade?",
3322
- message: /* @__PURE__ */ jsxs11(Fragment4, { children: [
3510
+ message: /* @__PURE__ */ jsxs12(Fragment4, { children: [
3323
3511
  "All ",
3324
3512
  members.length,
3325
3513
  " member tracks of this fade will be permanently removed from this scene. This cannot be undone."
@@ -3339,8 +3527,8 @@ function GroupFadeTrackRow({
3339
3527
  }
3340
3528
 
3341
3529
  // src/components/FadeModal.tsx
3342
- import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef8, useState as useState9 } from "react";
3343
- import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
3530
+ import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo4, useRef as useRef8, useState as useState10 } from "react";
3531
+ import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
3344
3532
  function shortId(dbId) {
3345
3533
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3346
3534
  }
@@ -3383,7 +3571,7 @@ function OrphanRow({
3383
3571
  }) {
3384
3572
  const primary = track.prompt?.trim() || track.name;
3385
3573
  const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
3386
- return /* @__PURE__ */ jsxs12(
3574
+ return /* @__PURE__ */ jsxs13(
3387
3575
  "button",
3388
3576
  {
3389
3577
  type: "button",
@@ -3395,8 +3583,8 @@ function OrphanRow({
3395
3583
  disabled,
3396
3584
  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"}`,
3397
3585
  children: [
3398
- /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3399
- meta && /* @__PURE__ */ jsx16("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3586
+ /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3587
+ meta && /* @__PURE__ */ jsx17("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3400
3588
  ]
3401
3589
  }
3402
3590
  );
@@ -3413,14 +3601,14 @@ function FadeModal({
3413
3601
  onCreate,
3414
3602
  testIdPrefix = "fade-modal"
3415
3603
  }) {
3416
- const [load, setLoad] = useState9({ status: "loading" });
3417
- const [selectedDbId, setSelectedDbId] = useState9("");
3418
- const [isCreating, setIsCreating] = useState9(false);
3419
- const [error, setError] = useState9(null);
3420
- const [fromName, setFromName] = useState9(null);
3421
- const [toName, setToName] = useState9(null);
3604
+ const [load, setLoad] = useState10({ status: "loading" });
3605
+ const [selectedDbId, setSelectedDbId] = useState10("");
3606
+ const [isCreating, setIsCreating] = useState10(false);
3607
+ const [error, setError] = useState10(null);
3608
+ const [fromName, setFromName] = useState10(null);
3609
+ const [toName, setToName] = useState10(null);
3422
3610
  const cancelRef = useRef8(null);
3423
- const refresh = useCallback5(async () => {
3611
+ const refresh = useCallback6(async () => {
3424
3612
  if (!host.listSceneFamilyTracks) {
3425
3613
  setLoad({ status: "error", message: "This host does not support fades." });
3426
3614
  return;
@@ -3440,7 +3628,7 @@ function FadeModal({
3440
3628
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
3441
3629
  }
3442
3630
  }, [host, fromSceneId, toSceneId]);
3443
- useEffect7(() => {
3631
+ useEffect8(() => {
3444
3632
  if (open) {
3445
3633
  setError(null);
3446
3634
  setIsCreating(false);
@@ -3460,17 +3648,17 @@ function FadeModal({
3460
3648
  ],
3461
3649
  [fadeOut, fadeIn]
3462
3650
  );
3463
- useEffect7(() => {
3651
+ useEffect8(() => {
3464
3652
  if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {
3465
3653
  setSelectedDbId(allOrphans[0]?.track.dbId ?? "");
3466
3654
  }
3467
3655
  }, [allOrphans, selectedDbId]);
3468
3656
  const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;
3469
3657
  const canCreate = !isCreating && !!selected;
3470
- const handleClose = useCallback5(() => {
3658
+ const handleClose = useCallback6(() => {
3471
3659
  if (!isCreating) onClose();
3472
3660
  }, [isCreating, onClose]);
3473
- const handleCreate = useCallback5(async () => {
3661
+ const handleCreate = useCallback6(async () => {
3474
3662
  if (!selected) return;
3475
3663
  setIsCreating(true);
3476
3664
  setError(null);
@@ -3491,16 +3679,16 @@ function FadeModal({
3491
3679
  if (!open) return null;
3492
3680
  const renderSection = (heading, list, section) => {
3493
3681
  if (list.length === 0) return null;
3494
- return /* @__PURE__ */ jsxs12("div", { className: "block", children: [
3495
- /* @__PURE__ */ jsx16("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
3496
- /* @__PURE__ */ jsx16(
3682
+ return /* @__PURE__ */ jsxs13("div", { className: "block", children: [
3683
+ /* @__PURE__ */ jsx17("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
3684
+ /* @__PURE__ */ jsx17(
3497
3685
  "div",
3498
3686
  {
3499
3687
  role: "radiogroup",
3500
3688
  "aria-label": heading,
3501
3689
  "data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
3502
3690
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3503
- children: list.map((t) => /* @__PURE__ */ jsx16(
3691
+ children: list.map((t) => /* @__PURE__ */ jsx17(
3504
3692
  OrphanRow,
3505
3693
  {
3506
3694
  track: t,
@@ -3516,32 +3704,32 @@ function FadeModal({
3516
3704
  )
3517
3705
  ] });
3518
3706
  };
3519
- return /* @__PURE__ */ jsx16(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs12(
3707
+ return /* @__PURE__ */ jsx17(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs13(
3520
3708
  "div",
3521
3709
  {
3522
3710
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3523
3711
  onClick: (e) => e.stopPropagation(),
3524
3712
  "data-testid": `${testIdPrefix}-box`,
3525
3713
  children: [
3526
- /* @__PURE__ */ jsx16("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
3527
- /* @__PURE__ */ jsxs12("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3714
+ /* @__PURE__ */ jsx17("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
3715
+ /* @__PURE__ */ jsxs13("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3528
3716
  "Tracks with no counterpart between",
3529
3717
  " ",
3530
- /* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3718
+ /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3531
3719
  " and",
3532
3720
  " ",
3533
- /* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3721
+ /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3534
3722
  " can gracefully fade out (leaving) or fade in (entering) across this transition."
3535
3723
  ] }),
3536
- load.status === "loading" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3537
- load.status === "error" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3538
- load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-muted py-4 text-center", "data-testid": `${testIdPrefix}-empty`, children: "Every track has a counterpart in the other scene \u2014 nothing to fade. Use \u201C+ Crossfade\u201D to bridge matching tracks." }) : /* @__PURE__ */ jsxs12(Fragment5, { children: [
3724
+ load.status === "loading" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3725
+ load.status === "error" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3726
+ load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ jsx17("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__ */ jsxs13(Fragment5, { children: [
3539
3727
  renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
3540
3728
  renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
3541
3729
  ] })),
3542
- error && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3543
- /* @__PURE__ */ jsxs12("div", { className: "flex justify-end gap-2 pt-1", children: [
3544
- /* @__PURE__ */ jsx16(
3730
+ error && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3731
+ /* @__PURE__ */ jsxs13("div", { className: "flex justify-end gap-2 pt-1", children: [
3732
+ /* @__PURE__ */ jsx17(
3545
3733
  "button",
3546
3734
  {
3547
3735
  ref: cancelRef,
@@ -3552,7 +3740,7 @@ function FadeModal({
3552
3740
  children: "Cancel"
3553
3741
  }
3554
3742
  ),
3555
- /* @__PURE__ */ jsx16(
3743
+ /* @__PURE__ */ jsx17(
3556
3744
  "button",
3557
3745
  {
3558
3746
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3569,8 +3757,8 @@ function FadeModal({
3569
3757
  }
3570
3758
 
3571
3759
  // src/components/ImportTrackModal.tsx
3572
- import { useCallback as useCallback6, useEffect as useEffect8, useState as useState10 } from "react";
3573
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
3760
+ import { useCallback as useCallback7, useEffect as useEffect9, useState as useState11 } from "react";
3761
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
3574
3762
  function ImportTrackModal({
3575
3763
  host,
3576
3764
  open,
@@ -3582,10 +3770,10 @@ function ImportTrackModal({
3582
3770
  onPick,
3583
3771
  onPortTrack
3584
3772
  }) {
3585
- const [load, setLoad] = useState10({ status: "loading" });
3586
- const [selectedSceneId, setSelectedSceneId] = useState10(null);
3587
- const [importingTrackId, setImportingTrackId] = useState10(null);
3588
- const refresh = useCallback6(async () => {
3773
+ const [load, setLoad] = useState11({ status: "loading" });
3774
+ const [selectedSceneId, setSelectedSceneId] = useState11(null);
3775
+ const [importingTrackId, setImportingTrackId] = useState11(null);
3776
+ const refresh = useCallback7(async () => {
3589
3777
  if (!host.listImportableTracks) {
3590
3778
  setLoad({ status: "error", message: "This host does not support importing tracks." });
3591
3779
  return;
@@ -3601,14 +3789,14 @@ function ImportTrackModal({
3601
3789
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
3602
3790
  }
3603
3791
  }, [host, mode, onPortTrack]);
3604
- useEffect8(() => {
3792
+ useEffect9(() => {
3605
3793
  if (open) {
3606
3794
  setSelectedSceneId(null);
3607
3795
  setImportingTrackId(null);
3608
3796
  void refresh();
3609
3797
  }
3610
3798
  }, [open, refresh]);
3611
- const handleImport = useCallback6(
3799
+ const handleImport = useCallback7(
3612
3800
  async (track, sourceSceneId, sceneName, isSameScene) => {
3613
3801
  if (isSameScene && onPortTrack) {
3614
3802
  if (!track.importable) return;
@@ -3649,16 +3837,16 @@ function ImportTrackModal({
3649
3837
  if (!open) return null;
3650
3838
  const scenes = load.status === "ready" ? load.scenes : [];
3651
3839
  const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
3652
- return /* @__PURE__ */ jsx17(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs13(
3840
+ return /* @__PURE__ */ jsx18(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs14(
3653
3841
  "div",
3654
3842
  {
3655
3843
  className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
3656
3844
  onClick: (e) => e.stopPropagation(),
3657
3845
  "data-testid": `${testIdPrefix}-modal`,
3658
3846
  children: [
3659
- /* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3660
- /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2", children: [
3661
- selectedScene && /* @__PURE__ */ jsx17(
3847
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3848
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
3849
+ selectedScene && /* @__PURE__ */ jsx18(
3662
3850
  "button",
3663
3851
  {
3664
3852
  className: "text-sas-muted hover:text-sas-accent text-xs",
@@ -3667,9 +3855,9 @@ function ImportTrackModal({
3667
3855
  children: "\u2190"
3668
3856
  }
3669
3857
  ),
3670
- /* @__PURE__ */ jsx17("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3858
+ /* @__PURE__ */ jsx18("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3671
3859
  ] }),
3672
- /* @__PURE__ */ jsx17(
3860
+ /* @__PURE__ */ jsx18(
3673
3861
  "button",
3674
3862
  {
3675
3863
  className: "text-sas-muted hover:text-sas-accent text-sm",
@@ -3679,30 +3867,30 @@ function ImportTrackModal({
3679
3867
  }
3680
3868
  )
3681
3869
  ] }),
3682
- /* @__PURE__ */ jsxs13("div", { className: "overflow-y-auto p-2 flex-1", children: [
3683
- load.status === "loading" && /* @__PURE__ */ jsx17("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3684
- load.status === "error" && /* @__PURE__ */ jsx17("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3685
- load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx17("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
3686
- load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx17("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsxs13(
3870
+ /* @__PURE__ */ jsxs14("div", { className: "overflow-y-auto p-2 flex-1", children: [
3871
+ load.status === "loading" && /* @__PURE__ */ jsx18("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3872
+ load.status === "error" && /* @__PURE__ */ jsx18("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3873
+ load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx18("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." }),
3874
+ load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx18("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsxs14(
3687
3875
  "button",
3688
3876
  {
3689
3877
  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",
3690
3878
  onClick: () => setSelectedSceneId(scene.sceneId),
3691
3879
  "data-testid": `${testIdPrefix}-scene`,
3692
3880
  children: [
3693
- /* @__PURE__ */ jsx17("span", { className: "truncate", children: scene.sceneName }),
3694
- /* @__PURE__ */ jsxs13("span", { className: "text-sas-muted", children: [
3881
+ /* @__PURE__ */ jsx18("span", { className: "truncate", children: scene.sceneName }),
3882
+ /* @__PURE__ */ jsxs14("span", { className: "text-sas-muted", children: [
3695
3883
  scene.tracks.length,
3696
3884
  " \u2192"
3697
3885
  ] })
3698
3886
  ]
3699
3887
  }
3700
3888
  ) }, scene.sceneId)) }),
3701
- selectedScene && /* @__PURE__ */ jsx17("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3889
+ selectedScene && /* @__PURE__ */ jsx18("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3702
3890
  const busy = importingTrackId === track.trackId;
3703
3891
  const gated = mode === "track" && !track.importable;
3704
3892
  const disabled = gated || busy;
3705
- return /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsxs13(
3893
+ return /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsxs14(
3706
3894
  "button",
3707
3895
  {
3708
3896
  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"}`,
@@ -3712,14 +3900,14 @@ function ImportTrackModal({
3712
3900
  "data-testid": `${testIdPrefix}-track`,
3713
3901
  "data-importable": mode === "sound" || track.importable ? "true" : "false",
3714
3902
  children: [
3715
- /* @__PURE__ */ jsxs13("span", { className: "truncate", children: [
3903
+ /* @__PURE__ */ jsxs14("span", { className: "truncate", children: [
3716
3904
  track.name,
3717
- track.role ? /* @__PURE__ */ jsxs13("span", { className: "text-sas-muted", children: [
3905
+ track.role ? /* @__PURE__ */ jsxs14("span", { className: "text-sas-muted", children: [
3718
3906
  " \xB7 ",
3719
3907
  track.role
3720
3908
  ] }) : null
3721
3909
  ] }),
3722
- busy ? /* @__PURE__ */ jsx17("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx17("span", { className: "text-sas-muted", children: "\u2298" }) : null
3910
+ busy ? /* @__PURE__ */ jsx18("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx18("span", { className: "text-sas-muted", children: "\u2298" }) : null
3723
3911
  ]
3724
3912
  }
3725
3913
  ) }, track.dbId);
@@ -3731,8 +3919,8 @@ function ImportTrackModal({
3731
3919
  }
3732
3920
 
3733
3921
  // src/components/CrossfadeModal.tsx
3734
- import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef9, useState as useState11 } from "react";
3735
- import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
3922
+ import { useCallback as useCallback8, useEffect as useEffect10, useMemo as useMemo5, useRef as useRef9, useState as useState12 } from "react";
3923
+ import { Fragment as Fragment6, jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
3736
3924
  function shortId2(dbId) {
3737
3925
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3738
3926
  }
@@ -3745,7 +3933,7 @@ function CandidateRow({
3745
3933
  }) {
3746
3934
  const primary = track.prompt?.trim() || track.name;
3747
3935
  const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
3748
- return /* @__PURE__ */ jsxs14(
3936
+ return /* @__PURE__ */ jsxs15(
3749
3937
  "button",
3750
3938
  {
3751
3939
  type: "button",
@@ -3757,8 +3945,8 @@ function CandidateRow({
3757
3945
  disabled,
3758
3946
  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"}`,
3759
3947
  children: [
3760
- /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3761
- meta && /* @__PURE__ */ jsx18("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3948
+ /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3949
+ meta && /* @__PURE__ */ jsx19("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3762
3950
  ]
3763
3951
  }
3764
3952
  );
@@ -3775,15 +3963,15 @@ function CrossfadeModal({
3775
3963
  onCreate,
3776
3964
  testIdPrefix = "crossfade-modal"
3777
3965
  }) {
3778
- const [load, setLoad] = useState11({ status: "loading" });
3779
- const [originDbId, setOriginDbId] = useState11("");
3780
- const [targetDbId, setTargetDbId] = useState11("");
3781
- const [isCreating, setIsCreating] = useState11(false);
3782
- const [error, setError] = useState11(null);
3783
- const [fromName, setFromName] = useState11(null);
3784
- const [toName, setToName] = useState11(null);
3966
+ const [load, setLoad] = useState12({ status: "loading" });
3967
+ const [originDbId, setOriginDbId] = useState12("");
3968
+ const [targetDbId, setTargetDbId] = useState12("");
3969
+ const [isCreating, setIsCreating] = useState12(false);
3970
+ const [error, setError] = useState12(null);
3971
+ const [fromName, setFromName] = useState12(null);
3972
+ const [toName, setToName] = useState12(null);
3785
3973
  const cancelRef = useRef9(null);
3786
- const refresh = useCallback7(async () => {
3974
+ const refresh = useCallback8(async () => {
3787
3975
  if (!host.listSceneFamilyTracks) {
3788
3976
  setLoad({ status: "error", message: "This host does not support crossfade tracks." });
3789
3977
  return;
@@ -3803,7 +3991,7 @@ function CrossfadeModal({
3803
3991
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
3804
3992
  }
3805
3993
  }, [host, fromSceneId, toSceneId]);
3806
- useEffect9(() => {
3994
+ useEffect10(() => {
3807
3995
  if (open) {
3808
3996
  setError(null);
3809
3997
  setIsCreating(false);
@@ -3821,12 +4009,12 @@ function CrossfadeModal({
3821
4009
  () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
3822
4010
  [load, excludeSet]
3823
4011
  );
3824
- useEffect9(() => {
4012
+ useEffect10(() => {
3825
4013
  if (!originCandidates.some((t) => t.dbId === originDbId)) {
3826
4014
  setOriginDbId(originCandidates[0]?.dbId ?? "");
3827
4015
  }
3828
4016
  }, [originCandidates, originDbId]);
3829
- useEffect9(() => {
4017
+ useEffect10(() => {
3830
4018
  if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
3831
4019
  setTargetDbId(targetCandidates[0]?.dbId ?? "");
3832
4020
  }
@@ -3834,10 +4022,10 @@ function CrossfadeModal({
3834
4022
  const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
3835
4023
  const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
3836
4024
  const canCreate = !isCreating && !!originTrack && !!targetTrack;
3837
- const handleClose = useCallback7(() => {
4025
+ const handleClose = useCallback8(() => {
3838
4026
  if (!isCreating) onClose();
3839
4027
  }, [isCreating, onClose]);
3840
- const handleCreate = useCallback7(async () => {
4028
+ const handleCreate = useCallback8(async () => {
3841
4029
  if (!originTrack || !targetTrack) return;
3842
4030
  setIsCreating(true);
3843
4031
  setError(null);
@@ -3855,26 +4043,26 @@ function CrossfadeModal({
3855
4043
  const fromLabel = fromName ?? fromSceneName ?? null;
3856
4044
  const toLabel = toName ?? toSceneName ?? null;
3857
4045
  if (!open) return null;
3858
- return /* @__PURE__ */ jsx18(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs14(
4046
+ return /* @__PURE__ */ jsx19(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs15(
3859
4047
  "div",
3860
4048
  {
3861
4049
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3862
4050
  onClick: (e) => e.stopPropagation(),
3863
4051
  "data-testid": `${testIdPrefix}-box`,
3864
4052
  children: [
3865
- /* @__PURE__ */ jsx18("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
3866
- /* @__PURE__ */ jsxs14("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
4053
+ /* @__PURE__ */ jsx19("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
4054
+ /* @__PURE__ */ jsxs15("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3867
4055
  "Bridge a track from",
3868
4056
  " ",
3869
- /* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
4057
+ /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3870
4058
  " into one from",
3871
4059
  " ",
3872
- /* @__PURE__ */ jsx18("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
4060
+ /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3873
4061
  ". Both layers share one generated part; each keeps its own preset."
3874
4062
  ] }),
3875
- load.status === "loading" && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3876
- load.status === "error" && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3877
- load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs14(
4063
+ load.status === "loading" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
4064
+ load.status === "error" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
4065
+ load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs15(
3878
4066
  "div",
3879
4067
  {
3880
4068
  className: "text-xs text-sas-muted py-4 text-center",
@@ -3885,20 +4073,20 @@ function CrossfadeModal({
3885
4073
  ". Add one (or free one from another crossfade) first."
3886
4074
  ]
3887
4075
  }
3888
- ) : /* @__PURE__ */ jsxs14(Fragment6, { children: [
3889
- /* @__PURE__ */ jsxs14("div", { className: "block", children: [
3890
- /* @__PURE__ */ jsxs14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4076
+ ) : /* @__PURE__ */ jsxs15(Fragment6, { children: [
4077
+ /* @__PURE__ */ jsxs15("div", { className: "block", children: [
4078
+ /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3891
4079
  "Origin ",
3892
4080
  fromLabel ? `(${fromLabel})` : "(top)"
3893
4081
  ] }),
3894
- /* @__PURE__ */ jsx18(
4082
+ /* @__PURE__ */ jsx19(
3895
4083
  "div",
3896
4084
  {
3897
4085
  role: "radiogroup",
3898
4086
  "aria-label": "Origin track",
3899
4087
  "data-testid": `${testIdPrefix}-origin-list`,
3900
4088
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3901
- children: originCandidates.map((t) => /* @__PURE__ */ jsx18(
4089
+ children: originCandidates.map((t) => /* @__PURE__ */ jsx19(
3902
4090
  CandidateRow,
3903
4091
  {
3904
4092
  track: t,
@@ -3912,23 +4100,23 @@ function CrossfadeModal({
3912
4100
  }
3913
4101
  )
3914
4102
  ] }),
3915
- /* @__PURE__ */ jsxs14("div", { className: "block", children: [
3916
- /* @__PURE__ */ jsxs14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4103
+ /* @__PURE__ */ jsxs15("div", { className: "block", children: [
4104
+ /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3917
4105
  "Target ",
3918
4106
  toLabel ? `(${toLabel})` : "(bottom)"
3919
4107
  ] }),
3920
- targetCandidates.length === 0 ? /* @__PURE__ */ jsxs14("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
4108
+ targetCandidates.length === 0 ? /* @__PURE__ */ jsxs15("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3921
4109
  "No available tracks in ",
3922
4110
  toLabel ?? "the target scene",
3923
4111
  " to crossfade into."
3924
- ] }) : /* @__PURE__ */ jsx18(
4112
+ ] }) : /* @__PURE__ */ jsx19(
3925
4113
  "div",
3926
4114
  {
3927
4115
  role: "radiogroup",
3928
4116
  "aria-label": "Target track",
3929
4117
  "data-testid": `${testIdPrefix}-target-list`,
3930
4118
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3931
- children: targetCandidates.map((t) => /* @__PURE__ */ jsx18(
4119
+ children: targetCandidates.map((t) => /* @__PURE__ */ jsx19(
3932
4120
  CandidateRow,
3933
4121
  {
3934
4122
  track: t,
@@ -3943,9 +4131,9 @@ function CrossfadeModal({
3943
4131
  )
3944
4132
  ] })
3945
4133
  ] })),
3946
- error && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3947
- /* @__PURE__ */ jsxs14("div", { className: "flex justify-end gap-2 pt-1", children: [
3948
- /* @__PURE__ */ jsx18(
4134
+ error && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
4135
+ /* @__PURE__ */ jsxs15("div", { className: "flex justify-end gap-2 pt-1", children: [
4136
+ /* @__PURE__ */ jsx19(
3949
4137
  "button",
3950
4138
  {
3951
4139
  ref: cancelRef,
@@ -3956,7 +4144,7 @@ function CrossfadeModal({
3956
4144
  children: "Cancel"
3957
4145
  }
3958
4146
  ),
3959
- /* @__PURE__ */ jsx18(
4147
+ /* @__PURE__ */ jsx19(
3960
4148
  "button",
3961
4149
  {
3962
4150
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3973,10 +4161,10 @@ function CrossfadeModal({
3973
4161
  }
3974
4162
 
3975
4163
  // src/components/TransitionDesigner.tsx
3976
- import { useCallback as useCallback9, useEffect as useEffect10, useMemo as useMemo6, useRef as useRef11, useState as useState13 } from "react";
4164
+ import { useCallback as useCallback10, useEffect as useEffect11, useMemo as useMemo6, useRef as useRef11, useState as useState14 } from "react";
3977
4165
 
3978
4166
  // src/hooks/useTrackReorder.ts
3979
- import { useCallback as useCallback8, useRef as useRef10, useState as useState12 } from "react";
4167
+ import { useCallback as useCallback9, useRef as useRef10, useState as useState13 } from "react";
3980
4168
  function moveItem(arr, from, to) {
3981
4169
  const next = arr.slice();
3982
4170
  if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
@@ -3993,12 +4181,12 @@ function useTrackReorder({
3993
4181
  getId,
3994
4182
  onError
3995
4183
  }) {
3996
- const [draggingIndex, setDraggingIndex] = useState12(null);
3997
- const [dragOverIndex, setDragOverIndex] = useState12(null);
4184
+ const [draggingIndex, setDraggingIndex] = useState13(null);
4185
+ const [dragOverIndex, setDragOverIndex] = useState13(null);
3998
4186
  const fromRef = useRef10(null);
3999
4187
  const itemsRef = useRef10(items);
4000
4188
  itemsRef.current = items;
4001
- const dragPropsFor = useCallback8(
4189
+ const dragPropsFor = useCallback9(
4002
4190
  (index) => ({
4003
4191
  handleProps: {
4004
4192
  draggable: true,
@@ -4180,7 +4368,7 @@ function dbIdsFromKeys(keys) {
4180
4368
  }
4181
4369
 
4182
4370
  // src/components/TransitionDesigner.tsx
4183
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
4371
+ import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
4184
4372
  var CROSSFADE_ESTIMATE_MS = 15e3;
4185
4373
  var FADE_ESTIMATE_MS = 11e3;
4186
4374
  var CREATE_ALL_CONCURRENCY = 5;
@@ -4207,14 +4395,14 @@ function TransitionDesigner({
4207
4395
  testIdPrefix = "transition-designer"
4208
4396
  }) {
4209
4397
  const fadeOnly = !onCreateCrossfade;
4210
- const [load, setLoad] = useState13({ status: "loading" });
4211
- const [fromName, setFromName] = useState13(null);
4212
- const [toName, setToName] = useState13(null);
4213
- const [originSlots, setOriginSlots] = useState13([]);
4214
- const [targetSlots, setTargetSlots] = useState13([]);
4215
- const [creatingKeys, setCreatingKeys] = useState13(() => /* @__PURE__ */ new Set());
4216
- const [rowErrors, setRowErrors] = useState13({});
4217
- const [rowEffects, setRowEffects] = useState13({});
4398
+ const [load, setLoad] = useState14({ status: "loading" });
4399
+ const [fromName, setFromName] = useState14(null);
4400
+ const [toName, setToName] = useState14(null);
4401
+ const [originSlots, setOriginSlots] = useState14([]);
4402
+ const [targetSlots, setTargetSlots] = useState14([]);
4403
+ const [creatingKeys, setCreatingKeys] = useState14(() => /* @__PURE__ */ new Set());
4404
+ const [rowErrors, setRowErrors] = useState14({});
4405
+ const [rowEffects, setRowEffects] = useState14({});
4218
4406
  const rowEffectsRef = useRef11(rowEffects);
4219
4407
  rowEffectsRef.current = rowEffects;
4220
4408
  const audioEffectsEnabled = !!onCreateAudioTransition;
@@ -4227,8 +4415,8 @@ function TransitionDesigner({
4227
4415
  const creatingKeysRef = useRef11(creatingKeys);
4228
4416
  creatingKeysRef.current = creatingKeys;
4229
4417
  const dragRef = useRef11(null);
4230
- const [dragging, setDragging] = useState13(null);
4231
- const [dragOver, setDragOver] = useState13(null);
4418
+ const [dragging, setDragging] = useState14(null);
4419
+ const [dragOver, setDragOver] = useState14(null);
4232
4420
  const excludeSet = useMemo6(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
4233
4421
  const originPool = useMemo6(
4234
4422
  () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
@@ -4244,7 +4432,7 @@ function TransitionDesigner({
4244
4432
  originByIdRef.current = originById;
4245
4433
  const targetByIdRef = useRef11(targetById);
4246
4434
  targetByIdRef.current = targetById;
4247
- const refresh = useCallback9(async () => {
4435
+ const refresh = useCallback10(async () => {
4248
4436
  if (!host.listSceneFamilyTracks) {
4249
4437
  setLoad({ status: "error", message: "This host does not support transition tracks." });
4250
4438
  return;
@@ -4287,10 +4475,10 @@ function TransitionDesigner({
4287
4475
  });
4288
4476
  }
4289
4477
  }, [host, fromSceneId, toSceneId, transitionSceneId, mapColumnSubjects]);
4290
- useEffect10(() => {
4478
+ useEffect11(() => {
4291
4479
  void refresh();
4292
4480
  }, [refresh]);
4293
- useEffect10(() => {
4481
+ useEffect11(() => {
4294
4482
  if (load.status !== "ready") return;
4295
4483
  const [po, pt] = padPair(
4296
4484
  reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
@@ -4299,7 +4487,7 @@ function TransitionDesigner({
4299
4487
  if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
4300
4488
  if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
4301
4489
  }, [originPool, targetPool, load.status]);
4302
- const mutate = useCallback9(
4490
+ const mutate = useCallback10(
4303
4491
  (nextOrigin, nextTarget) => {
4304
4492
  const norm = normalizeSlots(nextOrigin, nextTarget);
4305
4493
  const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
@@ -4312,7 +4500,7 @@ function TransitionDesigner({
4312
4500
  },
4313
4501
  [host, transitionSceneId]
4314
4502
  );
4315
- const setRowEffect = useCallback9(
4503
+ const setRowEffect = useCallback10(
4316
4504
  (sourceDbId, effect) => {
4317
4505
  setRowEffects((prev) => {
4318
4506
  const next = { ...prev, [sourceDbId]: effect };
@@ -4326,7 +4514,7 @@ function TransitionDesigner({
4326
4514
  },
4327
4515
  [host, transitionSceneId]
4328
4516
  );
4329
- const insertGapAbove = useCallback9(
4517
+ const insertGapAbove = useCallback10(
4330
4518
  (col, index) => {
4331
4519
  const slots = col === "origin" ? originSlots : targetSlots;
4332
4520
  const next = [...slots.slice(0, index), null, ...slots.slice(index)];
@@ -4335,7 +4523,7 @@ function TransitionDesigner({
4335
4523
  },
4336
4524
  [originSlots, targetSlots, mutate]
4337
4525
  );
4338
- const removeGap = useCallback9(
4526
+ const removeGap = useCallback10(
4339
4527
  (col, index) => {
4340
4528
  const slots = col === "origin" ? originSlots : targetSlots;
4341
4529
  const next = slots.filter((_, i) => i !== index);
@@ -4344,7 +4532,7 @@ function TransitionDesigner({
4344
4532
  },
4345
4533
  [originSlots, targetSlots, mutate]
4346
4534
  );
4347
- const handleDrop = useCallback9(
4535
+ const handleDrop = useCallback10(
4348
4536
  (col, to) => {
4349
4537
  const from = dragRef.current;
4350
4538
  dragRef.current = null;
@@ -4383,7 +4571,7 @@ function TransitionDesigner({
4383
4571
  }).length,
4384
4572
  [effectiveRows, creatingKeys]
4385
4573
  );
4386
- const createRow = useCallback9(
4574
+ const createRow = useCallback10(
4387
4575
  async (row) => {
4388
4576
  const key = rowKey(row);
4389
4577
  if (!key || !row.type || creatingKeysRef.current.has(key)) return;
@@ -4438,7 +4626,7 @@ function TransitionDesigner({
4438
4626
  },
4439
4627
  [onCreateCrossfade, onCreateFade, onCreateAudioTransition]
4440
4628
  );
4441
- const createAll = useCallback9(async () => {
4629
+ const createAll = useCallback10(async () => {
4442
4630
  const source = fadeOnly ? fadeOnlyRowsRef.current : buildRowSlots(originSlotsRef.current, targetSlotsRef.current);
4443
4631
  const eligible = source.filter((r) => {
4444
4632
  const k = rowKey(r);
@@ -4507,15 +4695,15 @@ function TransitionDesigner({
4507
4695
  const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
4508
4696
  const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
4509
4697
  if (slotId === null) {
4510
- return /* @__PURE__ */ jsxs15(
4698
+ return /* @__PURE__ */ jsxs16(
4511
4699
  "div",
4512
4700
  {
4513
4701
  ...cellDragProps(col, index, false),
4514
4702
  "data-testid": `${testIdPrefix}-${col}-gap-${index}`,
4515
4703
  className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
4516
4704
  children: [
4517
- /* @__PURE__ */ jsx19("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4518
- /* @__PURE__ */ jsx19(
4705
+ /* @__PURE__ */ jsx20("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4706
+ /* @__PURE__ */ jsx20(
4519
4707
  "button",
4520
4708
  {
4521
4709
  type: "button",
@@ -4532,7 +4720,7 @@ function TransitionDesigner({
4532
4720
  }
4533
4721
  const primary = track ? track.prompt?.trim() || track.name : slotId;
4534
4722
  const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
4535
- return /* @__PURE__ */ jsx19(
4723
+ return /* @__PURE__ */ jsx20(
4536
4724
  "div",
4537
4725
  {
4538
4726
  ...cellDragProps(col, index, locked),
@@ -4540,13 +4728,13 @@ function TransitionDesigner({
4540
4728
  "data-value": slotId,
4541
4729
  className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
4542
4730
  title: track ? track.dbId : "Track no longer available",
4543
- children: /* @__PURE__ */ jsxs15("div", { className: "flex items-start gap-1", children: [
4544
- /* @__PURE__ */ jsx19("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4545
- /* @__PURE__ */ jsxs15("div", { className: "min-w-0 flex-1", children: [
4546
- /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-text truncate", children: primary }),
4547
- meta && /* @__PURE__ */ jsx19("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4731
+ children: /* @__PURE__ */ jsxs16("div", { className: "flex items-start gap-1", children: [
4732
+ /* @__PURE__ */ jsx20("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4733
+ /* @__PURE__ */ jsxs16("div", { className: "min-w-0 flex-1", children: [
4734
+ /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-text truncate", children: primary }),
4735
+ meta && /* @__PURE__ */ jsx20("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4548
4736
  ] }),
4549
- /* @__PURE__ */ jsx19(
4737
+ /* @__PURE__ */ jsx20(
4550
4738
  "button",
4551
4739
  {
4552
4740
  type: "button",
@@ -4569,26 +4757,26 @@ function TransitionDesigner({
4569
4757
  const key = rowKey(row);
4570
4758
  const isCreatingThis = key !== null && creatingKeys.has(key);
4571
4759
  const errMsg = key !== null ? rowErrors[key] : void 0;
4572
- return /* @__PURE__ */ jsxs15(
4760
+ return /* @__PURE__ */ jsxs16(
4573
4761
  "div",
4574
4762
  {
4575
4763
  "data-testid": `${testIdPrefix}-row-${i}`,
4576
4764
  className: "grid grid-cols-[1fr_auto] gap-2 items-center",
4577
4765
  children: [
4578
- /* @__PURE__ */ jsxs15(
4766
+ /* @__PURE__ */ jsxs16(
4579
4767
  "div",
4580
4768
  {
4581
4769
  "data-testid": `${testIdPrefix}-subject-${slotId}`,
4582
4770
  className: `rounded-sm border border-sas-border bg-sas-panel px-2 py-1.5 ${isCreatingThis ? "opacity-60" : ""}`,
4583
4771
  title: track ? track.dbId : "Track no longer available",
4584
4772
  children: [
4585
- /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-text truncate", children: track ? track.prompt?.trim() || track.name : slotId }),
4586
- /* @__PURE__ */ jsx19("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing" })
4773
+ /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-text truncate", children: track ? track.prompt?.trim() || track.name : slotId }),
4774
+ /* @__PURE__ */ jsx20("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing" })
4587
4775
  ]
4588
4776
  }
4589
4777
  ),
4590
- /* @__PURE__ */ jsxs15("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4591
- /* @__PURE__ */ jsx19(
4778
+ /* @__PURE__ */ jsxs16("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4779
+ /* @__PURE__ */ jsx20(
4592
4780
  "span",
4593
4781
  {
4594
4782
  "data-testid": `${testIdPrefix}-type-${i}`,
@@ -4596,7 +4784,7 @@ function TransitionDesigner({
4596
4784
  children: row.type ? TYPE_LABEL[row.type] : "\u2014"
4597
4785
  }
4598
4786
  ),
4599
- isCreatingThis ? /* @__PURE__ */ jsx19("div", { className: "w-full", children: /* @__PURE__ */ jsx19(
4787
+ isCreatingThis ? /* @__PURE__ */ jsx20("div", { className: "w-full", children: /* @__PURE__ */ jsx20(
4600
4788
  SorceryProgressBar,
4601
4789
  {
4602
4790
  isLoading: true,
@@ -4604,7 +4792,7 @@ function TransitionDesigner({
4604
4792
  statusText: "CREATING",
4605
4793
  estimatedDurationMs: fadeEstimateMs ?? FADE_ESTIMATE_MS
4606
4794
  }
4607
- ) }) : /* @__PURE__ */ jsx19(
4795
+ ) }) : /* @__PURE__ */ jsx20(
4608
4796
  "button",
4609
4797
  {
4610
4798
  type: "button",
@@ -4614,7 +4802,7 @@ function TransitionDesigner({
4614
4802
  children: "Create"
4615
4803
  }
4616
4804
  ),
4617
- errMsg && /* @__PURE__ */ jsx19(
4805
+ errMsg && /* @__PURE__ */ jsx20(
4618
4806
  "span",
4619
4807
  {
4620
4808
  "data-testid": `${testIdPrefix}-row-error-${i}`,
@@ -4631,18 +4819,18 @@ function TransitionDesigner({
4631
4819
  if (fadeOnly) {
4632
4820
  const outRows = fadeOnlyRows.filter((r) => r.type === "fade-out");
4633
4821
  const inRows = fadeOnlyRows.filter((r) => r.type === "fade-in");
4634
- return /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4635
- /* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4636
- /* @__PURE__ */ jsxs15("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4637
- /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: fromLabel }),
4822
+ return /* @__PURE__ */ jsxs16("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4823
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4824
+ /* @__PURE__ */ jsxs16("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4825
+ /* @__PURE__ */ jsx20("span", { className: "text-sas-text", children: fromLabel }),
4638
4826
  " \u2192",
4639
4827
  " ",
4640
- /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: toLabel }),
4828
+ /* @__PURE__ */ jsx20("span", { className: "text-sas-text", children: toLabel }),
4641
4829
  familyLabel ? ` \xB7 ${familyLabel}` : "",
4642
4830
  " \xB7 each entry fades on its own \u2014 origin fades out, target fades in."
4643
4831
  ] }),
4644
- /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 shrink-0", children: [
4645
- creatingKeys.size > 0 && /* @__PURE__ */ jsxs15(
4832
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2 shrink-0", children: [
4833
+ creatingKeys.size > 0 && /* @__PURE__ */ jsxs16(
4646
4834
  "span",
4647
4835
  {
4648
4836
  className: "text-[10px] text-sas-accent whitespace-nowrap",
@@ -4653,7 +4841,7 @@ function TransitionDesigner({
4653
4841
  ]
4654
4842
  }
4655
4843
  ),
4656
- /* @__PURE__ */ jsxs15(
4844
+ /* @__PURE__ */ jsxs16(
4657
4845
  "button",
4658
4846
  {
4659
4847
  type: "button",
@@ -4670,26 +4858,26 @@ function TransitionDesigner({
4670
4858
  )
4671
4859
  ] })
4672
4860
  ] }),
4673
- load.status === "loading" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4674
- load.status === "error" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4675
- load.status === "ready" && (fadeOnlyRows.length === 0 ? /* @__PURE__ */ jsxs15("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4861
+ load.status === "loading" && /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4862
+ load.status === "error" && /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4863
+ load.status === "ready" && (fadeOnlyRows.length === 0 ? /* @__PURE__ */ jsxs16("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4676
4864
  "Nothing to fade in this panel for either scene. Add tracks to ",
4677
4865
  fromLabel,
4678
4866
  " or ",
4679
4867
  toLabel,
4680
4868
  " ",
4681
4869
  "first (or free one by deleting an existing fade)."
4682
- ] }) : /* @__PURE__ */ jsxs15("div", { className: "space-y-3", children: [
4683
- outRows.length > 0 && /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-out-section`, children: [
4684
- /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4870
+ ] }) : /* @__PURE__ */ jsxs16("div", { className: "space-y-3", children: [
4871
+ outRows.length > 0 && /* @__PURE__ */ jsxs16("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-out-section`, children: [
4872
+ /* @__PURE__ */ jsxs16("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4685
4873
  "Origin (",
4686
4874
  fromLabel,
4687
4875
  ") \u2014 fades out"
4688
4876
  ] }),
4689
4877
  outRows.map((row, i) => renderFadeOnlyRow(row, i))
4690
4878
  ] }),
4691
- inRows.length > 0 && /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-in-section`, children: [
4692
- /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4879
+ inRows.length > 0 && /* @__PURE__ */ jsxs16("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-in-section`, children: [
4880
+ /* @__PURE__ */ jsxs16("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4693
4881
  "Target (",
4694
4882
  toLabel,
4695
4883
  ") \u2014 fades in"
@@ -4699,22 +4887,22 @@ function TransitionDesigner({
4699
4887
  ] }))
4700
4888
  ] });
4701
4889
  }
4702
- return /* @__PURE__ */ jsxs15("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4703
- /* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4704
- /* @__PURE__ */ jsxs15("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4705
- /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: fromLabel }),
4890
+ return /* @__PURE__ */ jsxs16("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4891
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4892
+ /* @__PURE__ */ jsxs16("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4893
+ /* @__PURE__ */ jsx20("span", { className: "text-sas-text", children: fromLabel }),
4706
4894
  " \u2192",
4707
4895
  " ",
4708
- /* @__PURE__ */ jsx19("span", { className: "text-sas-text", children: toLabel }),
4896
+ /* @__PURE__ */ jsx20("span", { className: "text-sas-text", children: toLabel }),
4709
4897
  familyLabel ? ` \xB7 ${familyLabel}` : "",
4710
4898
  " \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
4711
4899
  ] }),
4712
- /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 shrink-0", children: [
4713
- creatingKeys.size > 0 && /* @__PURE__ */ jsxs15("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4900
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2 shrink-0", children: [
4901
+ creatingKeys.size > 0 && /* @__PURE__ */ jsxs16("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4714
4902
  creatingKeys.size,
4715
4903
  " creating\u2026"
4716
4904
  ] }),
4717
- /* @__PURE__ */ jsxs15(
4905
+ /* @__PURE__ */ jsxs16(
4718
4906
  "button",
4719
4907
  {
4720
4908
  type: "button",
@@ -4731,49 +4919,49 @@ function TransitionDesigner({
4731
4919
  )
4732
4920
  ] })
4733
4921
  ] }),
4734
- /* @__PURE__ */ jsxs15("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4735
- /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4922
+ /* @__PURE__ */ jsxs16("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4923
+ /* @__PURE__ */ jsxs16("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4736
4924
  "Origin (",
4737
4925
  fromLabel,
4738
4926
  ")"
4739
4927
  ] }),
4740
- /* @__PURE__ */ jsx19("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4741
- /* @__PURE__ */ jsxs15("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4928
+ /* @__PURE__ */ jsx20("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4929
+ /* @__PURE__ */ jsxs16("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4742
4930
  "Target (",
4743
4931
  toLabel,
4744
4932
  ")"
4745
4933
  ] })
4746
4934
  ] }),
4747
- load.status === "loading" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4748
- load.status === "error" && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4749
- load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ jsxs15("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4935
+ load.status === "loading" && /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4936
+ load.status === "error" && /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4937
+ load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ jsxs16("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4750
4938
  "No tracks to arrange in this panel for either scene. Add tracks to ",
4751
4939
  fromLabel,
4752
4940
  " or ",
4753
4941
  toLabel,
4754
4942
  " ",
4755
4943
  "first (or free one by deleting an existing crossfade/fade)."
4756
- ] }) : /* @__PURE__ */ jsx19("div", { className: "space-y-2", children: rows.map((row, i) => {
4944
+ ] }) : /* @__PURE__ */ jsx20("div", { className: "space-y-2", children: rows.map((row, i) => {
4757
4945
  const key = rowKey(row);
4758
4946
  const isCreatingThis = key !== null && creatingKeys.has(key);
4759
4947
  const errMsg = key !== null ? rowErrors[key] : void 0;
4760
- return /* @__PURE__ */ jsxs15(
4948
+ return /* @__PURE__ */ jsxs16(
4761
4949
  "div",
4762
4950
  {
4763
4951
  "data-testid": `${testIdPrefix}-row-${i}`,
4764
4952
  className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
4765
4953
  children: [
4766
4954
  renderCell("origin", i, row.originId),
4767
- /* @__PURE__ */ jsxs15("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4768
- !row.type ? /* @__PURE__ */ jsx19("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ jsx19(
4955
+ /* @__PURE__ */ jsxs16("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4956
+ !row.type ? /* @__PURE__ */ jsx20("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ jsx20(
4769
4957
  "span",
4770
4958
  {
4771
4959
  "data-testid": `${testIdPrefix}-type-${i}`,
4772
4960
  className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
4773
4961
  children: TYPE_LABEL[row.type]
4774
4962
  }
4775
- ) : audioEffectsEnabled ? /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4776
- /* @__PURE__ */ jsx19(
4963
+ ) : audioEffectsEnabled ? /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4964
+ /* @__PURE__ */ jsx20(
4777
4965
  "select",
4778
4966
  {
4779
4967
  "data-testid": `${testIdPrefix}-effect-${i}`,
@@ -4783,11 +4971,11 @@ function TransitionDesigner({
4783
4971
  if (id) setRowEffect(id, e.target.value);
4784
4972
  },
4785
4973
  className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
4786
- children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx19("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4974
+ children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx20("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4787
4975
  }
4788
4976
  ),
4789
- /* @__PURE__ */ jsx19("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4790
- ] }) : /* @__PURE__ */ jsx19(
4977
+ /* @__PURE__ */ jsx20("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4978
+ ] }) : /* @__PURE__ */ jsx20(
4791
4979
  "span",
4792
4980
  {
4793
4981
  "data-testid": `${testIdPrefix}-type-${i}`,
@@ -4795,7 +4983,7 @@ function TransitionDesigner({
4795
4983
  children: TYPE_LABEL[row.type]
4796
4984
  }
4797
4985
  ),
4798
- isCreatingThis ? /* @__PURE__ */ jsx19("div", { className: "w-full", children: /* @__PURE__ */ jsx19(
4986
+ isCreatingThis ? /* @__PURE__ */ jsx20("div", { className: "w-full", children: /* @__PURE__ */ jsx20(
4799
4987
  SorceryProgressBar,
4800
4988
  {
4801
4989
  isLoading: true,
@@ -4803,7 +4991,7 @@ function TransitionDesigner({
4803
4991
  statusText: "CREATING",
4804
4992
  estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
4805
4993
  }
4806
- ) }) : /* @__PURE__ */ jsx19(
4994
+ ) }) : /* @__PURE__ */ jsx20(
4807
4995
  "button",
4808
4996
  {
4809
4997
  type: "button",
@@ -4814,7 +5002,7 @@ function TransitionDesigner({
4814
5002
  children: "Create"
4815
5003
  }
4816
5004
  ),
4817
- errMsg && /* @__PURE__ */ jsx19(
5005
+ errMsg && /* @__PURE__ */ jsx20(
4818
5006
  "span",
4819
5007
  {
4820
5008
  "data-testid": `${testIdPrefix}-row-error-${i}`,
@@ -4832,9 +5020,38 @@ function TransitionDesigner({
4832
5020
  ] });
4833
5021
  }
4834
5022
 
5023
+ // src/components/GroupCollapseChevron.tsx
5024
+ import { ChevronDown as ChevronDown2 } from "lucide-react";
5025
+ import { jsx as jsx21 } from "react/jsx-runtime";
5026
+ function GroupCollapseChevron({
5027
+ collapsed = false,
5028
+ onToggle,
5029
+ what = "group"
5030
+ }) {
5031
+ if (!onToggle) return null;
5032
+ return /* @__PURE__ */ jsx21(
5033
+ "button",
5034
+ {
5035
+ type: "button",
5036
+ "data-testid": "sdk-group-collapse",
5037
+ onClick: onToggle,
5038
+ "aria-expanded": !collapsed,
5039
+ title: collapsed ? `Expand ${what}` : `Collapse ${what}`,
5040
+ className: "shrink-0 px-0.5 py-0.5 rounded-sm text-sas-muted hover:text-sas-accent transition-colors",
5041
+ children: /* @__PURE__ */ jsx21(
5042
+ ChevronDown2,
5043
+ {
5044
+ className: `w-3 h-3 transition-transform ${collapsed ? "-rotate-90" : ""}`,
5045
+ strokeWidth: 2.5
5046
+ }
5047
+ )
5048
+ }
5049
+ );
5050
+ }
5051
+
4835
5052
  // src/components/PanelMasterStrip.tsx
4836
- import { useMemo as useMemo7, useState as useState14 } from "react";
4837
- import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
5053
+ import { useMemo as useMemo7, useState as useState15 } from "react";
5054
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
4838
5055
  function PanelMasterStrip({
4839
5056
  bus,
4840
5057
  levels = null,
@@ -4853,7 +5070,7 @@ function PanelMasterStrip({
4853
5070
  onToggleFxEnabled,
4854
5071
  onShowFxEditor
4855
5072
  }) {
4856
- const [search, setSearch] = useState14("");
5073
+ const [search, setSearch] = useState15("");
4857
5074
  const filtered = useMemo7(() => {
4858
5075
  const q = search.trim().toLowerCase();
4859
5076
  if (!q) return availableFx;
@@ -4861,14 +5078,14 @@ function PanelMasterStrip({
4861
5078
  (fx) => fx.name.toLowerCase().includes(q) || fx.manufacturer.toLowerCase().includes(q)
4862
5079
  );
4863
5080
  }, [availableFx, search]);
4864
- return /* @__PURE__ */ jsxs16(
5081
+ return /* @__PURE__ */ jsxs17(
4865
5082
  "div",
4866
5083
  {
4867
5084
  "data-testid": "panel-master-strip",
4868
5085
  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" : ""}`,
4869
5086
  children: [
4870
- /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
4871
- /* @__PURE__ */ jsx20(
5087
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
5088
+ /* @__PURE__ */ jsx22(
4872
5089
  "span",
4873
5090
  {
4874
5091
  className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
@@ -4876,8 +5093,8 @@ function PanelMasterStrip({
4876
5093
  children: "BUS"
4877
5094
  }
4878
5095
  ),
4879
- /* @__PURE__ */ jsxs16("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
4880
- /* @__PURE__ */ jsx20(
5096
+ /* @__PURE__ */ jsxs17("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
5097
+ /* @__PURE__ */ jsx22(
4881
5098
  VolumeSlider,
4882
5099
  {
4883
5100
  value: dbToSlider(bus.volume),
@@ -4885,8 +5102,8 @@ function PanelMasterStrip({
4885
5102
  disabled
4886
5103
  }
4887
5104
  ),
4888
- /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
4889
- /* @__PURE__ */ jsx20(
5105
+ /* @__PURE__ */ jsxs17("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
5106
+ /* @__PURE__ */ jsx22(
4890
5107
  LevelMeter,
4891
5108
  {
4892
5109
  peakDb: levels?.leftDb ?? -120,
@@ -4896,7 +5113,7 @@ function PanelMasterStrip({
4896
5113
  "data-testid": "bus-meter-left"
4897
5114
  }
4898
5115
  ),
4899
- /* @__PURE__ */ jsx20(
5116
+ /* @__PURE__ */ jsx22(
4900
5117
  LevelMeter,
4901
5118
  {
4902
5119
  peakDb: levels?.rightDb ?? -120,
@@ -4907,7 +5124,7 @@ function PanelMasterStrip({
4907
5124
  )
4908
5125
  ] })
4909
5126
  ] }),
4910
- /* @__PURE__ */ jsx20(
5127
+ /* @__PURE__ */ jsx22(
4911
5128
  "button",
4912
5129
  {
4913
5130
  "data-testid": "bus-mute-button",
@@ -4918,7 +5135,7 @@ function PanelMasterStrip({
4918
5135
  children: "M"
4919
5136
  }
4920
5137
  ),
4921
- /* @__PURE__ */ jsx20(
5138
+ /* @__PURE__ */ jsx22(
4922
5139
  "button",
4923
5140
  {
4924
5141
  "data-testid": "bus-solo-button",
@@ -4929,14 +5146,14 @@ function PanelMasterStrip({
4929
5146
  children: "S"
4930
5147
  }
4931
5148
  ),
4932
- /* @__PURE__ */ jsx20("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ jsxs16(
5149
+ /* @__PURE__ */ jsx22("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ jsxs17(
4933
5150
  "span",
4934
5151
  {
4935
5152
  "data-testid": `bus-fx-chip-${fx.index}`,
4936
5153
  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"}`,
4937
5154
  title: `${fx.name}${fx.enabled ? "" : " (bypassed)"}`,
4938
5155
  children: [
4939
- /* @__PURE__ */ jsx20(
5156
+ /* @__PURE__ */ jsx22(
4940
5157
  "button",
4941
5158
  {
4942
5159
  "data-testid": `bus-fx-toggle-${fx.index}`,
@@ -4947,7 +5164,7 @@ function PanelMasterStrip({
4947
5164
  children: fx.enabled ? "\u25CF" : "\u25CB"
4948
5165
  }
4949
5166
  ),
4950
- onShowFxEditor ? /* @__PURE__ */ jsx20(
5167
+ onShowFxEditor ? /* @__PURE__ */ jsx22(
4951
5168
  "button",
4952
5169
  {
4953
5170
  "data-testid": `bus-fx-edit-${fx.index}`,
@@ -4957,8 +5174,8 @@ function PanelMasterStrip({
4957
5174
  title: `Open ${fx.name} editor`,
4958
5175
  children: fx.name
4959
5176
  }
4960
- ) : /* @__PURE__ */ jsx20("span", { className: "max-w-[80px] truncate", children: fx.name }),
4961
- /* @__PURE__ */ jsx20(
5177
+ ) : /* @__PURE__ */ jsx22("span", { className: "max-w-[80px] truncate", children: fx.name }),
5178
+ /* @__PURE__ */ jsx22(
4962
5179
  "button",
4963
5180
  {
4964
5181
  "data-testid": `bus-fx-remove-${fx.index}`,
@@ -4973,7 +5190,7 @@ function PanelMasterStrip({
4973
5190
  },
4974
5191
  `${fx.index}:${fx.pluginId}`
4975
5192
  )) }),
4976
- /* @__PURE__ */ jsx20(
5193
+ /* @__PURE__ */ jsx22(
4977
5194
  "button",
4978
5195
  {
4979
5196
  "data-testid": "bus-fx-add-button",
@@ -4985,9 +5202,9 @@ function PanelMasterStrip({
4985
5202
  }
4986
5203
  )
4987
5204
  ] }),
4988
- fxPickerOpen && /* @__PURE__ */ jsxs16("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
4989
- /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
4990
- /* @__PURE__ */ jsx20(
5205
+ fxPickerOpen && /* @__PURE__ */ jsxs17("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
5206
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
5207
+ /* @__PURE__ */ jsx22(
4991
5208
  "input",
4992
5209
  {
4993
5210
  type: "text",
@@ -4997,7 +5214,7 @@ function PanelMasterStrip({
4997
5214
  className: "sas-input flex-1 px-2 py-1 text-xs"
4998
5215
  }
4999
5216
  ),
5000
- onRefreshFx && /* @__PURE__ */ jsx20(
5217
+ onRefreshFx && /* @__PURE__ */ jsx22(
5001
5218
  "button",
5002
5219
  {
5003
5220
  onClick: () => onRefreshFx(),
@@ -5008,8 +5225,8 @@ function PanelMasterStrip({
5008
5225
  }
5009
5226
  )
5010
5227
  ] }),
5011
- fxLoading && availableFx.length === 0 ? /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs16("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
5012
- filtered.map((fx) => /* @__PURE__ */ jsxs16(
5228
+ fxLoading && availableFx.length === 0 ? /* @__PURE__ */ jsx22("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs17("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
5229
+ filtered.map((fx) => /* @__PURE__ */ jsxs17(
5013
5230
  "button",
5014
5231
  {
5015
5232
  "data-testid": `bus-fx-pick-${fx.pluginId}`,
@@ -5017,13 +5234,13 @@ function PanelMasterStrip({
5017
5234
  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",
5018
5235
  title: `${fx.name} by ${fx.manufacturer} (${fx.type.toUpperCase()})`,
5019
5236
  children: [
5020
- /* @__PURE__ */ jsx20("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
5021
- /* @__PURE__ */ jsx20("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
5237
+ /* @__PURE__ */ jsx22("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
5238
+ /* @__PURE__ */ jsx22("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
5022
5239
  ]
5023
5240
  },
5024
5241
  fx.pluginId
5025
5242
  )),
5026
- filtered.length === 0 && /* @__PURE__ */ jsx20("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
5243
+ filtered.length === 0 && /* @__PURE__ */ jsx22("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
5027
5244
  ] })
5028
5245
  ] })
5029
5246
  ]
@@ -5032,18 +5249,18 @@ function PanelMasterStrip({
5032
5249
  }
5033
5250
 
5034
5251
  // src/hooks/usePanelBus.ts
5035
- import { useCallback as useCallback10, useEffect as useEffect11, useRef as useRef12, useState as useState15 } from "react";
5252
+ import { useCallback as useCallback11, useEffect as useEffect12, useRef as useRef12, useState as useState16 } from "react";
5036
5253
  var LEVELS_POLL_MS = 66;
5037
5254
  function usePanelBus(host, activeSceneId) {
5038
5255
  const supported = typeof host.getPanelBusState === "function";
5039
- const [bus, setBus] = useState15(null);
5040
- const [levels, setLevels] = useState15(null);
5041
- const [availableFx, setAvailableFx] = useState15([]);
5042
- const [fxLoading, setFxLoading] = useState15(false);
5043
- const [fxPickerOpen, setFxPickerOpen] = useState15(false);
5256
+ const [bus, setBus] = useState16(null);
5257
+ const [levels, setLevels] = useState16(null);
5258
+ const [availableFx, setAvailableFx] = useState16([]);
5259
+ const [fxLoading, setFxLoading] = useState16(false);
5260
+ const [fxPickerOpen, setFxPickerOpen] = useState16(false);
5044
5261
  const fxLoadedRef = useRef12(false);
5045
5262
  const loadSeqRef = useRef12(0);
5046
- const reload = useCallback10(async () => {
5263
+ const reload = useCallback11(async () => {
5047
5264
  if (!supported || !activeSceneId || !host.getPanelBusState) {
5048
5265
  setBus(null);
5049
5266
  return;
@@ -5055,12 +5272,12 @@ function usePanelBus(host, activeSceneId) {
5055
5272
  } catch {
5056
5273
  }
5057
5274
  }, [host, activeSceneId, supported]);
5058
- useEffect11(() => {
5275
+ useEffect12(() => {
5059
5276
  setBus(null);
5060
5277
  setFxPickerOpen(false);
5061
5278
  void reload();
5062
5279
  }, [reload]);
5063
- useEffect11(() => {
5280
+ useEffect12(() => {
5064
5281
  if (!supported || !activeSceneId || !bus?.engaged || !host.getPanelBusLevels) {
5065
5282
  setLevels(null);
5066
5283
  return;
@@ -5082,7 +5299,7 @@ function usePanelBus(host, activeSceneId) {
5082
5299
  clearInterval(id);
5083
5300
  };
5084
5301
  }, [supported, activeSceneId, bus?.engaged, host]);
5085
- const loadFxList = useCallback10(
5302
+ const loadFxList = useCallback11(
5086
5303
  async (opts) => {
5087
5304
  if (!supported || !host.getAvailableFx) return;
5088
5305
  if (fxLoadedRef.current && !opts.force && !opts.rescan) return;
@@ -5098,14 +5315,14 @@ function usePanelBus(host, activeSceneId) {
5098
5315
  },
5099
5316
  [host, supported]
5100
5317
  );
5101
- const openPicker = useCallback10(
5318
+ const openPicker = useCallback11(
5102
5319
  (open) => {
5103
5320
  setFxPickerOpen(open);
5104
5321
  if (open) void loadFxList({});
5105
5322
  },
5106
5323
  [loadFxList]
5107
5324
  );
5108
- const mutate = useCallback10(
5325
+ const mutate = useCallback11(
5109
5326
  (fn) => {
5110
5327
  if (!fn || !activeSceneId) return;
5111
5328
  void (async () => {
@@ -5149,8 +5366,8 @@ function usePanelBus(host, activeSceneId) {
5149
5366
  }
5150
5367
 
5151
5368
  // src/components/DownloadPackButton.tsx
5152
- import { useCallback as useCallback11, useEffect as useEffect12, useState as useState16 } from "react";
5153
- import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
5369
+ import { useCallback as useCallback12, useEffect as useEffect13, useState as useState17 } from "react";
5370
+ import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
5154
5371
  function formatSize(bytes) {
5155
5372
  if (!bytes || bytes <= 0) return "";
5156
5373
  const gb = bytes / 1024 ** 3;
@@ -5166,10 +5383,10 @@ var DownloadPackButton = ({
5166
5383
  variant = "compact",
5167
5384
  onDownloadComplete
5168
5385
  }) => {
5169
- const [status, setStatus] = useState16("idle");
5170
- const [progress, setProgress] = useState16(0);
5171
- const [errorMessage, setErrorMessage] = useState16(null);
5172
- useEffect12(() => {
5386
+ const [status, setStatus] = useState17("idle");
5387
+ const [progress, setProgress] = useState17(0);
5388
+ const [errorMessage, setErrorMessage] = useState17(null);
5389
+ useEffect13(() => {
5173
5390
  const unsub = host.onSamplePackProgress(packId, (p) => {
5174
5391
  setStatus(p.status);
5175
5392
  setProgress(p.progress);
@@ -5184,7 +5401,7 @@ var DownloadPackButton = ({
5184
5401
  });
5185
5402
  return unsub;
5186
5403
  }, [host, packId, onDownloadComplete]);
5187
- const handleClick = useCallback11(async () => {
5404
+ const handleClick = useCallback12(async () => {
5188
5405
  if (status !== "idle" && status !== "error") return;
5189
5406
  try {
5190
5407
  setStatus("downloading");
@@ -5238,8 +5455,8 @@ var DownloadPackButton = ({
5238
5455
  } else {
5239
5456
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
5240
5457
  }
5241
- return /* @__PURE__ */ jsxs17("div", { children: [
5242
- /* @__PURE__ */ jsx21(
5458
+ return /* @__PURE__ */ jsxs18("div", { children: [
5459
+ /* @__PURE__ */ jsx23(
5243
5460
  "button",
5244
5461
  {
5245
5462
  "data-testid": `download-pack-button-${packId}`,
@@ -5250,12 +5467,12 @@ var DownloadPackButton = ({
5250
5467
  children: buttonLabel
5251
5468
  }
5252
5469
  ),
5253
- variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx21("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
5470
+ variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx23("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
5254
5471
  ] });
5255
5472
  };
5256
5473
 
5257
5474
  // src/components/SamplePackCTACard.tsx
5258
- import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
5475
+ import { jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
5259
5476
  var SamplePackCTACard = ({
5260
5477
  host,
5261
5478
  pack,
@@ -5263,7 +5480,7 @@ var SamplePackCTACard = ({
5263
5480
  onDownloadComplete
5264
5481
  }) => {
5265
5482
  if (status === "checking") {
5266
- return /* @__PURE__ */ jsx22(
5483
+ return /* @__PURE__ */ jsx24(
5267
5484
  "div",
5268
5485
  {
5269
5486
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -5274,16 +5491,16 @@ var SamplePackCTACard = ({
5274
5491
  }
5275
5492
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
5276
5493
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
5277
- return /* @__PURE__ */ jsxs18(
5494
+ return /* @__PURE__ */ jsxs19(
5278
5495
  "div",
5279
5496
  {
5280
5497
  "data-testid": `sample-pack-cta-${pack.packId}`,
5281
5498
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
5282
5499
  children: [
5283
- /* @__PURE__ */ jsx22("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
5284
- /* @__PURE__ */ jsx22("div", { className: "text-base text-sas-text mb-1", children: headline }),
5285
- /* @__PURE__ */ jsx22("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
5286
- /* @__PURE__ */ jsx22(
5500
+ /* @__PURE__ */ jsx24("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
5501
+ /* @__PURE__ */ jsx24("div", { className: "text-base text-sas-text mb-1", children: headline }),
5502
+ /* @__PURE__ */ jsx24("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
5503
+ /* @__PURE__ */ jsx24(
5287
5504
  DownloadPackButton,
5288
5505
  {
5289
5506
  host,
@@ -5300,7 +5517,7 @@ var SamplePackCTACard = ({
5300
5517
  };
5301
5518
 
5302
5519
  // src/components/WaveformView.tsx
5303
- import { useEffect as useEffect13, useRef as useRef13, useState as useState17 } from "react";
5520
+ import { useEffect as useEffect14, useRef as useRef13, useState as useState18 } from "react";
5304
5521
 
5305
5522
  // src/components/waveform.ts
5306
5523
  function computePeaks(audioBuffer, bins, targetSamples) {
@@ -5363,7 +5580,7 @@ function drawWaveform(canvas, peaks, options = {}) {
5363
5580
  }
5364
5581
 
5365
5582
  // src/components/WaveformView.tsx
5366
- import { jsx as jsx23 } from "react/jsx-runtime";
5583
+ import { jsx as jsx25 } from "react/jsx-runtime";
5367
5584
  var WaveformView = ({
5368
5585
  host,
5369
5586
  filePath,
@@ -5373,8 +5590,8 @@ var WaveformView = ({
5373
5590
  targetSamples
5374
5591
  }) => {
5375
5592
  const canvasRef = useRef13(null);
5376
- const [peaks, setPeaks] = useState17(null);
5377
- useEffect13(() => {
5593
+ const [peaks, setPeaks] = useState18(null);
5594
+ useEffect14(() => {
5378
5595
  let cancelled = false;
5379
5596
  let audioContext = null;
5380
5597
  (async () => {
@@ -5400,7 +5617,7 @@ var WaveformView = ({
5400
5617
  cancelled = true;
5401
5618
  };
5402
5619
  }, [host, filePath, bins, targetSamples]);
5403
- useEffect13(() => {
5620
+ useEffect14(() => {
5404
5621
  if (!peaks) return;
5405
5622
  const canvas = canvasRef.current;
5406
5623
  if (!canvas) return;
@@ -5411,7 +5628,7 @@ var WaveformView = ({
5411
5628
  observer.observe(canvas);
5412
5629
  return () => observer.disconnect();
5413
5630
  }, [peaks, fillStyle]);
5414
- return /* @__PURE__ */ jsx23(
5631
+ return /* @__PURE__ */ jsx25(
5415
5632
  "canvas",
5416
5633
  {
5417
5634
  ref: canvasRef,
@@ -5422,8 +5639,8 @@ var WaveformView = ({
5422
5639
  };
5423
5640
 
5424
5641
  // src/components/ScrollingWaveform.tsx
5425
- import { useEffect as useEffect14, useRef as useRef14 } from "react";
5426
- import { jsx as jsx24 } from "react/jsx-runtime";
5642
+ import { useEffect as useEffect15, useRef as useRef14 } from "react";
5643
+ import { jsx as jsx26 } from "react/jsx-runtime";
5427
5644
  var ScrollingWaveform = ({
5428
5645
  getPeakDb,
5429
5646
  active,
@@ -5435,7 +5652,7 @@ var ScrollingWaveform = ({
5435
5652
  const ringRef = useRef14(new Float32Array(columns));
5436
5653
  const writeIdxRef = useRef14(0);
5437
5654
  const rafRef = useRef14(null);
5438
- useEffect14(() => {
5655
+ useEffect15(() => {
5439
5656
  if (ringRef.current.length !== columns) {
5440
5657
  const next = new Float32Array(columns);
5441
5658
  const prev = ringRef.current;
@@ -5447,7 +5664,7 @@ var ScrollingWaveform = ({
5447
5664
  writeIdxRef.current = writeIdxRef.current % columns;
5448
5665
  }
5449
5666
  }, [columns]);
5450
- useEffect14(() => {
5667
+ useEffect15(() => {
5451
5668
  if (!active) {
5452
5669
  if (rafRef.current !== null) {
5453
5670
  cancelAnimationFrame(rafRef.current);
@@ -5499,7 +5716,7 @@ var ScrollingWaveform = ({
5499
5716
  }
5500
5717
  };
5501
5718
  }, [active, getPeakDb, fillStyle]);
5502
- return /* @__PURE__ */ jsx24(
5719
+ return /* @__PURE__ */ jsx26(
5503
5720
  "canvas",
5504
5721
  {
5505
5722
  ref: canvasRef,
@@ -5510,8 +5727,8 @@ var ScrollingWaveform = ({
5510
5727
  };
5511
5728
 
5512
5729
  // src/components/OffsetScrubber.tsx
5513
- import { useCallback as useCallback12, useEffect as useEffect15, useMemo as useMemo8, useRef as useRef15, useState as useState18 } from "react";
5514
- import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
5730
+ import { useCallback as useCallback13, useEffect as useEffect16, useMemo as useMemo8, useRef as useRef15, useState as useState19 } from "react";
5731
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
5515
5732
  var SLIDER_HEIGHT_PX = 28;
5516
5733
  var TICK_HEIGHT_PX = 14;
5517
5734
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -5525,9 +5742,9 @@ function OffsetScrubber({
5525
5742
  disabled = false
5526
5743
  }) {
5527
5744
  const trackRef = useRef15(null);
5528
- const [draftOffset, setDraftOffset] = useState18(offsetSamples);
5529
- const [isDragging, setIsDragging] = useState18(false);
5530
- useEffect15(() => {
5745
+ const [draftOffset, setDraftOffset] = useState19(offsetSamples);
5746
+ const [isDragging, setIsDragging] = useState19(false);
5747
+ useEffect16(() => {
5531
5748
  if (!isDragging) setDraftOffset(offsetSamples);
5532
5749
  }, [offsetSamples, isDragging]);
5533
5750
  const sampleRate = cuePoints?.sample_rate ?? 44100;
@@ -5536,14 +5753,14 @@ function OffsetScrubber({
5536
5753
  return Math.round(60 / projectBpm * sampleRate);
5537
5754
  }, [projectBpm, sampleRate]);
5538
5755
  const rangeSamples = beatsForRange * meter;
5539
- const sampleToFraction = useCallback12(
5756
+ const sampleToFraction = useCallback13(
5540
5757
  (sample) => {
5541
5758
  const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
5542
5759
  return (clamped + rangeSamples) / (2 * rangeSamples);
5543
5760
  },
5544
5761
  [rangeSamples]
5545
5762
  );
5546
- const fractionToSample = useCallback12(
5763
+ const fractionToSample = useCallback13(
5547
5764
  (fraction) => {
5548
5765
  const clamped = Math.max(0, Math.min(1, fraction));
5549
5766
  return Math.round(clamped * 2 * rangeSamples - rangeSamples);
@@ -5557,7 +5774,7 @@ function OffsetScrubber({
5557
5774
  const negatives = positives.slice(1).map((p) => -p);
5558
5775
  return [...negatives, ...positives].sort((a, b) => a - b);
5559
5776
  }, [cuePoints]);
5560
- const snapToBeat = useCallback12(
5777
+ const snapToBeat = useCallback13(
5561
5778
  (sample) => {
5562
5779
  if (snapTargets.length === 0) return sample;
5563
5780
  let best = snapTargets[0];
@@ -5573,7 +5790,7 @@ function OffsetScrubber({
5573
5790
  },
5574
5791
  [snapTargets]
5575
5792
  );
5576
- const handlePointerDown = useCallback12(
5793
+ const handlePointerDown = useCallback13(
5577
5794
  (e) => {
5578
5795
  if (disabled || !cuePoints) return;
5579
5796
  e.preventDefault();
@@ -5607,7 +5824,7 @@ function OffsetScrubber({
5607
5824
  },
5608
5825
  [disabled, cuePoints, fractionToSample, onChange, snapToBeat]
5609
5826
  );
5610
- const handleResetToZero = useCallback12(() => {
5827
+ const handleResetToZero = useCallback13(() => {
5611
5828
  if (disabled) return;
5612
5829
  setDraftOffset(0);
5613
5830
  onChange(0);
@@ -5626,9 +5843,9 @@ function OffsetScrubber({
5626
5843
  });
5627
5844
  }, [cuePoints, sampleToFraction]);
5628
5845
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
5629
- return /* @__PURE__ */ jsxs19("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
5630
- /* @__PURE__ */ jsx25("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
5631
- /* @__PURE__ */ jsxs19(
5846
+ return /* @__PURE__ */ jsxs20("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
5847
+ /* @__PURE__ */ jsx27("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
5848
+ /* @__PURE__ */ jsxs20(
5632
5849
  "div",
5633
5850
  {
5634
5851
  ref: trackRef,
@@ -5644,7 +5861,7 @@ function OffsetScrubber({
5644
5861
  "aria-valuenow": draftOffset,
5645
5862
  "aria-disabled": isDisabled,
5646
5863
  children: [
5647
- /* @__PURE__ */ jsx25(
5864
+ /* @__PURE__ */ jsx27(
5648
5865
  "div",
5649
5866
  {
5650
5867
  "aria-hidden": "true",
@@ -5652,7 +5869,7 @@ function OffsetScrubber({
5652
5869
  style: { left: "50%" }
5653
5870
  }
5654
5871
  ),
5655
- ticks.map((t) => /* @__PURE__ */ jsx25(
5872
+ ticks.map((t) => /* @__PURE__ */ jsx27(
5656
5873
  "div",
5657
5874
  {
5658
5875
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -5667,7 +5884,7 @@ function OffsetScrubber({
5667
5884
  },
5668
5885
  t.i
5669
5886
  )),
5670
- /* @__PURE__ */ jsx25(
5887
+ /* @__PURE__ */ jsx27(
5671
5888
  "div",
5672
5889
  {
5673
5890
  "data-testid": "offset-scrubber-thumb",
@@ -5684,7 +5901,7 @@ function OffsetScrubber({
5684
5901
  ]
5685
5902
  }
5686
5903
  ),
5687
- /* @__PURE__ */ jsx25(
5904
+ /* @__PURE__ */ jsx27(
5688
5905
  "span",
5689
5906
  {
5690
5907
  "data-testid": "offset-scrubber-readout",
@@ -5692,7 +5909,7 @@ function OffsetScrubber({
5692
5909
  children: formatOffset(draftOffset, sampleRate)
5693
5910
  }
5694
5911
  ),
5695
- /* @__PURE__ */ jsx25(
5912
+ /* @__PURE__ */ jsx27(
5696
5913
  "button",
5697
5914
  {
5698
5915
  type: "button",
@@ -5704,7 +5921,7 @@ function OffsetScrubber({
5704
5921
  children: "\u2316"
5705
5922
  }
5706
5923
  ),
5707
- bpmMismatch && /* @__PURE__ */ jsx25(
5924
+ bpmMismatch && /* @__PURE__ */ jsx27(
5708
5925
  "span",
5709
5926
  {
5710
5927
  "data-testid": "offset-bpm-mismatch",
@@ -5776,16 +5993,16 @@ function synthesizeCuePoints({
5776
5993
  }
5777
5994
 
5778
5995
  // src/panel-core/useGeneratorPanelCore.tsx
5779
- import { useState as useState23, useEffect as useEffect18, useCallback as useCallback16, useRef as useRef19, useMemo as useMemo10 } from "react";
5996
+ import { useState as useState24, useEffect as useEffect19, useCallback as useCallback17, useRef as useRef19, useMemo as useMemo10 } from "react";
5780
5997
 
5781
5998
  // src/hooks/useSceneState.ts
5782
- import { useState as useState19, useCallback as useCallback13, useRef as useRef16 } from "react";
5999
+ import { useState as useState20, useCallback as useCallback14, useRef as useRef16 } from "react";
5783
6000
  function useSceneState(activeSceneId, initialValue) {
5784
- const [stateMap, setStateMap] = useState19(() => /* @__PURE__ */ new Map());
6001
+ const [stateMap, setStateMap] = useState20(() => /* @__PURE__ */ new Map());
5785
6002
  const activeSceneIdRef = useRef16(activeSceneId);
5786
6003
  activeSceneIdRef.current = activeSceneId;
5787
6004
  const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
5788
- const setForCurrentScene = useCallback13((value) => {
6005
+ const setForCurrentScene = useCallback14((value) => {
5789
6006
  const sid = activeSceneIdRef.current;
5790
6007
  if (sid === null) return;
5791
6008
  setStateMap((prev) => {
@@ -5796,7 +6013,7 @@ function useSceneState(activeSceneId, initialValue) {
5796
6013
  return newMap;
5797
6014
  });
5798
6015
  }, [initialValue]);
5799
- const setForScene = useCallback13((sceneId, value) => {
6016
+ const setForScene = useCallback14((sceneId, value) => {
5800
6017
  setStateMap((prev) => {
5801
6018
  const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
5802
6019
  const next = typeof value === "function" ? value(current) : value;
@@ -5809,10 +6026,10 @@ function useSceneState(activeSceneId, initialValue) {
5809
6026
  }
5810
6027
 
5811
6028
  // src/hooks/useAnySolo.ts
5812
- import { useEffect as useEffect16, useState as useState20 } from "react";
6029
+ import { useEffect as useEffect17, useState as useState21 } from "react";
5813
6030
  function useAnySolo(host) {
5814
- const [anySolo, setAnySolo] = useState20(false);
5815
- useEffect16(() => {
6031
+ const [anySolo, setAnySolo] = useState21(false);
6032
+ useEffect17(() => {
5816
6033
  let active = true;
5817
6034
  const refresh = () => {
5818
6035
  host.isAnySoloActive().then((v) => {
@@ -5831,7 +6048,7 @@ function useAnySolo(host) {
5831
6048
  }
5832
6049
 
5833
6050
  // src/hooks/useSoundHistory.ts
5834
- import { useCallback as useCallback14, useMemo as useMemo9, useRef as useRef17, useState as useState21 } from "react";
6051
+ import { useCallback as useCallback15, useMemo as useMemo9, useRef as useRef17, useState as useState22 } from "react";
5835
6052
  var EMPTY = { entries: [], cursor: -1 };
5836
6053
  function sameDescriptor(a, b) {
5837
6054
  if (a === b) return true;
@@ -5848,9 +6065,9 @@ function useSoundHistory(applySound, opts = {}) {
5848
6065
  const onChangeRef = useRef17(opts.onChange);
5849
6066
  onChangeRef.current = opts.onChange;
5850
6067
  const dataRef = useRef17({});
5851
- const [, setVersion] = useState21(0);
5852
- const bump = useCallback14(() => setVersion((v) => v + 1), []);
5853
- const commit = useCallback14(
6068
+ const [, setVersion] = useState22(0);
6069
+ const bump = useCallback15(() => setVersion((v) => v + 1), []);
6070
+ const commit = useCallback15(
5854
6071
  (trackId, next, notify) => {
5855
6072
  dataRef.current = { ...dataRef.current, [trackId]: next };
5856
6073
  bump();
@@ -5858,7 +6075,7 @@ function useSoundHistory(applySound, opts = {}) {
5858
6075
  },
5859
6076
  [bump]
5860
6077
  );
5861
- const record = useCallback14(
6078
+ const record = useCallback15(
5862
6079
  (trackId, descriptor, label) => {
5863
6080
  const h = dataRef.current[trackId];
5864
6081
  const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
@@ -5873,7 +6090,7 @@ function useSoundHistory(applySound, opts = {}) {
5873
6090
  },
5874
6091
  [max, commit]
5875
6092
  );
5876
- const restoreTo = useCallback14(
6093
+ const restoreTo = useCallback15(
5877
6094
  async (trackId, index) => {
5878
6095
  const h = dataRef.current[trackId];
5879
6096
  if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
@@ -5883,7 +6100,7 @@ function useSoundHistory(applySound, opts = {}) {
5883
6100
  },
5884
6101
  [commit]
5885
6102
  );
5886
- const undo = useCallback14(
6103
+ const undo = useCallback15(
5887
6104
  (trackId) => {
5888
6105
  const h = dataRef.current[trackId];
5889
6106
  if (!h || h.cursor <= 0) return Promise.resolve(false);
@@ -5891,7 +6108,7 @@ function useSoundHistory(applySound, opts = {}) {
5891
6108
  },
5892
6109
  [restoreTo]
5893
6110
  );
5894
- const toggleFavorite = useCallback14(
6111
+ const toggleFavorite = useCallback15(
5895
6112
  (trackId, index) => {
5896
6113
  const h = dataRef.current[trackId];
5897
6114
  if (!h || index < 0 || index >= h.entries.length) return;
@@ -5900,7 +6117,7 @@ function useSoundHistory(applySound, opts = {}) {
5900
6117
  },
5901
6118
  [commit]
5902
6119
  );
5903
- const restore = useCallback14(
6120
+ const restore = useCallback15(
5904
6121
  (trackId, state) => {
5905
6122
  const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
5906
6123
  const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
@@ -5909,15 +6126,15 @@ function useSoundHistory(applySound, opts = {}) {
5909
6126
  },
5910
6127
  [commit]
5911
6128
  );
5912
- const list = useCallback14(
6129
+ const list = useCallback15(
5913
6130
  (trackId) => dataRef.current[trackId] ?? EMPTY,
5914
6131
  []
5915
6132
  );
5916
- const canUndo = useCallback14((trackId) => {
6133
+ const canUndo = useCallback15((trackId) => {
5917
6134
  const h = dataRef.current[trackId];
5918
6135
  return !!h && h.cursor > 0;
5919
6136
  }, []);
5920
- const clear = useCallback14(
6137
+ const clear = useCallback15(
5921
6138
  (trackId) => {
5922
6139
  if (dataRef.current[trackId]) {
5923
6140
  const next = { ...dataRef.current };
@@ -5929,7 +6146,7 @@ function useSoundHistory(applySound, opts = {}) {
5929
6146
  },
5930
6147
  [bump]
5931
6148
  );
5932
- const reset = useCallback14(() => {
6149
+ const reset = useCallback15(() => {
5933
6150
  dataRef.current = {};
5934
6151
  bump();
5935
6152
  }, [bump]);
@@ -6009,7 +6226,7 @@ function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
6009
6226
  }
6010
6227
 
6011
6228
  // src/panel-core/useTransitionOps.ts
6012
- import { useCallback as useCallback15, useEffect as useEffect17, useRef as useRef18, useState as useState22 } from "react";
6229
+ import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef18, useState as useState23 } from "react";
6013
6230
  function useTransitionOps({
6014
6231
  host,
6015
6232
  adapter,
@@ -6027,7 +6244,7 @@ function useTransitionOps({
6027
6244
  }) {
6028
6245
  const { identity } = adapter;
6029
6246
  const appliedFadeAutomationRef = useRef18(/* @__PURE__ */ new Set());
6030
- const applyCrossfadeAutomation = useCallback15(
6247
+ const applyCrossfadeAutomation = useCallback16(
6031
6248
  async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
6032
6249
  if (host.setTrackVolumeAutomation) {
6033
6250
  const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
@@ -6044,7 +6261,7 @@ function useTransitionOps({
6044
6261
  },
6045
6262
  [host]
6046
6263
  );
6047
- const applyFadeAutomation = useCallback15(
6264
+ const applyFadeAutomation = useCallback16(
6048
6265
  async (trackId, direction, bars, bpm, sliderPos, gesture) => {
6049
6266
  if (!host.setTrackVolumeAutomation) return;
6050
6267
  const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
@@ -6053,7 +6270,7 @@ function useTransitionOps({
6053
6270
  },
6054
6271
  [host]
6055
6272
  );
6056
- const copyTrackFx = useCallback15(
6273
+ const copyTrackFx = useCallback16(
6057
6274
  async (newTrackId, sourceDbId) => {
6058
6275
  if (typeof host.copyTrackFxFrom !== "function") return;
6059
6276
  try {
@@ -6070,8 +6287,8 @@ function useTransitionOps({
6070
6287
  },
6071
6288
  [host]
6072
6289
  );
6073
- const [isCreatingCrossfade, setIsCreatingCrossfade] = useState22(false);
6074
- const handleCreateCrossfade = useCallback15(
6290
+ const [isCreatingCrossfade, setIsCreatingCrossfade] = useState23(false);
6291
+ const handleCreateCrossfade = useCallback16(
6075
6292
  async (origin, target) => {
6076
6293
  const scene = activeSceneId;
6077
6294
  const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
@@ -6201,9 +6418,9 @@ function useTransitionOps({
6201
6418
  loadTracks
6202
6419
  ]
6203
6420
  );
6204
- const [isCreatingGroupFade, setIsCreatingGroupFade] = useState22(false);
6205
- const [isCreatingFade, setIsCreatingFade] = useState22(false);
6206
- const handleCreateFade = useCallback15(
6421
+ const [isCreatingGroupFade, setIsCreatingGroupFade] = useState23(false);
6422
+ const [isCreatingFade, setIsCreatingFade] = useState23(false);
6423
+ const handleCreateFade = useCallback16(
6207
6424
  async (selection, direction, gesture) => {
6208
6425
  const scene = activeSceneId;
6209
6426
  const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
@@ -6314,7 +6531,7 @@ function useTransitionOps({
6314
6531
  loadTracks
6315
6532
  ]
6316
6533
  );
6317
- const handleCrossfadeMute = useCallback15(
6534
+ const handleCrossfadeMute = useCallback16(
6318
6535
  (pair) => {
6319
6536
  const newMuted = !pair.origin.runtimeState.muted;
6320
6537
  for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
@@ -6329,7 +6546,7 @@ function useTransitionOps({
6329
6546
  },
6330
6547
  [host, setTracks]
6331
6548
  );
6332
- const handleCrossfadeSolo = useCallback15(
6549
+ const handleCrossfadeSolo = useCallback16(
6333
6550
  (pair) => {
6334
6551
  const newSolo = !pair.origin.runtimeState.solo;
6335
6552
  for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
@@ -6344,7 +6561,7 @@ function useTransitionOps({
6344
6561
  },
6345
6562
  [host, setTracks]
6346
6563
  );
6347
- const handleCrossfadeDelete = useCallback15(
6564
+ const handleCrossfadeDelete = useCallback16(
6348
6565
  async (pair) => {
6349
6566
  try {
6350
6567
  for (const member of [pair.origin, pair.target]) {
@@ -6371,7 +6588,7 @@ function useTransitionOps({
6371
6588
  [host, activeSceneId, setCrossfadePairsMeta, setTracks]
6372
6589
  );
6373
6590
  const crossfadeSliderTimers = useRef18({});
6374
- const handleCrossfadeSlider = useCallback15(
6591
+ const handleCrossfadeSlider = useCallback16(
6375
6592
  (pair, pos) => {
6376
6593
  setCrossfadePairsMeta(
6377
6594
  (prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
@@ -6404,7 +6621,7 @@ function useTransitionOps({
6404
6621
  },
6405
6622
  [host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
6406
6623
  );
6407
- const handleFadeDelete = useCallback15(
6624
+ const handleFadeDelete = useCallback16(
6408
6625
  async (fade) => {
6409
6626
  try {
6410
6627
  await host.deleteTrack(fade.track.handle.id);
@@ -6425,7 +6642,7 @@ function useTransitionOps({
6425
6642
  [host, activeSceneId, setFadesMeta, setTracks]
6426
6643
  );
6427
6644
  const fadeSliderTimers = useRef18({});
6428
- const handleFadeSlider = useCallback15(
6645
+ const handleFadeSlider = useCallback16(
6429
6646
  (fade, pos) => {
6430
6647
  setFadesMeta(
6431
6648
  (prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
@@ -6455,7 +6672,7 @@ function useTransitionOps({
6455
6672
  },
6456
6673
  [host, activeSceneId, applyFadeAutomation, setFadesMeta]
6457
6674
  );
6458
- const handleCreateVerbatimGroupFade = useCallback15(
6675
+ const handleCreateVerbatimGroupFade = useCallback16(
6459
6676
  async (subject, direction) => {
6460
6677
  const groupAdapter = adapter.transitionGroup;
6461
6678
  if (!groupAdapter) throw new Error("This panel does not support group fades.");
@@ -6571,7 +6788,7 @@ function useTransitionOps({
6571
6788
  ]
6572
6789
  );
6573
6790
  const groupFadeSliderTimers = useRef18({});
6574
- const handleGroupFadeSlider = useCallback15(
6791
+ const handleGroupFadeSlider = useCallback16(
6575
6792
  (group, pos) => {
6576
6793
  setFadesMeta(
6577
6794
  (prev) => prev.map(
@@ -6610,7 +6827,7 @@ function useTransitionOps({
6610
6827
  },
6611
6828
  [host, activeSceneId, applyFadeAutomation, setFadesMeta]
6612
6829
  );
6613
- const handleGroupFadeDelete = useCallback15(
6830
+ const handleGroupFadeDelete = useCallback16(
6614
6831
  async (group) => {
6615
6832
  const suffixes = ["fade", ...adapter.transitionGroup?.cleanupKeySuffixes ?? []];
6616
6833
  try {
@@ -6638,7 +6855,7 @@ function useTransitionOps({
6638
6855
  [host, adapter, activeSceneId, setFadesMeta, setTracks]
6639
6856
  );
6640
6857
  const lastResyncKeyRef = useRef18("");
6641
- useEffect17(() => {
6858
+ useEffect18(() => {
6642
6859
  if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
6643
6860
  return;
6644
6861
  }
@@ -6679,7 +6896,7 @@ function useTransitionOps({
6679
6896
  cancelled = true;
6680
6897
  };
6681
6898
  }, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
6682
- useEffect17(() => {
6899
+ useEffect18(() => {
6683
6900
  if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
6684
6901
  void (async () => {
6685
6902
  const mc = await host.getMusicalContext();
@@ -6717,7 +6934,7 @@ function useTransitionOps({
6717
6934
  }
6718
6935
 
6719
6936
  // src/panel-core/useGeneratorPanelCore.tsx
6720
- import { jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
6937
+ import { jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
6721
6938
  var EMPTY_PLACEHOLDERS = [];
6722
6939
  function useGeneratorPanelCore({
6723
6940
  ui,
@@ -6738,7 +6955,7 @@ function useGeneratorPanelCore({
6738
6955
  const { identity, features } = adapter;
6739
6956
  const logTag = identity.logTag;
6740
6957
  const adapterRef = useRef19(adapter);
6741
- useEffect18(() => {
6958
+ useEffect19(() => {
6742
6959
  if (adapterRef.current !== adapter) {
6743
6960
  adapterRef.current = adapter;
6744
6961
  console.warn(
@@ -6748,15 +6965,15 @@ function useGeneratorPanelCore({
6748
6965
  }, [adapter, logTag]);
6749
6966
  const supportsMeters = typeof host.getTrackLevels === "function";
6750
6967
  const trackLevels = useTrackLevels(host, isExpanded);
6751
- const [tracks, setTracks] = useState23([]);
6752
- const [isLoadingTracks, setIsLoadingTracks] = useState23(false);
6753
- const [importOpen, setImportOpen] = useState23(false);
6754
- const [soundImportTarget, setSoundImportTarget] = useState23(null);
6755
- const [designerView, setDesignerView] = useState23(false);
6756
- const [transitionSourceTotal, setTransitionSourceTotal] = useState23(0);
6757
- const [crossfadePairsMeta, setCrossfadePairsMeta] = useState23([]);
6758
- const [fadesMeta, setFadesMeta] = useState23([]);
6759
- const [genericGroupMetas, setGenericGroupMetas] = useState23({});
6968
+ const [tracks, setTracks] = useState24([]);
6969
+ const [isLoadingTracks, setIsLoadingTracks] = useState24(false);
6970
+ const [importOpen, setImportOpen] = useState24(false);
6971
+ const [soundImportTarget, setSoundImportTarget] = useState24(null);
6972
+ const [designerView, setDesignerView] = useState24(false);
6973
+ const [transitionSourceTotal, setTransitionSourceTotal] = useState24(0);
6974
+ const [crossfadePairsMeta, setCrossfadePairsMeta] = useState24([]);
6975
+ const [fadesMeta, setFadesMeta] = useState24([]);
6976
+ const [genericGroupMetas, setGenericGroupMetas] = useState24({});
6760
6977
  const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
6761
6978
  const [placeholders, , setPlaceholdersForScene] = useSceneState(
6762
6979
  activeSceneId,
@@ -6764,11 +6981,11 @@ function useGeneratorPanelCore({
6764
6981
  );
6765
6982
  const saveTimeoutRefs = useRef19({});
6766
6983
  const editLoadStartedRef = useRef19(/* @__PURE__ */ new Set());
6767
- const [availableInstruments, setAvailableInstruments] = useState23([]);
6768
- const [instrumentsLoading, setInstrumentsLoading] = useState23(false);
6984
+ const [availableInstruments, setAvailableInstruments] = useState24([]);
6985
+ const [instrumentsLoading, setInstrumentsLoading] = useState24(false);
6769
6986
  const engineToDbIdRef = useRef19(/* @__PURE__ */ new Map());
6770
6987
  const tracksLoadedForSceneRef = useRef19(null);
6771
- const persistSoundHistory = useCallback16(
6988
+ const persistSoundHistory = useCallback17(
6772
6989
  (trackId, state) => {
6773
6990
  if (!activeSceneId) return;
6774
6991
  const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
@@ -6788,7 +7005,7 @@ function useGeneratorPanelCore({
6788
7005
  setItems: setTracks,
6789
7006
  getId: (t) => t.handle.dbId
6790
7007
  });
6791
- const loadTracks = useCallback16(
7008
+ const loadTracks = useCallback17(
6792
7009
  async (incremental = false) => {
6793
7010
  const sceneAtStart = activeSceneId;
6794
7011
  if (!sceneAtStart) {
@@ -6913,10 +7130,10 @@ function useGeneratorPanelCore({
6913
7130
  },
6914
7131
  [host, activeSceneId, soundHistory, adapter, logTag]
6915
7132
  );
6916
- useEffect18(() => {
7133
+ useEffect19(() => {
6917
7134
  loadTracks();
6918
7135
  }, [loadTracks]);
6919
- useEffect18(() => {
7136
+ useEffect19(() => {
6920
7137
  const map = /* @__PURE__ */ new Map();
6921
7138
  for (const t of tracks) {
6922
7139
  map.set(t.handle.id, t.handle.dbId);
@@ -6924,7 +7141,7 @@ function useGeneratorPanelCore({
6924
7141
  engineToDbIdRef.current = map;
6925
7142
  }, [tracks]);
6926
7143
  const loadedCompletedIdsRef = useRef19(/* @__PURE__ */ new Set());
6927
- useEffect18(() => {
7144
+ useEffect19(() => {
6928
7145
  if (placeholders.length === 0) {
6929
7146
  loadedCompletedIdsRef.current.clear();
6930
7147
  return;
@@ -6943,16 +7160,16 @@ function useGeneratorPanelCore({
6943
7160
  loadTracks(true);
6944
7161
  }
6945
7162
  }, [placeholders, loadTracks, logTag]);
6946
- const adoptAndLoad = useCallback16(() => {
7163
+ const adoptAndLoad = useCallback17(() => {
6947
7164
  loadTracks(true);
6948
7165
  }, [loadTracks]);
6949
- useEffect18(() => {
7166
+ useEffect19(() => {
6950
7167
  const unsub = host.onEngineReady(() => {
6951
7168
  adoptAndLoad();
6952
7169
  });
6953
7170
  return unsub;
6954
7171
  }, [host, adoptAndLoad]);
6955
- useEffect18(() => {
7172
+ useEffect19(() => {
6956
7173
  if (typeof host.onAfterAgentMutation !== "function") return;
6957
7174
  let timer = null;
6958
7175
  const unsub = host.onAfterAgentMutation(() => {
@@ -6967,13 +7184,13 @@ function useGeneratorPanelCore({
6967
7184
  if (timer) clearTimeout(timer);
6968
7185
  };
6969
7186
  }, [host, loadTracks]);
6970
- useEffect18(() => {
7187
+ useEffect19(() => {
6971
7188
  const unsub = host.onTrackStateChange((trackId, state) => {
6972
7189
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
6973
7190
  });
6974
7191
  return unsub;
6975
7192
  }, [host]);
6976
- useEffect18(() => {
7193
+ useEffect19(() => {
6977
7194
  if (!features.bulkComposePlaceholders) return;
6978
7195
  console.log(`[${logTag}] Subscribing to composeProgress`);
6979
7196
  const unsub = host.onComposeProgress((event) => {
@@ -7007,7 +7224,7 @@ function useGeneratorPanelCore({
7007
7224
  });
7008
7225
  return unsub;
7009
7226
  }, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
7010
- useEffect18(() => {
7227
+ useEffect19(() => {
7011
7228
  const refs = saveTimeoutRefs;
7012
7229
  return () => {
7013
7230
  for (const timeout of Object.values(refs.current)) {
@@ -7016,8 +7233,8 @@ function useGeneratorPanelCore({
7016
7233
  };
7017
7234
  }, []);
7018
7235
  const isAddingTrackRef = useRef19(false);
7019
- const [isAddingTrack, setIsAddingTrack] = useState23(false);
7020
- const handleAddTrack = useCallback16(async () => {
7236
+ const [isAddingTrack, setIsAddingTrack] = useState24(false);
7237
+ const handleAddTrack = useCallback17(async () => {
7021
7238
  if (isAddingTrackRef.current) return;
7022
7239
  if (!activeSceneId) {
7023
7240
  host.showToast("warning", "Select SCENE");
@@ -7065,7 +7282,7 @@ function useGeneratorPanelCore({
7065
7282
  setIsAddingTrack(false);
7066
7283
  }
7067
7284
  }, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf, loadTracks, logTag]);
7068
- const handlePortTrack = useCallback16(
7285
+ const handlePortTrack = useCallback17(
7069
7286
  async (sel) => {
7070
7287
  if (!activeSceneId) {
7071
7288
  host.showToast("warning", "Select SCENE");
@@ -7128,39 +7345,8 @@ function useGeneratorPanelCore({
7128
7345
  },
7129
7346
  [host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
7130
7347
  );
7131
- const handleSoundImportPick = useCallback16(
7132
- async (sel) => {
7133
- const target = soundImportTarget;
7134
- if (!target || !host.getTrackSound) {
7135
- setSoundImportTarget(null);
7136
- return;
7137
- }
7138
- const noun = adapter.sound.importNoun;
7139
- const nounTitle = noun.charAt(0).toUpperCase() + noun.slice(1);
7140
- try {
7141
- const snap = await host.getTrackSound(sel.sourceTrackDbId);
7142
- if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) {
7143
- host.showToast(
7144
- "error",
7145
- `No ${noun} to import`,
7146
- `${sel.trackName} has no ${identity.familyKey} ${noun}.`
7147
- );
7148
- return;
7149
- }
7150
- const descriptor = adapter.sound.descriptorFromSnapshot(snap);
7151
- await adapter.sound.applySound(target.handle.id, descriptor);
7152
- soundHistory.record(target.handle.id, descriptor, snap.label);
7153
- host.showToast("success", `${nounTitle} imported`, `${snap.label} \u2192 ${target.handle.name}`);
7154
- } catch (err) {
7155
- host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
7156
- } finally {
7157
- setSoundImportTarget(null);
7158
- }
7159
- },
7160
- [soundImportTarget, host, adapter, identity.familyKey, soundHistory]
7161
- );
7162
- const [isExportingMidi, setIsExportingMidi] = useState23(false);
7163
- const handleExportMidi = useCallback16(async () => {
7348
+ const [isExportingMidi, setIsExportingMidi] = useState24(false);
7349
+ const handleExportMidi = useCallback17(async () => {
7164
7350
  if (isExportingMidi) return;
7165
7351
  setIsExportingMidi(true);
7166
7352
  try {
@@ -7191,10 +7377,10 @@ function useGeneratorPanelCore({
7191
7377
  const xfFromId = sceneContext?.transitionFromSceneId ?? null;
7192
7378
  const xfToId = sceneContext?.transitionToSceneId ?? null;
7193
7379
  const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
7194
- useEffect18(() => {
7380
+ useEffect19(() => {
7195
7381
  if (!canCrossfade) setDesignerView(false);
7196
7382
  }, [canCrossfade]);
7197
- useEffect18(() => {
7383
+ useEffect19(() => {
7198
7384
  if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
7199
7385
  setTransitionSourceTotal(0);
7200
7386
  return;
@@ -7210,12 +7396,12 @@ function useGeneratorPanelCore({
7210
7396
  };
7211
7397
  }, [canCrossfade, xfFromId, xfToId, host]);
7212
7398
  const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
7213
- useEffect18(() => {
7399
+ useEffect19(() => {
7214
7400
  if (!onHeaderContent) return;
7215
7401
  const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
7216
7402
  onHeaderContent(
7217
- /* @__PURE__ */ jsxs20("div", { className: "flex gap-1 items-center", children: [
7218
- features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx26(
7403
+ /* @__PURE__ */ jsxs21("div", { className: "flex gap-1 items-center", children: [
7404
+ features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx28(
7219
7405
  "button",
7220
7406
  {
7221
7407
  "data-testid": `import-from-scene-${identity.familyKey}-button`,
@@ -7229,7 +7415,7 @@ function useGeneratorPanelCore({
7229
7415
  children: identity.importTrackLabel ?? "Import Track"
7230
7416
  }
7231
7417
  ),
7232
- (!canCrossfade || !designerView) && /* @__PURE__ */ jsx26(
7418
+ (!canCrossfade || !designerView) && /* @__PURE__ */ jsx28(
7233
7419
  "button",
7234
7420
  {
7235
7421
  "data-testid": `add-${identity.familyKey}-track-button`,
@@ -7245,7 +7431,7 @@ function useGeneratorPanelCore({
7245
7431
  children: identity.addTrackLabel ?? "Add Track"
7246
7432
  }
7247
7433
  ),
7248
- canCrossfade && /* @__PURE__ */ jsxs20(
7434
+ canCrossfade && /* @__PURE__ */ jsxs21(
7249
7435
  "button",
7250
7436
  {
7251
7437
  "data-testid": `${identity.familyKey}-view-toggle`,
@@ -7264,7 +7450,7 @@ function useGeneratorPanelCore({
7264
7450
  title: designerView ? "Back to the track list" : "Open the transition designer",
7265
7451
  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",
7266
7452
  children: [
7267
- transitionSourceTotal > 0 && /* @__PURE__ */ jsx26(
7453
+ transitionSourceTotal > 0 && /* @__PURE__ */ jsx28(
7268
7454
  "span",
7269
7455
  {
7270
7456
  className: "absolute inset-y-0 left-0 bg-sas-accent/25",
@@ -7272,7 +7458,7 @@ function useGeneratorPanelCore({
7272
7458
  "aria-hidden": true
7273
7459
  }
7274
7460
  ),
7275
- /* @__PURE__ */ jsxs20("span", { className: "relative", children: [
7461
+ /* @__PURE__ */ jsxs21("span", { className: "relative", children: [
7276
7462
  "\u21C4 ",
7277
7463
  designerView ? "Transition" : "Tracks",
7278
7464
  transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
@@ -7303,7 +7489,7 @@ function useGeneratorPanelCore({
7303
7489
  identity,
7304
7490
  features.importTracks
7305
7491
  ]);
7306
- useEffect18(() => {
7492
+ useEffect19(() => {
7307
7493
  if (!onLoading) return;
7308
7494
  const anyGenerating = tracks.some((t) => t.isGenerating);
7309
7495
  onLoading(isLoadingTracks || anyGenerating || isBulkActive);
@@ -7311,7 +7497,7 @@ function useGeneratorPanelCore({
7311
7497
  onLoading(false);
7312
7498
  };
7313
7499
  }, [onLoading, isLoadingTracks, tracks, isBulkActive]);
7314
- const handleDeleteTrack = useCallback16(
7500
+ const handleDeleteTrack = useCallback17(
7315
7501
  async (trackId) => {
7316
7502
  try {
7317
7503
  await host.deleteTrack(trackId);
@@ -7327,7 +7513,7 @@ function useGeneratorPanelCore({
7327
7513
  },
7328
7514
  [host, activeSceneId]
7329
7515
  );
7330
- const handlePromptChange = useCallback16(
7516
+ const handlePromptChange = useCallback17(
7331
7517
  (trackId, prompt) => {
7332
7518
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
7333
7519
  const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
@@ -7364,11 +7550,11 @@ function useGeneratorPanelCore({
7364
7550
  }
7365
7551
  return s;
7366
7552
  }, [resolvedGenericGroups]);
7367
- const engineToDbId = useCallback16(
7553
+ const engineToDbId = useCallback17(
7368
7554
  (trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
7369
7555
  []
7370
7556
  );
7371
- const updateTrack = useCallback16(
7557
+ const updateTrack = useCallback17(
7372
7558
  (trackId, patch) => {
7373
7559
  setTracks(
7374
7560
  (prev) => prev.map(
@@ -7378,18 +7564,18 @@ function useGeneratorPanelCore({
7378
7564
  },
7379
7565
  []
7380
7566
  );
7381
- const markEditLoaded = useCallback16((trackId) => {
7567
+ const markEditLoaded = useCallback17((trackId) => {
7382
7568
  editLoadStartedRef.current.add(trackId);
7383
7569
  }, []);
7384
7570
  const tracksRef = useRef19(tracks);
7385
- useEffect18(() => {
7571
+ useEffect19(() => {
7386
7572
  tracksRef.current = tracks;
7387
7573
  }, [tracks]);
7388
7574
  const resolvedGenericGroupsRef = useRef19(resolvedGenericGroups);
7389
- useEffect18(() => {
7575
+ useEffect19(() => {
7390
7576
  resolvedGenericGroupsRef.current = resolvedGenericGroups;
7391
7577
  }, [resolvedGenericGroups]);
7392
- const makeServices = useCallback16(() => {
7578
+ const makeServices = useCallback17(() => {
7393
7579
  return {
7394
7580
  host,
7395
7581
  activeSceneId,
@@ -7405,10 +7591,164 @@ function useGeneratorPanelCore({
7405
7591
  name: `${identity.trackNamePrefix}-${Date.now()}${nameSuffix}`,
7406
7592
  ...adapter.createTrackOptions()
7407
7593
  }),
7408
- resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
7594
+ resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? [],
7595
+ sound: adapter.sound
7409
7596
  };
7410
7597
  }, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
7411
- const handleGenerate = useCallback16(
7598
+ const broadcastSoundFromTrack = useCallback17(
7599
+ async (sourceTrackId, descriptor, label) => {
7600
+ const targetsOf = adapter.sound.broadcastTargets;
7601
+ if (!targetsOf) return;
7602
+ const source = tracksRef.current.find((t) => t.handle.id === sourceTrackId);
7603
+ if (!source) return;
7604
+ let targets;
7605
+ try {
7606
+ targets = await targetsOf(source, makeServices());
7607
+ } catch (err) {
7608
+ console.warn(`[${logTag}] broadcastTargets failed:`, err);
7609
+ return;
7610
+ }
7611
+ const sourceInstrument = source.instrumentPluginId ?? null;
7612
+ const others = (targets ?? []).filter((t) => {
7613
+ if (t.engineId === sourceTrackId) return false;
7614
+ const live = tracksRef.current.find((x) => x.handle.id === t.engineId);
7615
+ return (live?.instrumentPluginId ?? null) === sourceInstrument;
7616
+ });
7617
+ if (others.length === 0) return;
7618
+ try {
7619
+ await adapter.sound.persistDescriptor?.(sourceTrackId, descriptor, label);
7620
+ } catch {
7621
+ }
7622
+ const appliedIds = /* @__PURE__ */ new Set();
7623
+ const failed = [];
7624
+ for (const target of others) {
7625
+ try {
7626
+ if (soundHistory.list(target.engineId).entries.length === 0) {
7627
+ try {
7628
+ const cap = await adapter.sound.captureSoundDescriptor(target.engineId);
7629
+ if (cap) {
7630
+ soundHistory.record(target.engineId, cap.descriptor, adapter.sound.previousSoundLabel);
7631
+ }
7632
+ } catch {
7633
+ }
7634
+ }
7635
+ await adapter.sound.applySound(target.engineId, descriptor);
7636
+ try {
7637
+ await adapter.sound.persistDescriptor?.(target.engineId, descriptor, label);
7638
+ } catch {
7639
+ }
7640
+ soundHistory.record(target.engineId, descriptor, label);
7641
+ appliedIds.add(target.engineId);
7642
+ } catch (err) {
7643
+ failed.push(target.label ?? target.engineId);
7644
+ console.warn(`[${logTag}] Linked sound apply failed for ${target.engineId}:`, err);
7645
+ }
7646
+ }
7647
+ if (appliedIds.size > 0) {
7648
+ setTracks(
7649
+ (prev) => prev.map(
7650
+ (t) => appliedIds.has(t.handle.id) ? { ...t, shuffleHistory: /* @__PURE__ */ new Set([...t.shuffleHistory, label]) } : t
7651
+ )
7652
+ );
7653
+ }
7654
+ if (failed.length > 0) {
7655
+ host.showToast(
7656
+ "warning",
7657
+ "Linked sound applied to some parts only",
7658
+ `Skipped: ${failed.join(", ")}`
7659
+ );
7660
+ }
7661
+ },
7662
+ [adapter, makeServices, soundHistory, host, logTag]
7663
+ );
7664
+ const broadcastInstrumentFromTrack = useCallback17(
7665
+ async (sourceTrackId, pluginId) => {
7666
+ const targetsOf = adapter.sound.broadcastTargets;
7667
+ if (!targetsOf) return;
7668
+ const source = tracksRef.current.find((t) => t.handle.id === sourceTrackId);
7669
+ if (!source) return;
7670
+ let targets;
7671
+ try {
7672
+ targets = await targetsOf(source, makeServices());
7673
+ } catch (err) {
7674
+ console.warn(`[${logTag}] broadcastTargets failed:`, err);
7675
+ return;
7676
+ }
7677
+ const others = (targets ?? []).filter((t) => t.engineId !== sourceTrackId);
7678
+ if (others.length === 0) return;
7679
+ const failed = [];
7680
+ for (const target of others) {
7681
+ try {
7682
+ await host.setTrackInstrument(target.engineId, pluginId);
7683
+ const descriptor = await host.getTrackInstrument(target.engineId);
7684
+ setTracks(
7685
+ (prev) => prev.map(
7686
+ (t) => t.handle.id === target.engineId ? {
7687
+ ...t,
7688
+ instrumentPluginId: descriptor?.pluginId ?? null,
7689
+ instrumentName: descriptor?.name ?? null,
7690
+ instrumentMissing: descriptor?.missing ?? false
7691
+ } : t
7692
+ )
7693
+ );
7694
+ } catch (err) {
7695
+ failed.push(target.label ?? target.engineId);
7696
+ console.warn(`[${logTag}] Linked instrument apply failed for ${target.engineId}:`, err);
7697
+ }
7698
+ }
7699
+ if (failed.length > 0) {
7700
+ host.showToast(
7701
+ "warning",
7702
+ "Instrument applied to some parts only",
7703
+ `Skipped: ${failed.join(", ")}`
7704
+ );
7705
+ }
7706
+ },
7707
+ [adapter, makeServices, host, logTag]
7708
+ );
7709
+ const handleRestoreSound = useCallback17(
7710
+ async (trackId, index) => {
7711
+ const entry = soundHistory.list(trackId).entries[index];
7712
+ const moved = await soundHistory.restoreTo(trackId, index);
7713
+ if (moved && entry) {
7714
+ await broadcastSoundFromTrack(trackId, entry.descriptor, entry.label);
7715
+ }
7716
+ },
7717
+ [soundHistory, broadcastSoundFromTrack]
7718
+ );
7719
+ const handleSoundImportPick = useCallback17(
7720
+ async (sel) => {
7721
+ const target = soundImportTarget;
7722
+ if (!target || !host.getTrackSound) {
7723
+ setSoundImportTarget(null);
7724
+ return;
7725
+ }
7726
+ const noun = adapter.sound.importNoun;
7727
+ const nounTitle = noun.charAt(0).toUpperCase() + noun.slice(1);
7728
+ try {
7729
+ const snap = await host.getTrackSound(sel.sourceTrackDbId);
7730
+ if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) {
7731
+ host.showToast(
7732
+ "error",
7733
+ `No ${noun} to import`,
7734
+ `${sel.trackName} has no ${identity.familyKey} ${noun}.`
7735
+ );
7736
+ return;
7737
+ }
7738
+ const descriptor = adapter.sound.descriptorFromSnapshot(snap);
7739
+ await adapter.sound.applySound(target.handle.id, descriptor);
7740
+ soundHistory.record(target.handle.id, descriptor, snap.label);
7741
+ host.showToast("success", `${nounTitle} imported`, `${snap.label} \u2192 ${target.handle.name}`);
7742
+ await broadcastSoundFromTrack(target.handle.id, descriptor, snap.label);
7743
+ } catch (err) {
7744
+ host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
7745
+ } finally {
7746
+ setSoundImportTarget(null);
7747
+ }
7748
+ },
7749
+ [soundImportTarget, host, adapter, identity.familyKey, soundHistory, broadcastSoundFromTrack]
7750
+ );
7751
+ const handleGenerate = useCallback17(
7412
7752
  async (trackId) => {
7413
7753
  const track = tracks.find((t) => t.handle.id === trackId);
7414
7754
  if (!track || !track.prompt.trim()) return;
@@ -7435,7 +7775,7 @@ function useGeneratorPanelCore({
7435
7775
  },
7436
7776
  [host, adapter, tracks, isAuthenticated, makeServices]
7437
7777
  );
7438
- const handleMuteToggle = useCallback16(
7778
+ const handleMuteToggle = useCallback17(
7439
7779
  (trackId) => {
7440
7780
  const track = tracks.find((t) => t.handle.id === trackId);
7441
7781
  if (!track) return;
@@ -7455,7 +7795,7 @@ function useGeneratorPanelCore({
7455
7795
  },
7456
7796
  [host, tracks]
7457
7797
  );
7458
- const handleSoloToggle = useCallback16(
7798
+ const handleSoloToggle = useCallback17(
7459
7799
  (trackId) => {
7460
7800
  const track = tracks.find((t) => t.handle.id === trackId);
7461
7801
  if (!track) return;
@@ -7475,7 +7815,7 @@ function useGeneratorPanelCore({
7475
7815
  },
7476
7816
  [host, tracks]
7477
7817
  );
7478
- const handleVolumeChange = useCallback16(
7818
+ const handleVolumeChange = useCallback17(
7479
7819
  (trackId, volume) => {
7480
7820
  setTracks(
7481
7821
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
@@ -7485,7 +7825,7 @@ function useGeneratorPanelCore({
7485
7825
  },
7486
7826
  [host]
7487
7827
  );
7488
- const handlePanChange = useCallback16(
7828
+ const handlePanChange = useCallback17(
7489
7829
  (trackId, pan) => {
7490
7830
  setTracks(
7491
7831
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
@@ -7495,7 +7835,7 @@ function useGeneratorPanelCore({
7495
7835
  },
7496
7836
  [host]
7497
7837
  );
7498
- const handleShuffle = useCallback16(
7838
+ const handleShuffle = useCallback17(
7499
7839
  async (trackId) => {
7500
7840
  const track = tracks.find((t) => t.handle.id === trackId);
7501
7841
  if (!track) return;
@@ -7526,7 +7866,10 @@ function useGeneratorPanelCore({
7526
7866
  );
7527
7867
  try {
7528
7868
  const cap = await adapter.sound.captureSoundDescriptor(trackId);
7529
- if (cap) soundHistory.record(trackId, cap.descriptor, result.appliedName);
7869
+ if (cap) {
7870
+ soundHistory.record(trackId, cap.descriptor, result.appliedName);
7871
+ await broadcastSoundFromTrack(trackId, cap.descriptor, result.appliedName);
7872
+ }
7530
7873
  } catch {
7531
7874
  }
7532
7875
  console.log(`[${logTag}] Sound shuffled: ${result.appliedName} (history ${nextHistory.size})`);
@@ -7535,9 +7878,9 @@ function useGeneratorPanelCore({
7535
7878
  host.showToast("error", "Shuffle failed", msg);
7536
7879
  }
7537
7880
  },
7538
- [host, adapter, tracks, soundHistory, logTag]
7881
+ [host, adapter, tracks, soundHistory, logTag, broadcastSoundFromTrack]
7539
7882
  );
7540
- const handleCopy = useCallback16(
7883
+ const handleCopy = useCallback17(
7541
7884
  async (trackId) => {
7542
7885
  try {
7543
7886
  const newHandle = await host.duplicateTrack(trackId);
@@ -7550,7 +7893,7 @@ function useGeneratorPanelCore({
7550
7893
  },
7551
7894
  [host, loadTracks]
7552
7895
  );
7553
- const handleFxToggle = useCallback16(
7896
+ const handleFxToggle = useCallback17(
7554
7897
  (trackId, category, enabled) => {
7555
7898
  setTracks(
7556
7899
  (prev) => prev.map(
@@ -7573,7 +7916,7 @@ function useGeneratorPanelCore({
7573
7916
  },
7574
7917
  [host]
7575
7918
  );
7576
- const handleFxPresetChange = useCallback16(
7919
+ const handleFxPresetChange = useCallback17(
7577
7920
  (trackId, category, presetIndex) => {
7578
7921
  setTracks(
7579
7922
  (prev) => prev.map(
@@ -7599,7 +7942,7 @@ function useGeneratorPanelCore({
7599
7942
  },
7600
7943
  [host]
7601
7944
  );
7602
- const handleFxDryWetChange = useCallback16(
7945
+ const handleFxDryWetChange = useCallback17(
7603
7946
  (trackId, category, value) => {
7604
7947
  setTracks(
7605
7948
  (prev) => prev.map(
@@ -7611,7 +7954,7 @@ function useGeneratorPanelCore({
7611
7954
  },
7612
7955
  [host]
7613
7956
  );
7614
- const toggleFxDrawer = useCallback16(
7957
+ const toggleFxDrawer = useCallback17(
7615
7958
  (trackId) => {
7616
7959
  setTracks(
7617
7960
  (prev) => prev.map((t) => {
@@ -7633,7 +7976,7 @@ function useGeneratorPanelCore({
7633
7976
  },
7634
7977
  [host, tracks]
7635
7978
  );
7636
- const loadEditNotes = useCallback16(
7979
+ const loadEditNotes = useCallback17(
7637
7980
  async (trackId) => {
7638
7981
  try {
7639
7982
  const mc = await host.getMusicalContext();
@@ -7651,7 +7994,7 @@ function useGeneratorPanelCore({
7651
7994
  },
7652
7995
  [host, logTag]
7653
7996
  );
7654
- const handleNotesChange = useCallback16(
7997
+ const handleNotesChange = useCallback17(
7655
7998
  (trackId, notes) => {
7656
7999
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
7657
8000
  const key = `edit:${trackId}`;
@@ -7681,7 +8024,7 @@ function useGeneratorPanelCore({
7681
8024
  },
7682
8025
  [host]
7683
8026
  );
7684
- const handleTabChange = useCallback16(
8027
+ const handleTabChange = useCallback17(
7685
8028
  (trackId, tab) => {
7686
8029
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
7687
8030
  if (tab === "fx") {
@@ -7706,10 +8049,10 @@ function useGeneratorPanelCore({
7706
8049
  },
7707
8050
  [host, availableInstruments.length, instrumentsLoading, loadEditNotes]
7708
8051
  );
7709
- const handleProgressChange = useCallback16((trackId, pct) => {
8052
+ const handleProgressChange = useCallback17((trackId, pct) => {
7710
8053
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
7711
8054
  }, []);
7712
- const handleToggleDrawer = useCallback16((trackId) => {
8055
+ const handleToggleDrawer = useCallback17((trackId) => {
7713
8056
  setTracks(
7714
8057
  (prev) => prev.map((t) => {
7715
8058
  if (t.handle.id !== trackId) return t;
@@ -7718,7 +8061,7 @@ function useGeneratorPanelCore({
7718
8061
  })
7719
8062
  );
7720
8063
  }, []);
7721
- const handleInstrumentSelect = useCallback16(
8064
+ const handleInstrumentSelect = useCallback17(
7722
8065
  async (trackId, pluginId) => {
7723
8066
  const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
7724
8067
  if (isDefaultInstrument) {
@@ -7738,6 +8081,7 @@ function useGeneratorPanelCore({
7738
8081
  } : t
7739
8082
  )
7740
8083
  );
8084
+ await broadcastInstrumentFromTrack(trackId, pluginId);
7741
8085
  } catch (err) {
7742
8086
  const msg = err instanceof Error ? err.message : "Failed to load instrument";
7743
8087
  host.showToast("error", "Instrument load failed", msg);
@@ -7760,6 +8104,7 @@ function useGeneratorPanelCore({
7760
8104
  } : t
7761
8105
  )
7762
8106
  );
8107
+ await broadcastInstrumentFromTrack(trackId, pluginId);
7763
8108
  } catch (err) {
7764
8109
  const msg = err instanceof Error ? err.message : "Failed to load instrument";
7765
8110
  console.error(`[${logTag}] Failed to set instrument:`, err);
@@ -7769,9 +8114,9 @@ function useGeneratorPanelCore({
7769
8114
  );
7770
8115
  }
7771
8116
  },
7772
- [host, identity.defaultInstrumentPluginId, logTag]
8117
+ [host, identity.defaultInstrumentPluginId, logTag, broadcastInstrumentFromTrack]
7773
8118
  );
7774
- const handleShowEditor = useCallback16(
8119
+ const handleShowEditor = useCallback17(
7775
8120
  async (trackId) => {
7776
8121
  try {
7777
8122
  await host.showInstrumentEditor(trackId);
@@ -7782,12 +8127,12 @@ function useGeneratorPanelCore({
7782
8127
  },
7783
8128
  [host]
7784
8129
  );
7785
- const handleBackToInstruments = useCallback16((trackId) => {
8130
+ const handleBackToInstruments = useCallback17((trackId) => {
7786
8131
  setTracks(
7787
8132
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
7788
8133
  );
7789
8134
  }, []);
7790
- const handleRefreshInstruments = useCallback16(() => {
8135
+ const handleRefreshInstruments = useCallback17(() => {
7791
8136
  setInstrumentsLoading(true);
7792
8137
  host.getAvailableInstruments().then((instruments) => {
7793
8138
  setAvailableInstruments(instruments);
@@ -7796,7 +8141,7 @@ function useGeneratorPanelCore({
7796
8141
  setInstrumentsLoading(false);
7797
8142
  });
7798
8143
  }, [host]);
7799
- const onAuditionNote = useCallback16(
8144
+ const onAuditionNote = useCallback17(
7800
8145
  (trackId, pitch, velocity, ms) => {
7801
8146
  void host.auditionNote(trackId, pitch, velocity, ms);
7802
8147
  },
@@ -7849,7 +8194,7 @@ function useGeneratorPanelCore({
7849
8194
  resolvedCrossfadePairs,
7850
8195
  resolvedFades
7851
8196
  });
7852
- const setGroupMute = useCallback16(
8197
+ const setGroupMute = useCallback17(
7853
8198
  (trackIds, muted) => {
7854
8199
  for (const id of trackIds) {
7855
8200
  setTracks(
@@ -7861,7 +8206,7 @@ function useGeneratorPanelCore({
7861
8206
  },
7862
8207
  [host]
7863
8208
  );
7864
- const setGroupSolo = useCallback16(
8209
+ const setGroupSolo = useCallback17(
7865
8210
  (trackIds, solo) => {
7866
8211
  for (const id of trackIds) {
7867
8212
  setTracks(
@@ -7873,7 +8218,7 @@ function useGeneratorPanelCore({
7873
8218
  },
7874
8219
  [host]
7875
8220
  );
7876
- const deleteGroup = useCallback16(
8221
+ const deleteGroup = useCallback17(
7877
8222
  async (members, cleanupKeySuffixes) => {
7878
8223
  for (const member of members) {
7879
8224
  try {
@@ -7979,6 +8324,7 @@ function useGeneratorPanelCore({
7979
8324
  handlers,
7980
8325
  handleGenerate,
7981
8326
  handleShuffle,
8327
+ handleRestoreSound,
7982
8328
  handleAddTrack,
7983
8329
  handleDeleteTrack,
7984
8330
  handleExportMidi,
@@ -8009,8 +8355,41 @@ function useGeneratorPanelCore({
8009
8355
  }
8010
8356
 
8011
8357
  // src/panel-core/GeneratorPanelShell.tsx
8012
- import React23, { useCallback as useCallback17 } from "react";
8013
- import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
8358
+ import { useCallback as useCallback18, useEffect as useEffect20, useState as useState25 } from "react";
8359
+ import { Fragment as Fragment7, jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
8360
+ var GROUP_UI_KEY = "groupUi";
8361
+ function CollapsibleGroup({
8362
+ ext,
8363
+ group,
8364
+ groupCtx
8365
+ }) {
8366
+ const { host, activeSceneId, trackDataKey: trackDataKey2 } = groupCtx.services;
8367
+ const uiKey = trackDataKey2(group.groupId, GROUP_UI_KEY);
8368
+ const [collapsed, setCollapsed] = useState25(false);
8369
+ useEffect20(() => {
8370
+ let cancelled = false;
8371
+ if (!activeSceneId) return void 0;
8372
+ void host.getSceneData(activeSceneId, uiKey).then((raw) => {
8373
+ if (cancelled || !raw || typeof raw !== "object") return;
8374
+ setCollapsed(raw.collapsed === true);
8375
+ }).catch(() => {
8376
+ });
8377
+ return () => {
8378
+ cancelled = true;
8379
+ };
8380
+ }, [host, activeSceneId, uiKey]);
8381
+ const onToggleCollapse = useCallback18(() => {
8382
+ setCollapsed((prev) => {
8383
+ const next = !prev;
8384
+ if (activeSceneId) {
8385
+ void host.setSceneData(activeSceneId, uiKey, { collapsed: next }).catch(() => {
8386
+ });
8387
+ }
8388
+ return next;
8389
+ });
8390
+ }, [host, activeSceneId, uiKey]);
8391
+ return /* @__PURE__ */ jsx29(Fragment7, { children: ext.renderGroup(group, { ...groupCtx, collapsed, onToggleCollapse }) });
8392
+ }
8014
8393
  function GeneratorPanelShell({ core, slots }) {
8015
8394
  const {
8016
8395
  ui,
@@ -8033,6 +8412,7 @@ function GeneratorPanelShell({ core, slots }) {
8033
8412
  soundImportTarget,
8034
8413
  setSoundImportTarget,
8035
8414
  handleSoundImportPick,
8415
+ handleRestoreSound,
8036
8416
  handlePortTrack,
8037
8417
  transition,
8038
8418
  crossfadePairsMeta,
@@ -8066,7 +8446,7 @@ function GeneratorPanelShell({ core, slots }) {
8066
8446
  const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
8067
8447
  const panelBus = usePanelBus(host, activeSceneId);
8068
8448
  const { identity, features } = adapter;
8069
- const buildRowProps = useCallback17(
8449
+ const buildRowProps = useCallback18(
8070
8450
  (track, drag) => {
8071
8451
  const id = track.handle.id;
8072
8452
  const pickerProps = features.instrumentPicker ? {
@@ -8128,7 +8508,7 @@ function GeneratorPanelShell({ core, slots }) {
8128
8508
  soundHistory: soundHistory.list(id).entries,
8129
8509
  soundHistoryCursor: soundHistory.list(id).cursor,
8130
8510
  onRestoreSound: (i) => {
8131
- void soundHistory.restoreTo(id, i);
8511
+ void handleRestoreSound(id, i);
8132
8512
  },
8133
8513
  onToggleFavorite: (i) => soundHistory.toggleFavorite(id, i),
8134
8514
  ...importSoundProps,
@@ -8159,6 +8539,7 @@ function GeneratorPanelShell({ core, slots }) {
8159
8539
  handleBackToInstruments,
8160
8540
  setSoundImportTarget,
8161
8541
  soundHistory,
8542
+ handleRestoreSound,
8162
8543
  handleFxToggle,
8163
8544
  handleFxPresetChange,
8164
8545
  handleFxDryWetChange,
@@ -8166,12 +8547,12 @@ function GeneratorPanelShell({ core, slots }) {
8166
8547
  ]
8167
8548
  );
8168
8549
  if (!activeSceneId) {
8169
- return /* @__PURE__ */ jsx27(
8550
+ return /* @__PURE__ */ jsx29(
8170
8551
  "div",
8171
8552
  {
8172
8553
  "data-testid": `no-scene-placeholder-${identity.familyKey}`,
8173
8554
  className: "flex items-center justify-center py-8",
8174
- children: /* @__PURE__ */ jsx27(
8555
+ children: /* @__PURE__ */ jsx29(
8175
8556
  "button",
8176
8557
  {
8177
8558
  onClick: () => onSelectScene?.(),
@@ -8183,12 +8564,12 @@ function GeneratorPanelShell({ core, slots }) {
8183
8564
  );
8184
8565
  }
8185
8566
  if (!sceneContext?.hasContract) {
8186
- return /* @__PURE__ */ jsx27(
8567
+ return /* @__PURE__ */ jsx29(
8187
8568
  "div",
8188
8569
  {
8189
8570
  "data-testid": `no-contract-placeholder-${identity.familyKey}`,
8190
8571
  className: "flex items-center justify-center py-8",
8191
- children: /* @__PURE__ */ jsx27(
8572
+ children: /* @__PURE__ */ jsx29(
8192
8573
  "button",
8193
8574
  {
8194
8575
  onClick: () => onOpenContract?.(),
@@ -8200,7 +8581,7 @@ function GeneratorPanelShell({ core, slots }) {
8200
8581
  );
8201
8582
  }
8202
8583
  if (features.bulkComposePlaceholders && isComposing) {
8203
- return /* @__PURE__ */ jsx27("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx27(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
8584
+ return /* @__PURE__ */ jsx29("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx29(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
8204
8585
  }
8205
8586
  const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
8206
8587
  if (activePlaceholders.length > 0) {
@@ -8211,18 +8592,18 @@ function GeneratorPanelShell({ core, slots }) {
8211
8592
  tracksByDbId.set(t.handle.id, t);
8212
8593
  }
8213
8594
  }
8214
- return /* @__PURE__ */ jsx27("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
8595
+ return /* @__PURE__ */ jsx29("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
8215
8596
  const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
8216
8597
  if (loadedTrack) {
8217
- return /* @__PURE__ */ jsx27(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
8598
+ return /* @__PURE__ */ jsx29(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
8218
8599
  }
8219
- return /* @__PURE__ */ jsx27(
8600
+ return /* @__PURE__ */ jsx29(
8220
8601
  "div",
8221
8602
  {
8222
8603
  "data-testid": "bulk-placeholder-track",
8223
8604
  className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
8224
8605
  style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
8225
- children: /* @__PURE__ */ jsx27(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
8606
+ children: /* @__PURE__ */ jsx29(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
8226
8607
  },
8227
8608
  ph.id
8228
8609
  );
@@ -8234,13 +8615,13 @@ function GeneratorPanelShell({ core, slots }) {
8234
8615
  supportsMeters,
8235
8616
  levels: supportsMeters ? trackLevels : void 0,
8236
8617
  handlers,
8237
- renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx27(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
8618
+ renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx29(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
8238
8619
  setGroupMute,
8239
8620
  setGroupSolo,
8240
8621
  deleteGroup
8241
8622
  };
8242
- return /* @__PURE__ */ jsxs21("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
8243
- features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx27(
8623
+ return /* @__PURE__ */ jsxs22("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
8624
+ features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx29(
8244
8625
  ImportTrackModal,
8245
8626
  {
8246
8627
  host,
@@ -8253,7 +8634,7 @@ function GeneratorPanelShell({ core, slots }) {
8253
8634
  testIdPrefix: `${identity.familyKey}-import`
8254
8635
  }
8255
8636
  ),
8256
- features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx27(
8637
+ features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx29(
8257
8638
  ImportTrackModal,
8258
8639
  {
8259
8640
  host,
@@ -8268,7 +8649,7 @@ function GeneratorPanelShell({ core, slots }) {
8268
8649
  }
8269
8650
  ),
8270
8651
  slots?.modals,
8271
- canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx27("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx27(
8652
+ canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx29("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx29(
8272
8653
  TransitionDesigner,
8273
8654
  {
8274
8655
  host,
@@ -8287,8 +8668,8 @@ function GeneratorPanelShell({ core, slots }) {
8287
8668
  testIdPrefix: `${identity.familyKey}-transition-designer`
8288
8669
  }
8289
8670
  ) }),
8290
- !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx27("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs21(Fragment7, { children: [
8291
- panelBus.supported && panelBus.bus && /* @__PURE__ */ jsx27(
8671
+ !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx29("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs22(Fragment7, { children: [
8672
+ panelBus.supported && panelBus.bus && /* @__PURE__ */ jsx29(
8292
8673
  PanelMasterStrip,
8293
8674
  {
8294
8675
  bus: panelBus.bus,
@@ -8309,7 +8690,7 @@ function GeneratorPanelShell({ core, slots }) {
8309
8690
  }
8310
8691
  ),
8311
8692
  slots?.beforeRows,
8312
- resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx27(
8693
+ resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx29(
8313
8694
  CrossfadeTrackRow,
8314
8695
  {
8315
8696
  accentColor: identity.transitionAccentColor,
@@ -8346,7 +8727,7 @@ function GeneratorPanelShell({ core, slots }) {
8346
8727
  },
8347
8728
  pair.groupId
8348
8729
  )),
8349
- resolvedSingleFades.map((fade) => /* @__PURE__ */ jsx27(
8730
+ resolvedSingleFades.map((fade) => /* @__PURE__ */ jsx29(
8350
8731
  FadeTrackRow,
8351
8732
  {
8352
8733
  accentColor: identity.transitionAccentColor,
@@ -8371,7 +8752,7 @@ function GeneratorPanelShell({ core, slots }) {
8371
8752
  },
8372
8753
  fade.dbId
8373
8754
  )),
8374
- resolvedGroupFades.map((group) => /* @__PURE__ */ jsx27(
8755
+ resolvedGroupFades.map((group) => /* @__PURE__ */ jsx29(
8375
8756
  GroupFadeTrackRow,
8376
8757
  {
8377
8758
  accentColor: identity.transitionAccentColor,
@@ -8407,20 +8788,28 @@ function GeneratorPanelShell({ core, slots }) {
8407
8788
  group.groupId
8408
8789
  )),
8409
8790
  (adapter.groupExtensions ?? []).flatMap(
8410
- (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).filter((group) => !group.members.every((m) => fadeMemberDbIds.has(m.dbId))).map((group) => /* @__PURE__ */ jsx27(React23.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
8791
+ (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).filter((group) => !group.members.every((m) => fadeMemberDbIds.has(m.dbId))).map((group) => /* @__PURE__ */ jsx29(
8792
+ CollapsibleGroup,
8793
+ {
8794
+ ext,
8795
+ group,
8796
+ groupCtx
8797
+ },
8798
+ `${ext.metaKey}:${group.groupId}`
8799
+ ))
8411
8800
  ),
8412
8801
  tracks.map((track, index) => {
8413
8802
  if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
8414
8803
  return null;
8415
8804
  }
8416
- return /* @__PURE__ */ jsx27(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
8805
+ return /* @__PURE__ */ jsx29(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
8417
8806
  }),
8418
8807
  slots?.afterRows
8419
8808
  ] })),
8420
8809
  features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
8421
8810
  const hasAnyMidi = tracks.some((t) => t.hasMidi);
8422
8811
  const exportDisabled = isExportingMidi || !hasAnyMidi;
8423
- return /* @__PURE__ */ jsx27("div", { className: "pt-2", children: /* @__PURE__ */ jsx27(
8812
+ return /* @__PURE__ */ jsx29("div", { className: "pt-2", children: /* @__PURE__ */ jsx29(
8424
8813
  "button",
8425
8814
  {
8426
8815
  "data-testid": "export-midi-tracks-button",
@@ -8475,6 +8864,15 @@ function createSurgeSoundAdapter(host, overrides = {}) {
8475
8864
  });
8476
8865
  return snap.label;
8477
8866
  },
8867
+ persistDescriptor: async (trackId, descriptor, label) => {
8868
+ const { state, stateType } = descriptor;
8869
+ await host.persistTrackPresetState?.(trackId, {
8870
+ state,
8871
+ stateType: stateType ?? "valuetree",
8872
+ name: label
8873
+ }).catch(() => {
8874
+ });
8875
+ },
8478
8876
  descriptorFromSnapshot: (snap) => {
8479
8877
  const preset = snap;
8480
8878
  return { state: preset.state, stateType: preset.stateType };
@@ -8559,18 +8957,183 @@ var SUB = {
8559
8957
  rootOnly: true,
8560
8958
  monoPreference: "low"
8561
8959
  };
8960
+ var HORN_MAX_NOTES_PER_BAR = 12;
8961
+ var HORN_FOLLOW_PALETTE = "EXACTLY the lead voice's rhythm \u2014 every attack lands together with voice 1";
8962
+ var HORN_LEAD = {
8963
+ label: "lead trumpet",
8964
+ role: "brass",
8965
+ registerLow: 60,
8966
+ // C4
8967
+ registerHigh: 84,
8968
+ // C6
8969
+ maxNotesPerBar: HORN_MAX_NOTES_PER_BAR,
8970
+ rhythmPalette: "tight funk syncopation \u2014 8ths and 16ths, staccato punches, off-beat anticipations",
8971
+ harmonicDiscipline: "the section's melodic top \u2014 chord tones on every hit; quick chromatic approach tones only on pickups",
8972
+ monoPreference: "high"
8973
+ };
8974
+ var HORN_TRUMPET_2 = {
8975
+ label: "second trumpet",
8976
+ role: "brass",
8977
+ registerLow: 55,
8978
+ // G3
8979
+ registerHigh: 79,
8980
+ // G5
8981
+ maxNotesPerBar: HORN_MAX_NOTES_PER_BAR,
8982
+ rhythmPalette: HORN_FOLLOW_PALETTE,
8983
+ harmonicDiscipline: "nearest chord tone directly below voice 1 \u2014 tight close voicing",
8984
+ monoPreference: "high"
8985
+ };
8986
+ var HORN_ALTO = {
8987
+ label: "alto sax",
8988
+ role: "brass",
8989
+ registerLow: 53,
8990
+ // F3
8991
+ registerHigh: 77,
8992
+ // F5
8993
+ maxNotesPerBar: HORN_MAX_NOTES_PER_BAR,
8994
+ rhythmPalette: HORN_FOLLOW_PALETTE,
8995
+ harmonicDiscipline: "chord tone below the trumpets \u2014 keep the upper stack inside one octave",
8996
+ monoPreference: "high"
8997
+ };
8998
+ var HORN_TENOR = {
8999
+ label: "tenor sax",
9000
+ role: "brass",
9001
+ registerLow: 46,
9002
+ // Bb2
9003
+ registerHigh: 72,
9004
+ // C5
9005
+ maxNotesPerBar: HORN_MAX_NOTES_PER_BAR,
9006
+ rhythmPalette: HORN_FOLLOW_PALETTE,
9007
+ harmonicDiscipline: "chord tone under the upper horns \u2014 complete the chord",
9008
+ monoPreference: "low"
9009
+ };
9010
+ var HORN_TROMBONE = {
9011
+ label: "trombone",
9012
+ role: "brass",
9013
+ registerLow: 43,
9014
+ // G2
9015
+ registerHigh: 67,
9016
+ // G4
9017
+ maxNotesPerBar: HORN_MAX_NOTES_PER_BAR,
9018
+ rhythmPalette: HORN_FOLLOW_PALETTE,
9019
+ harmonicDiscipline: "chord tone or root below the saxes \u2014 weight in the middle-low stack",
9020
+ monoPreference: "low"
9021
+ };
9022
+ var HORN_BARI = {
9023
+ label: "baritone sax",
9024
+ role: "brass",
9025
+ registerLow: 36,
9026
+ // C2
9027
+ registerHigh: 60,
9028
+ // C4
9029
+ maxNotesPerBar: HORN_MAX_NOTES_PER_BAR,
9030
+ rhythmPalette: HORN_FOLLOW_PALETTE,
9031
+ harmonicDiscipline: "chord roots and lower chord tones \u2014 the section's bottom anchor",
9032
+ monoPreference: "low"
9033
+ };
9034
+ var WIND_FLUTE = {
9035
+ label: "flute",
9036
+ role: "winds",
9037
+ registerLow: 60,
9038
+ // C4
9039
+ registerHigh: 93,
9040
+ // A6
9041
+ maxNotesPerBar: 8,
9042
+ rhythmPalette: "8ths and 16ths; runs, turns and trills welcome",
9043
+ harmonicDiscipline: "freest voice \u2014 non-chord tones as passing/neighbor tones on weak beats, resolving by step",
9044
+ monoPreference: "high"
9045
+ };
9046
+ var WIND_FLUTE_2 = {
9047
+ label: "second flute",
9048
+ role: "winds",
9049
+ registerLow: 60,
9050
+ // C4
9051
+ registerHigh: 86,
9052
+ // D6
9053
+ maxNotesPerBar: 5,
9054
+ rhythmPalette: "quarters with occasional 8th-note motion",
9055
+ harmonicDiscipline: "chord tones; fill gaps the other inner voices leave",
9056
+ monoPreference: "high"
9057
+ };
9058
+ var WIND_OBOE = {
9059
+ label: "oboe",
9060
+ role: "winds",
9061
+ registerLow: 58,
9062
+ // Bb3
9063
+ registerHigh: 84,
9064
+ // C6
9065
+ maxNotesPerBar: 6,
9066
+ rhythmPalette: "8ths and quarters; move when the flute rests",
9067
+ harmonicDiscipline: "mostly chord tones; may imitate the flute's motifs a bar later",
9068
+ monoPreference: "high"
9069
+ };
9070
+ var WIND_CLARINET = {
9071
+ label: "clarinet",
9072
+ role: "winds",
9073
+ registerLow: 50,
9074
+ // D3
9075
+ registerHigh: 81,
9076
+ // A5
9077
+ maxNotesPerBar: 4,
9078
+ rhythmPalette: "quarters and halves",
9079
+ harmonicDiscipline: "chord tones with smooth stepwise motion between them",
9080
+ monoPreference: "high"
9081
+ };
9082
+ var WIND_FRENCH_HORN = {
9083
+ label: "french horn",
9084
+ role: "winds",
9085
+ registerLow: 41,
9086
+ // F2
9087
+ registerHigh: 72,
9088
+ // C5
9089
+ maxNotesPerBar: 4,
9090
+ rhythmPalette: "quarters and halves; long held tones welcome",
9091
+ harmonicDiscipline: "chord tones \u2014 the warm glue in the middle of the ensemble",
9092
+ monoPreference: "low"
9093
+ };
9094
+ var WIND_BASSOON = {
9095
+ label: "bassoon",
9096
+ role: "winds",
9097
+ registerLow: 34,
9098
+ // Bb1
9099
+ registerHigh: 62,
9100
+ // D4
9101
+ maxNotesPerBar: 3,
9102
+ rhythmPalette: "quarters and halves; brief walking figures at cadences",
9103
+ harmonicDiscipline: "roots and fifths emphasized; passing tones only between chord tones",
9104
+ monoPreference: "low"
9105
+ };
8562
9106
  var ENSEMBLE_MIN_VOICES = 2;
8563
9107
  var ENSEMBLE_MAX_VOICES = 6;
8564
- var SPEC_TABLES = {
9108
+ var STRINGS_SPEC_TABLES = {
8565
9109
  2: [TOP, BASS],
8566
9110
  3: [TOP, INNER, BASS],
8567
9111
  4: [TOP, COUNTER, TENOR, BASS],
8568
9112
  5: [TOP, COUNTER, INNER, TENOR, SUB],
8569
9113
  6: [TOP, COUNTER, INNER_2, INNER, TENOR, SUB]
8570
9114
  };
8571
- function defaultVoiceSpecs(voiceCount) {
9115
+ var HORNS_SPEC_TABLES = {
9116
+ 2: [HORN_LEAD, HORN_TENOR],
9117
+ 3: [HORN_LEAD, HORN_TENOR, HORN_BARI],
9118
+ 4: [HORN_LEAD, HORN_TRUMPET_2, HORN_TENOR, HORN_BARI],
9119
+ 5: [HORN_LEAD, HORN_TRUMPET_2, HORN_TENOR, HORN_TROMBONE, HORN_BARI],
9120
+ 6: [HORN_LEAD, HORN_TRUMPET_2, HORN_ALTO, HORN_TENOR, HORN_TROMBONE, HORN_BARI]
9121
+ };
9122
+ var WINDS_SPEC_TABLES = {
9123
+ 2: [WIND_FLUTE, WIND_BASSOON],
9124
+ 3: [WIND_FLUTE, WIND_CLARINET, WIND_BASSOON],
9125
+ 4: [WIND_FLUTE, WIND_OBOE, WIND_FRENCH_HORN, WIND_BASSOON],
9126
+ 5: [WIND_FLUTE, WIND_OBOE, WIND_CLARINET, WIND_FRENCH_HORN, WIND_BASSOON],
9127
+ 6: [WIND_FLUTE, WIND_FLUTE_2, WIND_OBOE, WIND_CLARINET, WIND_FRENCH_HORN, WIND_BASSOON]
9128
+ };
9129
+ var SPEC_TABLES_BY_INSTRUMENTATION = {
9130
+ strings: STRINGS_SPEC_TABLES,
9131
+ horns: HORNS_SPEC_TABLES,
9132
+ winds: WINDS_SPEC_TABLES
9133
+ };
9134
+ function defaultVoiceSpecs(voiceCount, instrumentation = "strings") {
8572
9135
  const n = Math.max(ENSEMBLE_MIN_VOICES, Math.min(ENSEMBLE_MAX_VOICES, Math.round(voiceCount)));
8573
- return SPEC_TABLES[n].map((spec, voiceIndex) => ({ ...spec, voiceIndex }));
9136
+ return SPEC_TABLES_BY_INSTRUMENTATION[instrumentation][n].map((spec, voiceIndex) => ({ ...spec, voiceIndex }));
8574
9137
  }
8575
9138
 
8576
9139
  // src/ensemble-core/enforce-voice.ts
@@ -8610,10 +9173,14 @@ function enforceVoice(rawNotes, spec, opts) {
8610
9173
  repairs.push(`voice ${spec.voiceIndex}: dropped note outside the ${opts.bars}-bar clip (start ${n.startBeat})`);
8611
9174
  continue;
8612
9175
  }
8613
- const durationBeats = Math.max(
9176
+ let durationBeats = Math.max(
8614
9177
  MIN_NOTE_DURATION_BEATS,
8615
9178
  Math.min(n.durationBeats, clipEnd - n.startBeat)
8616
9179
  );
9180
+ if (opts.maxNoteDurationBeats !== void 0 && durationBeats > opts.maxNoteDurationBeats) {
9181
+ durationBeats = Math.max(MIN_NOTE_DURATION_BEATS, opts.maxNoteDurationBeats);
9182
+ repairs.push(`voice ${spec.voiceIndex}: trimmed note at beat ${n.startBeat} to the style's ${opts.maxNoteDurationBeats}-beat stab ceiling`);
9183
+ }
8617
9184
  notes.push({ ...n, durationBeats });
8618
9185
  }
8619
9186
  notes = notes.map((n) => {
@@ -8774,12 +9341,19 @@ function describeViolations(analysis, rules) {
8774
9341
  if (pair.onsetIndependence < rules.minOnsetIndependence) {
8775
9342
  out.push(`Voices ${pair.upperVoice + 1} and ${pair.upperVoice + 2} attack together too often (independence ${pair.onsetIndependence.toFixed(2)} < ${rules.minOnsetIndependence}) \u2014 stagger entrances and let one voice move while the other holds.`);
8776
9343
  }
9344
+ if (rules.maxOnsetIndependence !== void 0 && pair.onsetIndependence > rules.maxOnsetIndependence) {
9345
+ out.push(`Voices ${pair.upperVoice + 1} and ${pair.upperVoice + 2} don't attack together enough (independence ${pair.onsetIndependence.toFixed(2)} > ${rules.maxOnsetIndependence}) \u2014 this is a section: give every voice the SAME rhythm as the lead so the hits land as one.`);
9346
+ }
8777
9347
  }
8778
9348
  return out;
8779
9349
  }
8780
9350
 
8781
9351
  // src/ensemble-core/styles.ts
8782
9352
  var ENSEMBLE_STYLES = ["counterpoint", "chorale", "interlock"];
9353
+ var STAB_MAX_ONSET_INDEPENDENCE = 0.25;
9354
+ var RIFF_MAX_ONSET_INDEPENDENCE = 0.3;
9355
+ var UNISON_MAX_ONSET_INDEPENDENCE = 0.15;
9356
+ var STAB_MAX_NOTE_DURATION_BEATS = 1;
8783
9357
  var STYLE_RULES = {
8784
9358
  counterpoint: {
8785
9359
  forbidParallelPerfects: true,
@@ -8798,9 +9372,55 @@ var STYLE_RULES = {
8798
9372
  forbidVoiceCrossing: false,
8799
9373
  minOnsetIndependence: 0.6,
8800
9374
  promptParagraph: "STYLE \u2014 INTERLOCK (minimal / systems music): short repeating cells that mesh like gears. Each voice keeps its own ostinato; onsets rarely coincide with the neighboring voice; parallel motion and doubling are welcome when the composite rhythm stays busy and even."
9375
+ },
9376
+ stabs: {
9377
+ forbidParallelPerfects: false,
9378
+ forbidVoiceCrossing: true,
9379
+ minOnsetIndependence: 0,
9380
+ maxOnsetIndependence: STAB_MAX_ONSET_INDEPENDENCE,
9381
+ maxNoteDurationBeats: STAB_MAX_NOTE_DURATION_BEATS,
9382
+ promptParagraph: `STYLE \u2014 STABS (James Brown funk-45 horn punches): short accented chord hits \u2014 single punches and two-to-four-note kicks \u2014 placed on the groove's pressure points: the one, off-beat "ands", and the pickup into the next bar. Every hit is staccato (a 16th to an 8th long) and voiced as ONE tight chord under the lead. Leave REAL space between hits \u2014 the section punctuates the groove, it never carpets it.`
9383
+ },
9384
+ riffs: {
9385
+ forbidParallelPerfects: false,
9386
+ forbidVoiceCrossing: true,
9387
+ minOnsetIndependence: 0,
9388
+ maxOnsetIndependence: RIFF_MAX_ONSET_INDEPENDENCE,
9389
+ promptParagraph: 'STYLE \u2014 RIFFS (funk/soul section soli): ONE syncopated 8th/16th riff, one or two bars long, repeated with small variations for the whole clip \u2014 the entire section plays it together, harmonized in tight close voicings under the lead. Anticipate downbeats (attack the "and" just before the barline), leave air at phrase ends, and let the final hit of a phrase ring a little longer.'
9390
+ },
9391
+ unison: {
9392
+ forbidParallelPerfects: false,
9393
+ forbidVoiceCrossing: false,
9394
+ minOnsetIndependence: 0,
9395
+ maxOnsetIndependence: UNISON_MAX_ONSET_INDEPENDENCE,
9396
+ promptParagraph: "STYLE \u2014 UNISON (the Cold Sweat power line): every voice doubles the SAME riff in octaves \u2014 no harmony, one line, maximum punch. Keep it syncopated and riff-based with space between phrases; octave doubling and parallel motion are the entire point."
8801
9397
  }
8802
9398
  };
8803
9399
 
9400
+ // src/ensemble-core/instrumentation.ts
9401
+ var ENSEMBLE_INSTRUMENTATIONS = [
9402
+ "strings",
9403
+ "horns",
9404
+ "winds"
9405
+ ];
9406
+ var STYLES_FOR_INSTRUMENTATION = {
9407
+ strings: ["counterpoint", "chorale", "interlock"],
9408
+ horns: ["stabs", "riffs", "unison"],
9409
+ winds: ["counterpoint", "chorale", "interlock"]
9410
+ };
9411
+ var DEFAULT_STYLE_FOR_INSTRUMENTATION = {
9412
+ strings: "counterpoint",
9413
+ horns: "stabs",
9414
+ winds: "chorale"
9415
+ };
9416
+ function normalizeInstrumentation(raw) {
9417
+ return ENSEMBLE_INSTRUMENTATIONS.includes(raw) ? raw : "strings";
9418
+ }
9419
+ function styleForInstrumentation(instrumentation, rawStyle) {
9420
+ const allowed = STYLES_FOR_INSTRUMENTATION[instrumentation];
9421
+ return allowed.includes(rawStyle ?? "") ? rawStyle : DEFAULT_STYLE_FOR_INSTRUMENTATION[instrumentation];
9422
+ }
9423
+
8804
9424
  // src/ensemble-core/ensemble-schema.ts
8805
9425
  var SUBMIT_ENSEMBLE_TOOL_NAME = "submit_ensemble";
8806
9426
  function buildSubmitEnsembleParameters(voiceCount) {
@@ -8882,9 +9502,13 @@ function voiceContractLine(spec) {
8882
9502
  const root = spec.rootOnly ? " ROOT PITCH CLASS ONLY (each bar's chord root)." : "";
8883
9503
  return `- Voice ${spec.voiceIndex + 1} (${spec.label}): MIDI ${spec.registerLow}-${spec.registerHigh}, max ${spec.maxNotesPerBar} notes/bar, rhythm: ${spec.rhythmPalette}. ${spec.harmonicDiscipline}.${root}`;
8884
9504
  }
8885
- function buildEnsembleSystemPrompt(specs, style) {
9505
+ var WOVEN_PERSONA = {
9506
+ strings: "You are an ensemble composer.",
9507
+ winds: "You are a wind-ensemble composer writing for chamber winds (flutes, oboe, clarinet, french horn, bassoon)."
9508
+ };
9509
+ function buildWovenPrompt(specs, style, instrumentation) {
8886
9510
  const styleParagraph = STYLE_RULES[style].promptParagraph;
8887
- return `You are an ensemble composer. Compose ${specs.length} voices as ONE piece of music \u2014 not ${specs.length} independent parts.
9511
+ return `${WOVEN_PERSONA[instrumentation]} Compose ${specs.length} voices as ONE piece of music \u2014 not ${specs.length} independent parts.
8888
9512
 
8889
9513
  Submit your composition by calling the ${SUBMIT_ENSEMBLE_TOOL_NAME} tool with all ${specs.length} voices.
8890
9514
 
@@ -8901,6 +9525,28 @@ ${styleParagraph}
8901
9525
  PER-VOICE CONTRACTS:
8902
9526
  ${specs.map(voiceContractLine).join("\n")}`;
8903
9527
  }
9528
+ function buildSectionPrompt(specs, style) {
9529
+ const styleParagraph = STYLE_RULES[style].promptParagraph;
9530
+ return `You are a funk horn-section arranger. Compose ${specs.length} horn parts as ONE section \u2014 ${specs.length} players phrasing like a single instrument, not ${specs.length} independent parts.
9531
+
9532
+ Submit your composition by calling the ${SUBMIT_ENSEMBLE_TOOL_NAME} tool with all ${specs.length} voices.
9533
+
9534
+ THE SECTION RULES (most important):
9535
+ 1. The section speaks with ONE rhythm: every voice attacks on the same beats as Voice 1 (the lead). Compose the lead line first, then stack the other voices onto its rhythm exactly \u2014 the style below dictates chord voicing vs octave doubling.
9536
+ 2. Each voice is a SINGLE monophonic line \u2014 within one voice, no two notes overlap in time and no chords. The chord is the STACK of voices hitting together.
9537
+ 3. SPACE IS THE GROOVE: short phrases and punches separated by real rests. Never carpet the bar \u2014 the section punctuates around the rhythm section, and the silence between hits is part of the riff.
9538
+ 4. Follow the chord progression in the musical context exactly \u2014 every hit is voiced from the chord sounding at that beat.
9539
+ 5. Make it DANCE: this is funk. Syncopate, anticipate downbeats (attack the "and" just before the barline), and accent with velocity \u2014 105-120 on accented hits, 70-90 on lighter punches. Repeat the riff so the clip locks like a loop.
9540
+ 6. startBeat/durationBeats are in quarter-note beats from the start of the clip; respect each voice's register window and notes-per-bar cap exactly; fill the stated bar length and land the first hit in bar 1 (no long silent intro).
9541
+
9542
+ ${styleParagraph}
9543
+
9544
+ PER-VOICE CONTRACTS:
9545
+ ${specs.map(voiceContractLine).join("\n")}`;
9546
+ }
9547
+ function buildEnsembleSystemPrompt(specs, style, instrumentation = "strings") {
9548
+ return instrumentation === "horns" ? buildSectionPrompt(specs, style) : buildWovenPrompt(specs, style, instrumentation);
9549
+ }
8904
9550
  function buildViolationRetrySuffix(violations) {
8905
9551
  if (violations.length === 0) return "";
8906
9552
  return `
@@ -8910,7 +9556,7 @@ Your previous ensemble had these problems \u2014 fix them while keeping everythi
8910
9556
  }
8911
9557
 
8912
9558
  // src/constants/sdk-version.ts
8913
- var PLUGIN_SDK_VERSION = "2.45.0";
9559
+ var PLUGIN_SDK_VERSION = "2.46.0";
8914
9560
 
8915
9561
  // src/utils/format-concurrent-tracks.ts
8916
9562
  function formatConcurrentTracks(ctx) {
@@ -9075,10 +9721,12 @@ export {
9075
9721
  DB_MIN,
9076
9722
  DEFAULT_FX_CATEGORY_DETAIL,
9077
9723
  DEFAULT_FX_DRY_WET,
9724
+ DEFAULT_STYLE_FOR_INSTRUMENTATION,
9078
9725
  DRAG_DEAD_ZONE,
9079
9726
  DownloadPackButton,
9080
9727
  EMPTY_FX_DETAIL_STATE,
9081
9728
  EMPTY_FX_STATE,
9729
+ ENSEMBLE_INSTRUMENTATIONS,
9082
9730
  ENSEMBLE_MAX_VOICES,
9083
9731
  ENSEMBLE_MIN_VOICES,
9084
9732
  ENSEMBLE_STYLES,
@@ -9093,7 +9741,9 @@ export {
9093
9741
  FxToggleBar,
9094
9742
  GUTTER_W,
9095
9743
  GeneratorPanelShell,
9744
+ GroupCollapseChevron,
9096
9745
  GroupFadeTrackRow,
9746
+ HORN_MAX_NOTES_PER_BAR,
9097
9747
  ImportTrackModal,
9098
9748
  TrackDrawer as InstrumentDrawer,
9099
9749
  LevelMeter,
@@ -9107,8 +9757,12 @@ export {
9107
9757
  PianoRollEditor,
9108
9758
  PluginError,
9109
9759
  RESIZE_HANDLE_PX,
9760
+ RIFF_MAX_ONSET_INDEPENDENCE,
9110
9761
  ROW_HEIGHT,
9111
9762
  SLIDER_UNITY,
9763
+ STAB_MAX_NOTE_DURATION_BEATS,
9764
+ STAB_MAX_ONSET_INDEPENDENCE,
9765
+ STYLES_FOR_INSTRUMENTATION,
9112
9766
  STYLE_RULES,
9113
9767
  SUBMIT_ENSEMBLE_TOOL_NAME,
9114
9768
  SamplePackCTACard,
@@ -9118,9 +9772,11 @@ export {
9118
9772
  TRANSITION_DESIGNER_DRAFT_KEY,
9119
9773
  TrackDrawer,
9120
9774
  TrackExternalFxSection,
9775
+ TrackFreezeSection,
9121
9776
  TrackMeterStrip,
9122
9777
  TrackRow,
9123
9778
  TransitionDesigner,
9779
+ UNISON_MAX_ONSET_INDEPENDENCE,
9124
9780
  VolumeSlider,
9125
9781
  WaveformView,
9126
9782
  analyzeEnsemble,
@@ -9155,6 +9811,7 @@ export {
9155
9811
  moveItem,
9156
9812
  nearestPitchWithPc,
9157
9813
  newTrackState,
9814
+ normalizeInstrumentation,
9158
9815
  normalizeSlots,
9159
9816
  padPair,
9160
9817
  padSlots,
@@ -9178,6 +9835,7 @@ export {
9178
9835
  slotsEqual,
9179
9836
  soundIdentity,
9180
9837
  splitFadeEntries,
9838
+ styleForInstrumentation,
9181
9839
  synthesizeCuePoints,
9182
9840
  tokenizePrompt,
9183
9841
  trackDataKey,
@@ -9188,6 +9846,7 @@ export {
9188
9846
  useSceneState,
9189
9847
  useSoundHistory,
9190
9848
  useTrackExternalFx,
9849
+ useTrackFreeze,
9191
9850
  useTrackLevel,
9192
9851
  useTrackLevels,
9193
9852
  useTrackMeter,