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