@signalsandsorcery/plugin-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1312 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DB_MAX: () => DB_MAX,
24
+ DB_MIN: () => DB_MIN,
25
+ DEFAULT_FX_CATEGORY_DETAIL: () => DEFAULT_FX_CATEGORY_DETAIL,
26
+ DEFAULT_FX_DRY_WET: () => DEFAULT_FX_DRY_WET,
27
+ EMPTY_FX_DETAIL_STATE: () => EMPTY_FX_DETAIL_STATE,
28
+ EMPTY_FX_STATE: () => EMPTY_FX_STATE,
29
+ FX_CATEGORIES: () => FX_CATEGORIES,
30
+ FX_CHAIN_ORDER: () => FX_CHAIN_ORDER,
31
+ FX_DISPLAY_LABELS: () => FX_DISPLAY_LABELS,
32
+ FX_ENGINE_PLUGIN_NAMES: () => FX_ENGINE_PLUGIN_NAMES,
33
+ FX_PRESET_CONFIGS: () => FX_PRESET_CONFIGS,
34
+ FxToggleBar: () => FxToggleBar,
35
+ InstrumentDrawer: () => InstrumentDrawer,
36
+ PLUGIN_SDK_VERSION: () => PLUGIN_SDK_VERSION,
37
+ PanSlider: () => PanSlider,
38
+ PluginError: () => PluginError,
39
+ SLIDER_UNITY: () => SLIDER_UNITY,
40
+ SorceryProgressBar: () => SorceryProgressBar,
41
+ TrackRow: () => TrackRow,
42
+ VALID_INSTRUMENT_ROLES: () => VALID_INSTRUMENT_ROLES,
43
+ VolumeSlider: () => VolumeSlider,
44
+ calculateTimeBasedTarget: () => calculateTimeBasedTarget,
45
+ dbToSlider: () => dbToSlider,
46
+ sliderToDb: () => sliderToDb,
47
+ useSceneState: () => useSceneState
48
+ });
49
+ module.exports = __toCommonJS(index_exports);
50
+
51
+ // src/types/plugin-sdk.types.ts
52
+ var PluginError = class extends Error {
53
+ constructor(code, message, details) {
54
+ super(message);
55
+ this.name = "PluginError";
56
+ this.code = code;
57
+ this.details = details;
58
+ }
59
+ };
60
+
61
+ // src/types/fx-toggle.types.ts
62
+ var FX_CATEGORIES = [
63
+ "eq",
64
+ "compressor",
65
+ "chorus",
66
+ "phaser",
67
+ "delay",
68
+ "reverb"
69
+ ];
70
+ var FX_CHAIN_ORDER = {
71
+ eq: 0,
72
+ compressor: 1,
73
+ chorus: 2,
74
+ phaser: 3,
75
+ delay: 4,
76
+ reverb: 5
77
+ };
78
+ var FX_ENGINE_PLUGIN_NAMES = {
79
+ eq: "4bandEq",
80
+ compressor: "compressor",
81
+ chorus: "chorus",
82
+ phaser: "phaser",
83
+ delay: "delay",
84
+ reverb: "reverb"
85
+ };
86
+ var FX_DISPLAY_LABELS = {
87
+ eq: "EQ",
88
+ compressor: "Comp",
89
+ chorus: "Chorus",
90
+ phaser: "Phaser",
91
+ delay: "Delay",
92
+ reverb: "Reverb"
93
+ };
94
+ var EMPTY_FX_STATE = {
95
+ eq: false,
96
+ compressor: false,
97
+ chorus: false,
98
+ phaser: false,
99
+ delay: false,
100
+ reverb: false
101
+ };
102
+ var DEFAULT_FX_DRY_WET = 0.33;
103
+ var DEFAULT_FX_CATEGORY_DETAIL = {
104
+ enabled: false,
105
+ presetIndex: 0,
106
+ dryWet: DEFAULT_FX_DRY_WET
107
+ };
108
+ var EMPTY_FX_DETAIL_STATE = {
109
+ eq: { ...DEFAULT_FX_CATEGORY_DETAIL },
110
+ compressor: { ...DEFAULT_FX_CATEGORY_DETAIL },
111
+ chorus: { ...DEFAULT_FX_CATEGORY_DETAIL },
112
+ phaser: { ...DEFAULT_FX_CATEGORY_DETAIL },
113
+ delay: { ...DEFAULT_FX_CATEGORY_DETAIL },
114
+ reverb: { ...DEFAULT_FX_CATEGORY_DETAIL }
115
+ };
116
+
117
+ // src/components/TrackRow.tsx
118
+ var import_lucide_react = require("lucide-react");
119
+
120
+ // src/components/InstrumentDrawer.tsx
121
+ var import_react = require("react");
122
+ var import_jsx_runtime = require("react/jsx-runtime");
123
+ function InstrumentDrawer({
124
+ instruments,
125
+ currentPluginId,
126
+ isLoading,
127
+ onSelect,
128
+ onRefresh
129
+ }) {
130
+ const [search, setSearch] = (0, import_react.useState)("");
131
+ const SURGE_XT_DEFAULT_ID = "Surge XT";
132
+ const filtered = (0, import_react.useMemo)(() => {
133
+ const all = instruments.filter(
134
+ (i) => i.name !== "Surge XT"
135
+ );
136
+ if (!search.trim()) return all;
137
+ const q = search.toLowerCase();
138
+ return all.filter(
139
+ (i) => i.name.toLowerCase().includes(q) || i.manufacturer.toLowerCase().includes(q)
140
+ );
141
+ }, [instruments, search]);
142
+ const isDefaultSelected = currentPluginId === null;
143
+ const isSelected = (pluginId) => {
144
+ return pluginId === currentPluginId;
145
+ };
146
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2", children: [
147
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
148
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
149
+ "input",
150
+ {
151
+ type: "text",
152
+ value: search,
153
+ onChange: (e) => setSearch(e.target.value),
154
+ placeholder: "Search instruments...",
155
+ className: "sas-input flex-1 px-2 py-1 text-xs"
156
+ }
157
+ ),
158
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
159
+ "button",
160
+ {
161
+ onClick: onRefresh,
162
+ disabled: isLoading,
163
+ className: "px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-50",
164
+ title: "Re-scan plugins",
165
+ children: isLoading ? "..." : "Refresh"
166
+ }
167
+ )
168
+ ] }),
169
+ isLoading && instruments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
170
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
171
+ "button",
172
+ {
173
+ onClick: () => onSelect(SURGE_XT_DEFAULT_ID),
174
+ 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"}`,
175
+ title: "Surge XT \u2014 Default instrument",
176
+ children: [
177
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-xs font-medium truncate w-full", children: [
178
+ isDefaultSelected && "\u2713 ",
179
+ "Surge XT"
180
+ ] }),
181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: "Default" })
182
+ ]
183
+ },
184
+ "__surge-xt-default__"
185
+ ),
186
+ filtered.map((inst) => {
187
+ const selected = isSelected(inst.pluginId);
188
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
189
+ "button",
190
+ {
191
+ onClick: () => onSelect(inst.pluginId),
192
+ 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"}`,
193
+ title: `${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? " \u2014 MISSING" : ""}`,
194
+ children: [
195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-xs font-medium truncate w-full", children: [
196
+ selected && "\u2713 ",
197
+ inst.name
198
+ ] }),
199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: inst.manufacturer || inst.type.toUpperCase() })
200
+ ]
201
+ },
202
+ inst.pluginId
203
+ );
204
+ }),
205
+ filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "col-span-2 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No other plugins found" })
206
+ ] })
207
+ ] });
208
+ }
209
+
210
+ // src/components/VolumeSlider.tsx
211
+ var import_react2 = require("react");
212
+
213
+ // src/utils/volume-conversion.ts
214
+ var SLIDER_UNITY = 0.75;
215
+ var DB_MAX = 6;
216
+ var DB_MIN = -60;
217
+ var EXPONENT = Math.log(Math.pow(10, DB_MAX / 20)) / Math.log(1 / SLIDER_UNITY);
218
+ function sliderToDb(slider) {
219
+ if (slider <= 0) return DB_MIN;
220
+ const gain = Math.pow(slider / SLIDER_UNITY, EXPONENT);
221
+ const db = 20 * Math.log10(gain);
222
+ return Math.max(DB_MIN, Math.min(DB_MAX, db));
223
+ }
224
+ function dbToSlider(db) {
225
+ if (db <= DB_MIN) return 0;
226
+ if (db >= DB_MAX) return 1;
227
+ const gain = Math.pow(10, db / 20);
228
+ const slider = SLIDER_UNITY * Math.pow(gain, 1 / EXPONENT);
229
+ return Math.min(1, Math.max(0, slider));
230
+ }
231
+
232
+ // src/components/VolumeSlider.tsx
233
+ var import_jsx_runtime2 = require("react/jsx-runtime");
234
+ function formatDb(value) {
235
+ const db = sliderToDb(value);
236
+ if (db <= -60) return "-\u221E dB";
237
+ const sign = db >= 0 ? "+" : "";
238
+ return `${sign}${db.toFixed(1)} dB`;
239
+ }
240
+ function useDebouncedCallback(callback, delay) {
241
+ const timeoutRef = (0, import_react2.useRef)(null);
242
+ const callbackRef = (0, import_react2.useRef)(callback);
243
+ (0, import_react2.useEffect)(() => {
244
+ callbackRef.current = callback;
245
+ }, [callback]);
246
+ const debouncedCallback = (0, import_react2.useCallback)(
247
+ (...args) => {
248
+ if (timeoutRef.current) {
249
+ clearTimeout(timeoutRef.current);
250
+ }
251
+ timeoutRef.current = setTimeout(() => {
252
+ callbackRef.current(...args);
253
+ }, delay);
254
+ },
255
+ [delay]
256
+ );
257
+ (0, import_react2.useEffect)(() => {
258
+ return () => {
259
+ if (timeoutRef.current) {
260
+ clearTimeout(timeoutRef.current);
261
+ }
262
+ };
263
+ }, []);
264
+ return debouncedCallback;
265
+ }
266
+ var VolumeSlider = ({
267
+ value,
268
+ onChange,
269
+ disabled = false,
270
+ className = ""
271
+ }) => {
272
+ const [localValue, setLocalValue] = (0, import_react2.useState)(value);
273
+ const [isDragging, setIsDragging] = (0, import_react2.useState)(false);
274
+ (0, import_react2.useEffect)(() => {
275
+ if (!isDragging) {
276
+ setLocalValue(value);
277
+ }
278
+ }, [value, isDragging]);
279
+ const debouncedOnChange = useDebouncedCallback(onChange, 50);
280
+ const handleChange = (0, import_react2.useCallback)(
281
+ (e) => {
282
+ const newValue = parseFloat(e.target.value);
283
+ setLocalValue(newValue);
284
+ debouncedOnChange(newValue);
285
+ },
286
+ [debouncedOnChange]
287
+ );
288
+ const handleMouseDown = (0, import_react2.useCallback)(() => {
289
+ setIsDragging(true);
290
+ }, []);
291
+ const handleMouseUp = (0, import_react2.useCallback)(() => {
292
+ setIsDragging(false);
293
+ onChange(localValue);
294
+ }, [localValue, onChange]);
295
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
296
+ "div",
297
+ {
298
+ className: `flex items-center ${className}`,
299
+ title: `Volume: ${formatDb(localValue)}`,
300
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
301
+ "input",
302
+ {
303
+ type: "range",
304
+ min: "0",
305
+ max: "1",
306
+ step: "0.01",
307
+ value: localValue,
308
+ onChange: handleChange,
309
+ onMouseDown: handleMouseDown,
310
+ onMouseUp: handleMouseUp,
311
+ onTouchStart: handleMouseDown,
312
+ onTouchEnd: handleMouseUp,
313
+ disabled,
314
+ className: `
315
+ w-full h-1.5 rounded-full appearance-none cursor-pointer
316
+ bg-gray-700
317
+ disabled:opacity-50 disabled:cursor-not-allowed
318
+ [&::-webkit-slider-thumb]:appearance-none
319
+ [&::-webkit-slider-thumb]:w-3
320
+ [&::-webkit-slider-thumb]:h-3
321
+ [&::-webkit-slider-thumb]:rounded-full
322
+ [&::-webkit-slider-thumb]:bg-sas-accent
323
+ [&::-webkit-slider-thumb]:cursor-pointer
324
+ [&::-webkit-slider-thumb]:transition-transform
325
+ [&::-webkit-slider-thumb]:hover:scale-110
326
+ [&::-moz-range-thumb]:w-3
327
+ [&::-moz-range-thumb]:h-3
328
+ [&::-moz-range-thumb]:rounded-full
329
+ [&::-moz-range-thumb]:bg-sas-accent
330
+ [&::-moz-range-thumb]:border-0
331
+ [&::-moz-range-thumb]:cursor-pointer
332
+ `
333
+ }
334
+ )
335
+ }
336
+ );
337
+ };
338
+
339
+ // src/components/PanSlider.tsx
340
+ var import_react3 = require("react");
341
+ var import_jsx_runtime3 = require("react/jsx-runtime");
342
+ function toPanDisplay(value) {
343
+ if (Math.abs(value) < 0.02) {
344
+ return "Center";
345
+ }
346
+ const percent = Math.abs(Math.round(value * 100));
347
+ return value < 0 ? `L${percent}` : `R${percent}`;
348
+ }
349
+ function useDebouncedCallback2(callback, delay) {
350
+ const timeoutRef = (0, import_react3.useRef)(null);
351
+ const callbackRef = (0, import_react3.useRef)(callback);
352
+ (0, import_react3.useEffect)(() => {
353
+ callbackRef.current = callback;
354
+ }, [callback]);
355
+ const debouncedCallback = (0, import_react3.useCallback)(
356
+ (...args) => {
357
+ if (timeoutRef.current) {
358
+ clearTimeout(timeoutRef.current);
359
+ }
360
+ timeoutRef.current = setTimeout(() => {
361
+ callbackRef.current(...args);
362
+ }, delay);
363
+ },
364
+ [delay]
365
+ );
366
+ (0, import_react3.useEffect)(() => {
367
+ return () => {
368
+ if (timeoutRef.current) {
369
+ clearTimeout(timeoutRef.current);
370
+ }
371
+ };
372
+ }, []);
373
+ return debouncedCallback;
374
+ }
375
+ var PanSlider = ({
376
+ value,
377
+ onChange,
378
+ disabled = false,
379
+ className = ""
380
+ }) => {
381
+ const [localValue, setLocalValue] = (0, import_react3.useState)(value);
382
+ const [isDragging, setIsDragging] = (0, import_react3.useState)(false);
383
+ (0, import_react3.useEffect)(() => {
384
+ if (!isDragging) {
385
+ setLocalValue(value);
386
+ }
387
+ }, [value, isDragging]);
388
+ const debouncedOnChange = useDebouncedCallback2(onChange, 50);
389
+ const handleChange = (0, import_react3.useCallback)(
390
+ (e) => {
391
+ const newValue = parseFloat(e.target.value);
392
+ setLocalValue(newValue);
393
+ debouncedOnChange(newValue);
394
+ },
395
+ [debouncedOnChange]
396
+ );
397
+ const handleMouseDown = (0, import_react3.useCallback)(() => {
398
+ setIsDragging(true);
399
+ }, []);
400
+ const handleMouseUp = (0, import_react3.useCallback)(() => {
401
+ setIsDragging(false);
402
+ onChange(localValue);
403
+ }, [localValue, onChange]);
404
+ const handleDoubleClick = (0, import_react3.useCallback)(() => {
405
+ setLocalValue(0);
406
+ onChange(0);
407
+ }, [onChange]);
408
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
409
+ "div",
410
+ {
411
+ className: `flex items-center ${className}`,
412
+ title: `Pan: ${toPanDisplay(localValue)}`,
413
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
414
+ "input",
415
+ {
416
+ type: "range",
417
+ min: "-1",
418
+ max: "1",
419
+ step: "0.01",
420
+ value: localValue,
421
+ onChange: handleChange,
422
+ onMouseDown: handleMouseDown,
423
+ onMouseUp: handleMouseUp,
424
+ onTouchStart: handleMouseDown,
425
+ onTouchEnd: handleMouseUp,
426
+ onDoubleClick: handleDoubleClick,
427
+ disabled,
428
+ className: `
429
+ w-full h-1.5 rounded-full appearance-none cursor-pointer
430
+ bg-gray-700
431
+ disabled:opacity-50 disabled:cursor-not-allowed
432
+ [&::-webkit-slider-thumb]:appearance-none
433
+ [&::-webkit-slider-thumb]:w-3
434
+ [&::-webkit-slider-thumb]:h-3
435
+ [&::-webkit-slider-thumb]:rounded-full
436
+ [&::-webkit-slider-thumb]:bg-sas-accent
437
+ [&::-webkit-slider-thumb]:cursor-pointer
438
+ [&::-webkit-slider-thumb]:transition-transform
439
+ [&::-webkit-slider-thumb]:hover:scale-110
440
+ [&::-moz-range-thumb]:w-3
441
+ [&::-moz-range-thumb]:h-3
442
+ [&::-moz-range-thumb]:rounded-full
443
+ [&::-moz-range-thumb]:bg-sas-accent
444
+ [&::-moz-range-thumb]:border-0
445
+ [&::-moz-range-thumb]:cursor-pointer
446
+ `
447
+ }
448
+ )
449
+ }
450
+ );
451
+ };
452
+
453
+ // src/constants/fx-presets.ts
454
+ var EQ_PRESETS = {
455
+ presets: [
456
+ {
457
+ name: "The Smiley",
458
+ shortLabel: "SM",
459
+ params: {
460
+ "Low-shelf freq": 80,
461
+ "Low-shelf gain": 4,
462
+ "Low-shelf Q": 0.5,
463
+ "Mid freq 1": 500,
464
+ "Mid gain 1": -3,
465
+ "Mid Q 1": 0.7,
466
+ "Mid freq 2": 2e3,
467
+ "Mid gain 2": -2,
468
+ "Mid Q 2": 0.7,
469
+ "High-shelf freq": 12e3,
470
+ "High-shelf gain": 4,
471
+ "High-shelf Q": 0.5
472
+ }
473
+ },
474
+ {
475
+ name: "Telephone",
476
+ shortLabel: "TP",
477
+ params: {
478
+ "Low-shelf freq": 400,
479
+ "Low-shelf gain": -20,
480
+ "Low-shelf Q": 1,
481
+ "Mid freq 1": 1e3,
482
+ "Mid gain 1": 5,
483
+ "Mid Q 1": 2,
484
+ "Mid freq 2": 3e3,
485
+ "Mid gain 2": -5,
486
+ "Mid Q 2": 1,
487
+ "High-shelf freq": 5e3,
488
+ "High-shelf gain": -20,
489
+ "High-shelf Q": 1
490
+ }
491
+ },
492
+ {
493
+ name: "Warmth",
494
+ shortLabel: "WM",
495
+ params: {
496
+ "Low-shelf freq": 120,
497
+ "Low-shelf gain": 3,
498
+ "Low-shelf Q": 0.7,
499
+ "Mid freq 1": 400,
500
+ "Mid gain 1": 2,
501
+ "Mid Q 1": 1,
502
+ "Mid freq 2": 4e3,
503
+ "Mid gain 2": 0,
504
+ "Mid Q 2": 0.5,
505
+ "High-shelf freq": 1e4,
506
+ "High-shelf gain": -4,
507
+ "High-shelf Q": 0.5
508
+ }
509
+ },
510
+ {
511
+ name: "Vocal Air",
512
+ shortLabel: "VA",
513
+ params: {
514
+ "Low-shelf freq": 100,
515
+ "Low-shelf gain": -6,
516
+ "Low-shelf Q": 0.7,
517
+ "Mid freq 1": 300,
518
+ "Mid gain 1": -2,
519
+ "Mid Q 1": 1,
520
+ "Mid freq 2": 1500,
521
+ "Mid gain 2": 0,
522
+ "Mid Q 2": 0.5,
523
+ "High-shelf freq": 14e3,
524
+ "High-shelf gain": 6,
525
+ "High-shelf Q": 0.4
526
+ }
527
+ },
528
+ {
529
+ name: "De-Box",
530
+ shortLabel: "DB",
531
+ params: {
532
+ "Low-shelf freq": 60,
533
+ "Low-shelf gain": 0,
534
+ "Low-shelf Q": 0.5,
535
+ "Mid freq 1": 350,
536
+ "Mid gain 1": -5,
537
+ "Mid Q 1": 2,
538
+ "Mid freq 2": 800,
539
+ "Mid gain 2": -3,
540
+ "Mid Q 2": 2,
541
+ "High-shelf freq": 1e4,
542
+ "High-shelf gain": 0,
543
+ "High-shelf Q": 0.5
544
+ }
545
+ }
546
+ ],
547
+ mixParamName: null,
548
+ mixInterpolation: "gain-scale"
549
+ };
550
+ var COMPRESSOR_PRESETS = {
551
+ presets: [
552
+ {
553
+ name: "Vocal Leveler",
554
+ shortLabel: "VL",
555
+ params: { "Threshold": 0.251, "Ratio": 0.5, "Attack": 20, "Release": 200, "Output": 2 }
556
+ },
557
+ {
558
+ name: "Drum Smash",
559
+ shortLabel: "DS",
560
+ params: { "Threshold": 0.1, "Ratio": 0.1, "Attack": 0.5, "Release": 100, "Output": 8 }
561
+ },
562
+ {
563
+ name: "Bus Glue",
564
+ shortLabel: "BG",
565
+ params: { "Threshold": 0.316, "Ratio": 0.666, "Attack": 80, "Release": 150, "Output": 1 }
566
+ },
567
+ {
568
+ name: "Bass Anchor",
569
+ shortLabel: "BA",
570
+ params: { "Threshold": 0.177, "Ratio": 0.25, "Attack": 10, "Release": 250, "Output": 4 }
571
+ },
572
+ {
573
+ name: "Safety Net",
574
+ shortLabel: "SN",
575
+ params: { "Threshold": 0.891, "Ratio": 0, "Attack": 0.3, "Release": 50, "Output": 0 }
576
+ }
577
+ ],
578
+ mixParamName: null,
579
+ mixInterpolation: "ratio-scale"
580
+ };
581
+ var CHORUS_PRESETS = {
582
+ presets: [
583
+ {
584
+ name: "Dimension",
585
+ shortLabel: "DM",
586
+ params: {},
587
+ xmlStateParams: { depthMs: 1.5, speedHz: 0.5, width: 1, mixProportion: 0.5 }
588
+ },
589
+ {
590
+ name: "80s Crystal",
591
+ shortLabel: "80",
592
+ params: {},
593
+ xmlStateParams: { depthMs: 4, speedHz: 2.5, width: 0.8, mixProportion: 0.4 }
594
+ },
595
+ {
596
+ name: "Sea Sick",
597
+ shortLabel: "SS",
598
+ params: {},
599
+ xmlStateParams: { depthMs: 7, speedHz: 0.8, width: 0.3, mixProportion: 1 }
600
+ },
601
+ {
602
+ name: "Pseudo-Leslie",
603
+ shortLabel: "PL",
604
+ params: {},
605
+ xmlStateParams: { depthMs: 2, speedHz: 6, width: 0.9, mixProportion: 0.7 }
606
+ },
607
+ {
608
+ name: "Thickener",
609
+ shortLabel: "TK",
610
+ params: {},
611
+ xmlStateParams: { depthMs: 1, speedHz: 0.2, width: 1, mixProportion: 0.3 }
612
+ }
613
+ ],
614
+ mixParamName: null,
615
+ mixXmlAttr: "mixProportion",
616
+ mixInterpolation: "direct"
617
+ };
618
+ var PHASER_PRESETS = {
619
+ presets: [
620
+ {
621
+ name: "Slow Burn",
622
+ shortLabel: "SB",
623
+ params: {},
624
+ xmlStateParams: { depth: 6, rate: 0.1, feedback: 0.3 }
625
+ },
626
+ {
627
+ name: "Funky Quack",
628
+ shortLabel: "FQ",
629
+ params: {},
630
+ xmlStateParams: { depth: 3, rate: 2, feedback: 0.8 }
631
+ },
632
+ {
633
+ name: "Jet Plane",
634
+ shortLabel: "JP",
635
+ params: {},
636
+ xmlStateParams: { depth: 8, rate: 0.2, feedback: 0.9 }
637
+ },
638
+ {
639
+ name: "Underwater",
640
+ shortLabel: "UW",
641
+ params: {},
642
+ xmlStateParams: { depth: 1.5, rate: 4, feedback: 0.1 }
643
+ },
644
+ {
645
+ name: "Static Notch",
646
+ shortLabel: "ST",
647
+ params: {},
648
+ xmlStateParams: { depth: 2, rate: 0.05, feedback: 0.6 }
649
+ }
650
+ ],
651
+ mixParamName: null,
652
+ mixXmlAttr: "depth",
653
+ mixInterpolation: "direct"
654
+ };
655
+ var DELAY_PRESETS = {
656
+ presets: [
657
+ {
658
+ name: "Vocal Slap",
659
+ shortLabel: "VS",
660
+ fixedLengthMs: 110,
661
+ params: { "Feedback": -20, "Mix proportion": 0.25 }
662
+ },
663
+ {
664
+ name: "Grand Canyon",
665
+ shortLabel: "GC",
666
+ noteMultiplier: 1,
667
+ params: { "Feedback": -4, "Mix proportion": 0.45 }
668
+ },
669
+ {
670
+ name: "Wide Doubler",
671
+ shortLabel: "WD",
672
+ fixedLengthMs: 25,
673
+ params: { "Feedback": -30, "Mix proportion": 0.5 }
674
+ },
675
+ {
676
+ name: "Dub Echo",
677
+ shortLabel: "DE",
678
+ noteMultiplier: 0.6,
679
+ params: { "Feedback": -1.5, "Mix proportion": 0.4 }
680
+ },
681
+ {
682
+ name: "Rhythmic Wash",
683
+ shortLabel: "RW",
684
+ noteMultiplier: 0.75,
685
+ params: { "Feedback": -8, "Mix proportion": 0.2 }
686
+ }
687
+ ],
688
+ mixParamName: "Mix proportion",
689
+ mixInterpolation: "direct"
690
+ };
691
+ var REVERB_PRESETS = {
692
+ presets: [
693
+ {
694
+ name: "Drum Room",
695
+ shortLabel: "DR",
696
+ params: { "Room Size": 0.2, "Damping": 0.2, "Wet Level": 0.15, "Dry Level": 0.5, "Width": 0.8 }
697
+ },
698
+ {
699
+ name: "Vocal Hall",
700
+ shortLabel: "VH",
701
+ params: { "Room Size": 0.8, "Damping": 0.6, "Wet Level": 0.25, "Dry Level": 0.5, "Width": 1 }
702
+ },
703
+ {
704
+ name: "Cathedral",
705
+ shortLabel: "CT",
706
+ params: { "Room Size": 1, "Damping": 0.1, "Wet Level": 0.333, "Dry Level": 0.2, "Width": 1 }
707
+ },
708
+ {
709
+ name: "Tile Bathroom",
710
+ shortLabel: "TB",
711
+ params: { "Room Size": 0.15, "Damping": 0, "Wet Level": 0.2, "Dry Level": 0.5, "Width": 0.5 }
712
+ },
713
+ {
714
+ name: "Vintage Plate",
715
+ shortLabel: "VP",
716
+ params: { "Room Size": 0.4, "Damping": 1, "Wet Level": 0.2, "Dry Level": 0.5, "Width": 1 }
717
+ }
718
+ ],
719
+ mixParamName: "Wet Level",
720
+ mixInterpolation: "direct"
721
+ };
722
+ var FX_PRESET_CONFIGS = {
723
+ eq: EQ_PRESETS,
724
+ compressor: COMPRESSOR_PRESETS,
725
+ chorus: CHORUS_PRESETS,
726
+ phaser: PHASER_PRESETS,
727
+ delay: DELAY_PRESETS,
728
+ reverb: REVERB_PRESETS
729
+ };
730
+
731
+ // src/components/FxToggleBar.tsx
732
+ var import_jsx_runtime4 = require("react/jsx-runtime");
733
+ var FX_COLORS = {
734
+ eq: "bg-blue-500",
735
+ compressor: "bg-orange-500",
736
+ chorus: "bg-teal-500",
737
+ phaser: "bg-purple-500",
738
+ delay: "bg-green-500",
739
+ reverb: "bg-cyan-500"
740
+ };
741
+ var FxToggleBar = ({
742
+ trackId,
743
+ fxState,
744
+ onToggle,
745
+ onPresetChange,
746
+ onDryWetChange,
747
+ disabled = false
748
+ }) => {
749
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-col gap-1", "data-testid": "fx-toggle-bar", children: FX_CATEGORIES.map((category) => {
750
+ const detail = fxState[category];
751
+ const isActive = detail.enabled;
752
+ const label = FX_DISPLAY_LABELS[category];
753
+ const activeColor = FX_COLORS[category];
754
+ const config = FX_PRESET_CONFIGS[category];
755
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-0.5", children: [
756
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
757
+ "button",
758
+ {
759
+ "data-testid": `fx-toggle-${category}`,
760
+ disabled,
761
+ onClick: () => onToggle(trackId, category, !isActive),
762
+ className: `w-14 py-0.5 text-[10px] font-semibold rounded-sm transition-colors leading-none flex-shrink-0 text-center ${disabled ? "bg-sas-panel text-sas-muted/30 cursor-not-allowed" : isActive ? `${activeColor} text-white` : "bg-sas-panel-alt text-sas-muted/60 hover:bg-sas-border hover:text-sas-muted"}`,
763
+ title: `${isActive ? "Disable" : "Enable"} ${category.toUpperCase()}`,
764
+ children: label
765
+ }
766
+ ),
767
+ config.presets.map((preset, idx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
768
+ "button",
769
+ {
770
+ "data-testid": `fx-preset-${category}-${idx}`,
771
+ disabled: disabled || !isActive,
772
+ onClick: () => onPresetChange(trackId, category, idx),
773
+ className: `w-5 h-5 text-[9px] font-medium rounded-sm transition-colors leading-none flex-shrink-0 ${disabled || !isActive ? "bg-sas-panel text-sas-muted/20 cursor-not-allowed" : detail.presetIndex === idx ? `${activeColor} text-white` : "bg-sas-panel-alt text-sas-muted/50 hover:bg-sas-border hover:text-sas-muted"}`,
774
+ title: preset.name,
775
+ children: idx + 1
776
+ },
777
+ idx
778
+ )),
779
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
780
+ "input",
781
+ {
782
+ type: "range",
783
+ "data-testid": `fx-drywet-${category}`,
784
+ min: "0",
785
+ max: "100",
786
+ value: Math.round(detail.dryWet * 100),
787
+ disabled: disabled || !isActive,
788
+ onChange: (e) => onDryWetChange(trackId, category, Number(e.target.value) / 100),
789
+ className: "flex-1 min-w-[30px] h-3 accent-sas-accent disabled:opacity-30 cursor-pointer disabled:cursor-not-allowed",
790
+ title: `Dry/Wet: ${Math.round(detail.dryWet * 100)}%`
791
+ }
792
+ ),
793
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-[8px] text-sas-muted/50 w-6 text-right flex-shrink-0", children: [
794
+ Math.round(detail.dryWet * 100),
795
+ "%"
796
+ ] })
797
+ ] }, category);
798
+ }) });
799
+ };
800
+
801
+ // src/components/SorceryProgressBar.tsx
802
+ var import_react4 = require("react");
803
+ var import_jsx_runtime5 = require("react/jsx-runtime");
804
+ function calculateTimeBasedTarget(elapsedMs, estimatedDurationMs) {
805
+ const t = elapsedMs / estimatedDurationMs;
806
+ if (t <= 0) return 0;
807
+ if (t <= 1) {
808
+ return 90 * (1 - Math.pow(1 - t, 2.5));
809
+ }
810
+ const overshootRatio = (elapsedMs - estimatedDurationMs) / estimatedDurationMs;
811
+ return 90 + 5 * (1 - Math.exp(-overshootRatio * 3));
812
+ }
813
+ function calculateNextProgress(currentProgress) {
814
+ if (currentProgress < 20) {
815
+ return currentProgress + Math.random() * 10 + 5;
816
+ }
817
+ if (currentProgress < 60) {
818
+ return currentProgress + Math.random() * 5 + 2;
819
+ }
820
+ if (currentProgress < 95) {
821
+ const remaining = 95 - currentProgress;
822
+ const increment = remaining * (Math.random() * 0.2 + 0.1);
823
+ return currentProgress + Math.max(increment, 0.1);
824
+ }
825
+ return 95;
826
+ }
827
+ function calculateNextTickInterval(progress) {
828
+ if (progress < 30) {
829
+ return Math.random() * 200 + 150;
830
+ }
831
+ if (progress < 70) {
832
+ return Math.random() * 300 + 200;
833
+ }
834
+ return Math.random() * 600 + 400;
835
+ }
836
+ var TIME_BASED_TICK_MIN = 200;
837
+ var TIME_BASED_TICK_RANGE = 100;
838
+ function SorceryProgressBar({
839
+ isLoading,
840
+ statusText = "CONJURING...",
841
+ completeText = "COMPLETE",
842
+ onComplete,
843
+ heightClass = "h-10",
844
+ initialProgress = 0,
845
+ onProgressChange,
846
+ estimatedDurationMs
847
+ }) {
848
+ const [progress, setProgress] = (0, import_react4.useState)(initialProgress);
849
+ const timerRef = (0, import_react4.useRef)(null);
850
+ const isLoadingRef = (0, import_react4.useRef)(false);
851
+ const hasStartedRef = (0, import_react4.useRef)(false);
852
+ const startTimeRef = (0, import_react4.useRef)(0);
853
+ const onProgressChangeRef = (0, import_react4.useRef)(onProgressChange);
854
+ const onCompleteRef = (0, import_react4.useRef)(onComplete);
855
+ onProgressChangeRef.current = onProgressChange;
856
+ onCompleteRef.current = onComplete;
857
+ const initialProgressRef = (0, import_react4.useRef)(initialProgress);
858
+ initialProgressRef.current = initialProgress;
859
+ const estimatedDurationMsRef = (0, import_react4.useRef)(estimatedDurationMs);
860
+ estimatedDurationMsRef.current = estimatedDurationMs;
861
+ (0, import_react4.useEffect)(() => {
862
+ const wasLoading = isLoadingRef.current;
863
+ isLoadingRef.current = isLoading;
864
+ if (isLoading && !wasLoading) {
865
+ hasStartedRef.current = true;
866
+ startTimeRef.current = Date.now();
867
+ const startProgress = initialProgressRef.current > 0 ? initialProgressRef.current : 0;
868
+ setProgress(startProgress);
869
+ const duration = estimatedDurationMsRef.current;
870
+ if (duration && duration > 0) {
871
+ const tick = () => {
872
+ setProgress((prev) => {
873
+ const elapsed = Date.now() - startTimeRef.current;
874
+ const target = calculateTimeBasedTarget(elapsed, duration);
875
+ const jitter = (Math.random() - 0.5) * 1;
876
+ const next = Math.min(Math.max(target + jitter, prev + 0.05), 95);
877
+ onProgressChangeRef.current?.(next);
878
+ timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN + Math.random() * TIME_BASED_TICK_RANGE);
879
+ return next;
880
+ });
881
+ };
882
+ timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN);
883
+ } else {
884
+ const tick = () => {
885
+ setProgress((prev) => {
886
+ if (prev >= 95) {
887
+ timerRef.current = setTimeout(tick, 1e3);
888
+ return 95;
889
+ }
890
+ const next = Math.min(calculateNextProgress(prev), 95);
891
+ onProgressChangeRef.current?.(next);
892
+ const interval = calculateNextTickInterval(next);
893
+ timerRef.current = setTimeout(tick, interval);
894
+ return next;
895
+ });
896
+ };
897
+ const firstInterval = calculateNextTickInterval(startProgress);
898
+ timerRef.current = setTimeout(tick, firstInterval);
899
+ }
900
+ } else if (!isLoading && wasLoading && hasStartedRef.current) {
901
+ if (timerRef.current) {
902
+ clearTimeout(timerRef.current);
903
+ timerRef.current = null;
904
+ }
905
+ setProgress(100);
906
+ onProgressChangeRef.current?.(100);
907
+ onCompleteRef.current?.();
908
+ hasStartedRef.current = false;
909
+ }
910
+ return () => {
911
+ if (timerRef.current) {
912
+ clearTimeout(timerRef.current);
913
+ timerRef.current = null;
914
+ }
915
+ };
916
+ }, [isLoading]);
917
+ if (!isLoading && progress === 0) {
918
+ return null;
919
+ }
920
+ const displayProgress = Math.floor(progress);
921
+ const isComplete = !isLoading && progress === 100;
922
+ const transitionDuration = progress < 50 ? "300ms" : progress < 80 ? "500ms" : "700ms";
923
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
924
+ "div",
925
+ {
926
+ className: `relative w-full ${heightClass} bg-sas-panel-alt border border-sas-border rounded-sm overflow-hidden shadow-inner`,
927
+ children: [
928
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
929
+ "div",
930
+ {
931
+ className: `
932
+ h-full
933
+ bg-gradient-to-r from-sas-accent/70 to-sas-accent
934
+ shadow-glow-soft
935
+ sorcery-progress-fill
936
+ animate-progress-stripes
937
+ ${progress > 70 ? "animate-progress-pulse" : ""}
938
+ transition-all ease-out
939
+ `,
940
+ style: {
941
+ width: `${progress}%`,
942
+ transitionDuration
943
+ }
944
+ }
945
+ ),
946
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute inset-0 flex items-center justify-center", children: isLoading && progress < 100 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider", children: [
947
+ statusText,
948
+ " ",
949
+ displayProgress,
950
+ "%"
951
+ ] }) : isComplete ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider", children: completeText }) : null }),
952
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
953
+ "div",
954
+ {
955
+ className: "absolute inset-0 pointer-events-none opacity-10",
956
+ style: {
957
+ backgroundImage: `repeating-linear-gradient(
958
+ to bottom,
959
+ transparent,
960
+ transparent 2px,
961
+ rgba(0, 0, 0, 0.3) 2px,
962
+ rgba(0, 0, 0, 0.3) 4px
963
+ )`
964
+ }
965
+ }
966
+ )
967
+ ]
968
+ }
969
+ );
970
+ }
971
+
972
+ // src/components/TrackRow.tsx
973
+ var import_jsx_runtime6 = require("react/jsx-runtime");
974
+ function TrackRow({
975
+ track,
976
+ prompt,
977
+ runtimeState,
978
+ fxDetailState,
979
+ fxDrawerOpen,
980
+ isGenerating = false,
981
+ isAuthenticated = false,
982
+ error,
983
+ hasMidi = false,
984
+ generationProgress = 0,
985
+ estimatedGenerationMs = 15e3,
986
+ onPromptChange,
987
+ onGenerate,
988
+ onShuffle,
989
+ onCopy,
990
+ onDelete,
991
+ contentSlot,
992
+ onMuteToggle,
993
+ onSoloToggle,
994
+ onVolumeChange,
995
+ onPanChange,
996
+ onFxToggle,
997
+ onFxPresetChange,
998
+ onFxDryWetChange,
999
+ onToggleFxDrawer,
1000
+ onProgressChange,
1001
+ accentColor = "#A78BFA",
1002
+ instrumentName,
1003
+ instrumentMissing,
1004
+ instrumentDrawerOpen,
1005
+ onToggleInstrumentDrawer,
1006
+ availableInstruments,
1007
+ currentInstrumentPluginId,
1008
+ onInstrumentSelect,
1009
+ instrumentsLoading,
1010
+ onRefreshInstruments
1011
+ }) {
1012
+ const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;
1013
+ const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);
1014
+ const hasFxActive = Object.values(fxDetailState).some(
1015
+ (d) => d.enabled
1016
+ );
1017
+ const handleKeyDown = (e) => {
1018
+ if (e.key === "Enter" && !e.shiftKey && onGenerate) {
1019
+ e.preventDefault();
1020
+ onGenerate();
1021
+ }
1022
+ };
1023
+ const borderColorStyle = needsGeneration ? void 0 : accentColor;
1024
+ const borderClass = needsGeneration ? "border-amber-400 animate-pulse" : "border-sas-border";
1025
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { "data-testid": "sdk-track-row-wrapper", className: "w-full", children: [
1026
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1027
+ "div",
1028
+ {
1029
+ "data-testid": "sdk-track-row",
1030
+ className: `relative flex items-stretch gap-1 p-2 rounded-sm border w-full overflow-hidden ${borderClass} bg-sas-panel-alt`,
1031
+ style: {
1032
+ borderLeftColor: needsGeneration ? "#f59e0b" : borderColorStyle,
1033
+ borderLeftWidth: "3px"
1034
+ },
1035
+ children: [
1036
+ isGenerating && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute left-0 top-0 bottom-0 right-44 z-20", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1037
+ SorceryProgressBar,
1038
+ {
1039
+ isLoading: true,
1040
+ statusText: "CONJURING MIDI...",
1041
+ heightClass: "h-full",
1042
+ initialProgress: generationProgress,
1043
+ onProgressChange,
1044
+ estimatedDurationMs: estimatedGenerationMs
1045
+ }
1046
+ ) }),
1047
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col flex-1 min-w-0 relative z-10", children: [
1048
+ contentSlot ? contentSlot : onPromptChange ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1049
+ "input",
1050
+ {
1051
+ type: "text",
1052
+ "data-testid": "sdk-prompt-input",
1053
+ value: prompt ?? "",
1054
+ onChange: (e) => onPromptChange(e.target.value),
1055
+ onKeyDown: handleKeyDown,
1056
+ placeholder: "Describe your part...",
1057
+ disabled: isGenerating,
1058
+ className: "sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed"
1059
+ }
1060
+ ) : null,
1061
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-2 mt-1", children: [
1062
+ track.name && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]", title: track.name, children: track.name }),
1063
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "vol:" }),
1064
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1065
+ VolumeSlider,
1066
+ {
1067
+ value: currentVolume,
1068
+ onChange: onVolumeChange,
1069
+ disabled: isGenerating,
1070
+ className: "flex-1 min-w-[40px]"
1071
+ }
1072
+ ),
1073
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: "pan:" }),
1074
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1075
+ PanSlider,
1076
+ {
1077
+ value: currentPan,
1078
+ onChange: onPanChange,
1079
+ disabled: isGenerating,
1080
+ className: "w-10 flex-shrink-0"
1081
+ }
1082
+ )
1083
+ ] })
1084
+ ] }),
1085
+ error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1086
+ "div",
1087
+ {
1088
+ "data-testid": "sdk-error-indicator",
1089
+ className: "flex-shrink-0 relative z-10 self-stretch flex items-center px-1 group cursor-help",
1090
+ title: error,
1091
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "relative", children: [
1092
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1093
+ import_lucide_react.AlertCircle,
1094
+ {
1095
+ className: "w-5 h-5 text-red-500 animate-pulse",
1096
+ strokeWidth: 2.5
1097
+ }
1098
+ ),
1099
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("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 })
1100
+ ] })
1101
+ }
1102
+ ),
1103
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center", children: [
1104
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-1 items-center", children: [
1105
+ onGenerate && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1106
+ "button",
1107
+ {
1108
+ "data-testid": "sdk-generate-button",
1109
+ onClick: onGenerate,
1110
+ disabled: !isAuthenticated || isGenerating || !prompt?.trim(),
1111
+ className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!isAuthenticated || isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : needsGeneration ? "bg-amber-500/30 border-amber-500 text-amber-400 hover:bg-amber-500 hover:text-sas-bg animate-pulse" : prompt?.trim() ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
1112
+ title: !isAuthenticated ? "Please log in" : isGenerating ? "Generating..." : "Generate MIDI",
1113
+ children: "Create"
1114
+ }
1115
+ ),
1116
+ onCopy && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1117
+ "button",
1118
+ {
1119
+ "data-testid": "sdk-copy-button",
1120
+ onClick: onCopy,
1121
+ disabled: !hasMidi || isGenerating,
1122
+ className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating ? "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"}`,
1123
+ title: hasMidi ? "Duplicate track with different preset" : "Generate MIDI first",
1124
+ children: "Copy"
1125
+ }
1126
+ ),
1127
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1128
+ "button",
1129
+ {
1130
+ "data-testid": "sdk-mute-button",
1131
+ onClick: onMuteToggle,
1132
+ disabled: isGenerating,
1133
+ 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" : isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
1134
+ title: isMuted ? "Unmute track" : "Mute track",
1135
+ children: "M"
1136
+ }
1137
+ ),
1138
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1139
+ "button",
1140
+ {
1141
+ "data-testid": "sdk-delete-button",
1142
+ onClick: onDelete,
1143
+ className: "text-sas-danger/70 hover:text-sas-danger px-1 py-0.5 transition-colors text-sm",
1144
+ title: "Delete track",
1145
+ children: "x"
1146
+ }
1147
+ )
1148
+ ] }),
1149
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-1 items-center", children: [
1150
+ onShuffle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1151
+ "button",
1152
+ {
1153
+ "data-testid": "sdk-shuffle-button",
1154
+ onClick: onShuffle,
1155
+ disabled: !hasMidi || isGenerating,
1156
+ className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating ? "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"}`,
1157
+ title: hasMidi ? "Re-roll sound (keep MIDI)" : "Generate MIDI first",
1158
+ children: "Shuffle"
1159
+ }
1160
+ ),
1161
+ onToggleFxDrawer && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1162
+ "button",
1163
+ {
1164
+ "data-testid": "sdk-fx-button",
1165
+ onClick: onToggleFxDrawer,
1166
+ disabled: isGenerating,
1167
+ className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : fxDrawerOpen ? "bg-sas-accent border-sas-accent text-sas-bg" : hasFxActive ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
1168
+ title: fxDrawerOpen ? "Hide FX controls" : "Show FX controls",
1169
+ children: "FX"
1170
+ }
1171
+ ),
1172
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1173
+ "button",
1174
+ {
1175
+ "data-testid": "sdk-solo-button",
1176
+ onClick: onSoloToggle,
1177
+ disabled: isGenerating,
1178
+ 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" : isSoloed ? "bg-yellow-500 text-black" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
1179
+ title: isSoloed ? "Unsolo track" : "Solo track",
1180
+ children: "S"
1181
+ }
1182
+ ),
1183
+ onToggleInstrumentDrawer && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1184
+ "button",
1185
+ {
1186
+ "data-testid": "sdk-plugin-button",
1187
+ onClick: onToggleInstrumentDrawer,
1188
+ disabled: isGenerating,
1189
+ 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" : instrumentDrawerOpen ? "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"}`,
1190
+ title: `Plugin: ${instrumentName ?? "Surge XT"}${instrumentMissing ? " (missing)" : ""}`,
1191
+ children: "P"
1192
+ }
1193
+ )
1194
+ ] })
1195
+ ] })
1196
+ ]
1197
+ }
1198
+ ),
1199
+ fxDrawerOpen && !instrumentDrawerOpen && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { "data-testid": "sdk-fx-drawer", className: "border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2 max-h-[180px] overflow-y-auto", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1200
+ FxToggleBar,
1201
+ {
1202
+ trackId: track.id,
1203
+ fxState: fxDetailState,
1204
+ onToggle: (_trackId, category, enabled) => onFxToggle?.(category, enabled),
1205
+ onPresetChange: (_trackId, category, presetIndex) => onFxPresetChange?.(category, presetIndex),
1206
+ onDryWetChange: (_trackId, category, value) => onFxDryWetChange?.(category, value),
1207
+ disabled: isGenerating
1208
+ }
1209
+ ) }),
1210
+ instrumentDrawerOpen && !fxDrawerOpen && availableInstruments && onInstrumentSelect && onRefreshInstruments && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { "data-testid": "sdk-instrument-drawer", className: "border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1211
+ InstrumentDrawer,
1212
+ {
1213
+ instruments: availableInstruments,
1214
+ currentPluginId: currentInstrumentPluginId ?? null,
1215
+ isLoading: instrumentsLoading ?? false,
1216
+ onSelect: onInstrumentSelect,
1217
+ onRefresh: onRefreshInstruments
1218
+ }
1219
+ ) })
1220
+ ] });
1221
+ }
1222
+
1223
+ // src/hooks/useSceneState.ts
1224
+ var import_react5 = require("react");
1225
+ function useSceneState(activeSceneId, initialValue) {
1226
+ const [stateMap, setStateMap] = (0, import_react5.useState)(() => /* @__PURE__ */ new Map());
1227
+ const activeSceneIdRef = (0, import_react5.useRef)(activeSceneId);
1228
+ activeSceneIdRef.current = activeSceneId;
1229
+ const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
1230
+ const setForCurrentScene = (0, import_react5.useCallback)((value) => {
1231
+ const sid = activeSceneIdRef.current;
1232
+ if (sid === null) return;
1233
+ setStateMap((prev) => {
1234
+ const current = prev.has(sid) ? prev.get(sid) : initialValue;
1235
+ const next = typeof value === "function" ? value(current) : value;
1236
+ const newMap = new Map(prev);
1237
+ newMap.set(sid, next);
1238
+ return newMap;
1239
+ });
1240
+ }, [initialValue]);
1241
+ const setForScene = (0, import_react5.useCallback)((sceneId, value) => {
1242
+ setStateMap((prev) => {
1243
+ const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
1244
+ const next = typeof value === "function" ? value(current) : value;
1245
+ const newMap = new Map(prev);
1246
+ newMap.set(sceneId, next);
1247
+ return newMap;
1248
+ });
1249
+ }, [initialValue]);
1250
+ return [currentValue, setForCurrentScene, setForScene];
1251
+ }
1252
+
1253
+ // src/constants/instrument-roles.ts
1254
+ var VALID_INSTRUMENT_ROLES = [
1255
+ "bass",
1256
+ "kick",
1257
+ "snare",
1258
+ "hat",
1259
+ "808",
1260
+ "percussion",
1261
+ "lead",
1262
+ "pad",
1263
+ "keys",
1264
+ "piano",
1265
+ "organ",
1266
+ "pluck",
1267
+ "strings",
1268
+ "brass",
1269
+ "winds",
1270
+ "bell",
1271
+ "mallet",
1272
+ "guitar",
1273
+ "synth",
1274
+ "atmosphere",
1275
+ "drone",
1276
+ "rhythm",
1277
+ "soundscape",
1278
+ "vocal",
1279
+ "fx"
1280
+ ];
1281
+
1282
+ // src/constants/sdk-version.ts
1283
+ var PLUGIN_SDK_VERSION = "1.0.0";
1284
+ // Annotate the CommonJS export names for ESM import in node:
1285
+ 0 && (module.exports = {
1286
+ DB_MAX,
1287
+ DB_MIN,
1288
+ DEFAULT_FX_CATEGORY_DETAIL,
1289
+ DEFAULT_FX_DRY_WET,
1290
+ EMPTY_FX_DETAIL_STATE,
1291
+ EMPTY_FX_STATE,
1292
+ FX_CATEGORIES,
1293
+ FX_CHAIN_ORDER,
1294
+ FX_DISPLAY_LABELS,
1295
+ FX_ENGINE_PLUGIN_NAMES,
1296
+ FX_PRESET_CONFIGS,
1297
+ FxToggleBar,
1298
+ InstrumentDrawer,
1299
+ PLUGIN_SDK_VERSION,
1300
+ PanSlider,
1301
+ PluginError,
1302
+ SLIDER_UNITY,
1303
+ SorceryProgressBar,
1304
+ TrackRow,
1305
+ VALID_INSTRUMENT_ROLES,
1306
+ VolumeSlider,
1307
+ calculateTimeBasedTarget,
1308
+ dbToSlider,
1309
+ sliderToDb,
1310
+ useSceneState
1311
+ });
1312
+ //# sourceMappingURL=index.js.map