@signalsandsorcery/plugin-sdk 2.28.1 → 2.34.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.d.mts +234 -3
- package/dist/index.d.ts +234 -3
- package/dist/index.js +831 -168
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +815 -169
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AUDIO_EFFECTS: () => AUDIO_EFFECTS,
|
|
34
|
+
AUDIO_EFFECT_LABEL: () => AUDIO_EFFECT_LABEL,
|
|
33
35
|
ConfirmDialog: () => ConfirmDialog,
|
|
34
36
|
CrossfadeModal: () => CrossfadeModal,
|
|
35
37
|
CrossfadeTrackRow: () => CrossfadeTrackRow,
|
|
@@ -68,34 +70,49 @@ __export(index_exports, {
|
|
|
68
70
|
ScrollingWaveform: () => ScrollingWaveform,
|
|
69
71
|
SorceryProgressBar: () => SorceryProgressBar,
|
|
70
72
|
TEXTURAL_ROLES: () => TEXTURAL_ROLES,
|
|
73
|
+
TRANSITION_DESIGNER_DRAFT_KEY: () => TRANSITION_DESIGNER_DRAFT_KEY,
|
|
71
74
|
TrackDrawer: () => TrackDrawer,
|
|
72
75
|
TrackMeterStrip: () => TrackMeterStrip,
|
|
73
76
|
TrackRow: () => TrackRow,
|
|
77
|
+
TransitionDesigner: () => TransitionDesigner,
|
|
74
78
|
VolumeSlider: () => VolumeSlider,
|
|
75
79
|
WaveformView: () => WaveformView,
|
|
76
80
|
analyzeWavPeak: () => analyzeWavPeak,
|
|
81
|
+
asAudioEffect: () => asAudioEffect,
|
|
77
82
|
asCrossfadeMeta: () => asCrossfadeMeta,
|
|
78
83
|
asFadeMeta: () => asFadeMeta,
|
|
84
|
+
asTransitionDesignerDraft: () => asTransitionDesignerDraft,
|
|
79
85
|
buildCrossfadeInpaintPrompt: () => buildCrossfadeInpaintPrompt,
|
|
80
86
|
buildCrossfadeVolumeCurves: () => buildCrossfadeVolumeCurves,
|
|
81
87
|
buildFadeVolumeCurve: () => buildFadeVolumeCurve,
|
|
88
|
+
buildRowSlots: () => buildRowSlots,
|
|
82
89
|
calculateTimeBasedTarget: () => calculateTimeBasedTarget,
|
|
83
90
|
cellToPx: () => cellToPx,
|
|
84
91
|
centerScrollTop: () => centerScrollTop,
|
|
85
92
|
computePeaks: () => computePeaks,
|
|
93
|
+
dbIdsFromKeys: () => dbIdsFromKeys,
|
|
86
94
|
dbToSlider: () => dbToSlider,
|
|
87
95
|
defaultFadeGesture: () => defaultFadeGesture,
|
|
88
96
|
drawWaveform: () => drawWaveform,
|
|
89
97
|
formatConcurrentTracks: () => formatConcurrentTracks,
|
|
98
|
+
hashString: () => hashString,
|
|
90
99
|
moveItem: () => moveItem,
|
|
100
|
+
normalizeSlots: () => normalizeSlots,
|
|
101
|
+
padPair: () => padPair,
|
|
102
|
+
padSlots: () => padSlots,
|
|
91
103
|
parseCrossfadePairs: () => parseCrossfadePairs,
|
|
92
104
|
parseFades: () => parseFades,
|
|
93
105
|
pickTopKWeighted: () => pickTopKWeighted,
|
|
94
106
|
pitchToName: () => pitchToName,
|
|
95
107
|
pxToCell: () => pxToCell,
|
|
108
|
+
reconcileSlots: () => reconcileSlots,
|
|
96
109
|
resizeNoteDuration: () => resizeNoteDuration,
|
|
110
|
+
rowKey: () => rowKey,
|
|
111
|
+
rowType: () => rowType,
|
|
97
112
|
scorePromptMatch: () => scorePromptMatch,
|
|
98
113
|
sliderToDb: () => sliderToDb,
|
|
114
|
+
slotsEqual: () => slotsEqual,
|
|
115
|
+
soundIdentity: () => soundIdentity,
|
|
99
116
|
synthesizeCuePoints: () => synthesizeCuePoints,
|
|
100
117
|
tokenizePrompt: () => tokenizePrompt,
|
|
101
118
|
transposeNotes: () => transposeNotes,
|
|
@@ -1455,6 +1472,7 @@ var LevelMeter = ({
|
|
|
1455
1472
|
|
|
1456
1473
|
// src/hooks/useTrackLevels.ts
|
|
1457
1474
|
var import_react5 = require("react");
|
|
1475
|
+
var meterDiagRLast = /* @__PURE__ */ new Map();
|
|
1458
1476
|
var POLL_INTERVAL_MS = 33;
|
|
1459
1477
|
var HIDDEN_RECHECK_MS = 250;
|
|
1460
1478
|
var METER_FLOOR_DB = -120;
|
|
@@ -1586,6 +1604,11 @@ function useTrackMeter(handle, trackId) {
|
|
|
1586
1604
|
}
|
|
1587
1605
|
const update = () => {
|
|
1588
1606
|
const level = handle.getLevel(trackId);
|
|
1607
|
+
const dNow = Date.now();
|
|
1608
|
+
if ((meterDiagRLast.get(trackId) ?? 0) < dNow - 3e3) {
|
|
1609
|
+
meterDiagRLast.set(trackId, dNow);
|
|
1610
|
+
console.log(`[MeterDiagR] lookup trackId=${trackId} \u2192 ${level === null ? "MISS (no level for this id)" : "hit"}`);
|
|
1611
|
+
}
|
|
1589
1612
|
const now = performance.now();
|
|
1590
1613
|
const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1e3) : 0;
|
|
1591
1614
|
lastTickRef.current = now;
|
|
@@ -2287,8 +2310,7 @@ function TrackRow({
|
|
|
2287
2310
|
{
|
|
2288
2311
|
"data-testid": "sdk-mute-button",
|
|
2289
2312
|
onClick: onMuteToggle,
|
|
2290
|
-
|
|
2291
|
-
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"}`,
|
|
2313
|
+
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
2292
2314
|
title: isMuted ? "Unmute track" : "Mute track",
|
|
2293
2315
|
children: "M"
|
|
2294
2316
|
}
|
|
@@ -2539,6 +2561,18 @@ function CrossfadeTrackRow({
|
|
|
2539
2561
|
}
|
|
2540
2562
|
|
|
2541
2563
|
// src/crossfade-meta.ts
|
|
2564
|
+
function hashString(s) {
|
|
2565
|
+
let h = 5381;
|
|
2566
|
+
for (let i = 0; i < s.length; i++) h = (h << 5) + h ^ s.charCodeAt(i) | 0;
|
|
2567
|
+
return (h >>> 0).toString(36);
|
|
2568
|
+
}
|
|
2569
|
+
function soundIdentity(snap) {
|
|
2570
|
+
if (!snap) return "";
|
|
2571
|
+
if (snap.kind === "preset") return `p:${hashString(snap.state)}`;
|
|
2572
|
+
if (snap.kind === "sample") return `s:${snap.samplePath}`;
|
|
2573
|
+
if (snap.kind === "instrument") return `i:${snap.instrumentId ?? ""}:${hashString(JSON.stringify(snap.zones))}`;
|
|
2574
|
+
return "";
|
|
2575
|
+
}
|
|
2542
2576
|
var EQUAL_POWER_GAIN = 0.707;
|
|
2543
2577
|
function asCrossfadeMeta(val) {
|
|
2544
2578
|
if (!val || typeof val !== "object") return null;
|
|
@@ -2688,9 +2722,11 @@ function asFadeMeta(val) {
|
|
|
2688
2722
|
const m = val;
|
|
2689
2723
|
if (m.direction !== "in" && m.direction !== "out") return null;
|
|
2690
2724
|
if (m.gesture !== "volume" && m.gesture !== "build") return null;
|
|
2725
|
+
const effect = m.effect === "stutter" || m.effect === "chopped" || m.effect === "delay" || m.effect === "fade" ? m.effect : void 0;
|
|
2691
2726
|
return {
|
|
2692
2727
|
direction: m.direction,
|
|
2693
2728
|
gesture: m.gesture,
|
|
2729
|
+
effect,
|
|
2694
2730
|
sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
|
|
2695
2731
|
sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
|
|
2696
2732
|
sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
|
|
@@ -2777,6 +2813,7 @@ function FadeTrackRow({
|
|
|
2777
2813
|
layer,
|
|
2778
2814
|
direction,
|
|
2779
2815
|
gesture,
|
|
2816
|
+
effect,
|
|
2780
2817
|
sliderPos = 0.5,
|
|
2781
2818
|
onMuteToggle,
|
|
2782
2819
|
onSoloToggle,
|
|
@@ -2790,7 +2827,8 @@ function FadeTrackRow({
|
|
|
2790
2827
|
const [confirmDelete, setConfirmDelete] = import_react11.default.useState(false);
|
|
2791
2828
|
const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
|
|
2792
2829
|
const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
|
|
2793
|
-
const
|
|
2830
|
+
const verb = effect && effect !== "fade" ? effect.charAt(0).toUpperCase() + effect.slice(1) : "Fade";
|
|
2831
|
+
const badge = direction === "in" ? `\u2197 ${verb} in` : `\u2198 ${verb} out`;
|
|
2794
2832
|
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
2795
2833
|
"div",
|
|
2796
2834
|
{
|
|
@@ -3525,9 +3563,701 @@ function CrossfadeModal({
|
|
|
3525
3563
|
) });
|
|
3526
3564
|
}
|
|
3527
3565
|
|
|
3528
|
-
// src/components/
|
|
3566
|
+
// src/components/TransitionDesigner.tsx
|
|
3567
|
+
var import_react16 = require("react");
|
|
3568
|
+
|
|
3569
|
+
// src/hooks/useTrackReorder.ts
|
|
3529
3570
|
var import_react15 = require("react");
|
|
3571
|
+
function moveItem(arr, from, to) {
|
|
3572
|
+
const next = arr.slice();
|
|
3573
|
+
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
3574
|
+
return next;
|
|
3575
|
+
}
|
|
3576
|
+
const [moved] = next.splice(from, 1);
|
|
3577
|
+
next.splice(to, 0, moved);
|
|
3578
|
+
return next;
|
|
3579
|
+
}
|
|
3580
|
+
function useTrackReorder({
|
|
3581
|
+
host,
|
|
3582
|
+
items,
|
|
3583
|
+
setItems,
|
|
3584
|
+
getId,
|
|
3585
|
+
onError
|
|
3586
|
+
}) {
|
|
3587
|
+
const [draggingIndex, setDraggingIndex] = (0, import_react15.useState)(null);
|
|
3588
|
+
const [dragOverIndex, setDragOverIndex] = (0, import_react15.useState)(null);
|
|
3589
|
+
const fromRef = (0, import_react15.useRef)(null);
|
|
3590
|
+
const itemsRef = (0, import_react15.useRef)(items);
|
|
3591
|
+
itemsRef.current = items;
|
|
3592
|
+
const dragPropsFor = (0, import_react15.useCallback)(
|
|
3593
|
+
(index) => ({
|
|
3594
|
+
handleProps: {
|
|
3595
|
+
draggable: true,
|
|
3596
|
+
onDragStart: (e) => {
|
|
3597
|
+
fromRef.current = index;
|
|
3598
|
+
setDraggingIndex(index);
|
|
3599
|
+
if (e.dataTransfer) {
|
|
3600
|
+
e.dataTransfer.effectAllowed = "move";
|
|
3601
|
+
try {
|
|
3602
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
3603
|
+
} catch {
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
},
|
|
3607
|
+
onDragEnd: () => {
|
|
3608
|
+
fromRef.current = null;
|
|
3609
|
+
setDraggingIndex(null);
|
|
3610
|
+
setDragOverIndex(null);
|
|
3611
|
+
}
|
|
3612
|
+
},
|
|
3613
|
+
rowProps: {
|
|
3614
|
+
onDragEnter: (e) => {
|
|
3615
|
+
if (fromRef.current === null) return;
|
|
3616
|
+
e.preventDefault();
|
|
3617
|
+
setDragOverIndex(index);
|
|
3618
|
+
},
|
|
3619
|
+
onDragOver: (e) => {
|
|
3620
|
+
if (fromRef.current === null) return;
|
|
3621
|
+
e.preventDefault();
|
|
3622
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3623
|
+
setDragOverIndex((cur) => cur === index ? cur : index);
|
|
3624
|
+
},
|
|
3625
|
+
onDragLeave: () => {
|
|
3626
|
+
setDragOverIndex((cur) => cur === index ? null : cur);
|
|
3627
|
+
},
|
|
3628
|
+
onDrop: (e) => {
|
|
3629
|
+
e.preventDefault();
|
|
3630
|
+
const from = fromRef.current;
|
|
3631
|
+
fromRef.current = null;
|
|
3632
|
+
setDraggingIndex(null);
|
|
3633
|
+
setDragOverIndex(null);
|
|
3634
|
+
if (from === null || from === index) return;
|
|
3635
|
+
const prev = itemsRef.current;
|
|
3636
|
+
const next = moveItem(prev, from, index);
|
|
3637
|
+
setItems(next);
|
|
3638
|
+
const ids = next.map(getId);
|
|
3639
|
+
Promise.resolve(host.reorderTracks(ids)).catch((err) => {
|
|
3640
|
+
setItems(prev);
|
|
3641
|
+
onError?.(err);
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
},
|
|
3645
|
+
isDragging: draggingIndex === index,
|
|
3646
|
+
isDragTarget: dragOverIndex === index && draggingIndex !== index
|
|
3647
|
+
}),
|
|
3648
|
+
[host, setItems, getId, onError, draggingIndex, dragOverIndex]
|
|
3649
|
+
);
|
|
3650
|
+
return { dragPropsFor, draggingIndex, dragOverIndex };
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// src/transition-designer-meta.ts
|
|
3654
|
+
var TRANSITION_DESIGNER_DRAFT_KEY = "transitionDesigner:draft";
|
|
3655
|
+
var AUDIO_EFFECTS = ["fade", "stutter", "chopped", "delay"];
|
|
3656
|
+
var AUDIO_EFFECT_LABEL = {
|
|
3657
|
+
fade: "Fade",
|
|
3658
|
+
stutter: "Stutter",
|
|
3659
|
+
chopped: "Chopped",
|
|
3660
|
+
delay: "Delay"
|
|
3661
|
+
};
|
|
3662
|
+
function asAudioEffect(v) {
|
|
3663
|
+
return v === "fade" || v === "stutter" || v === "chopped" || v === "delay" ? v : null;
|
|
3664
|
+
}
|
|
3665
|
+
function rowType(hasOrigin, hasTarget) {
|
|
3666
|
+
if (hasOrigin && hasTarget) return "crossfade";
|
|
3667
|
+
if (hasOrigin) return "fade-out";
|
|
3668
|
+
if (hasTarget) return "fade-in";
|
|
3669
|
+
return null;
|
|
3670
|
+
}
|
|
3671
|
+
function asTransitionDesignerDraft(val) {
|
|
3672
|
+
if (!val || typeof val !== "object") return null;
|
|
3673
|
+
const d = val;
|
|
3674
|
+
const clean = (a) => Array.isArray(a) ? a.filter((x) => x === null || typeof x === "string") : [];
|
|
3675
|
+
const cleanEffects = (e) => {
|
|
3676
|
+
const out = {};
|
|
3677
|
+
if (e && typeof e === "object") {
|
|
3678
|
+
for (const [k, v] of Object.entries(e)) {
|
|
3679
|
+
const eff = asAudioEffect(v);
|
|
3680
|
+
if (eff) out[k] = eff;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
return out;
|
|
3684
|
+
};
|
|
3685
|
+
return {
|
|
3686
|
+
originOrder: clean(d.originOrder),
|
|
3687
|
+
targetOrder: clean(d.targetOrder),
|
|
3688
|
+
rowEffects: cleanEffects(d.rowEffects)
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
function reconcileSlots(saved, poolIds) {
|
|
3692
|
+
const pool = new Set(poolIds);
|
|
3693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3694
|
+
const out = [];
|
|
3695
|
+
for (const slot of saved ?? []) {
|
|
3696
|
+
if (slot === null) {
|
|
3697
|
+
out.push(null);
|
|
3698
|
+
continue;
|
|
3699
|
+
}
|
|
3700
|
+
if (pool.has(slot) && !seen.has(slot)) {
|
|
3701
|
+
out.push(slot);
|
|
3702
|
+
seen.add(slot);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
for (const id of poolIds) {
|
|
3706
|
+
if (!seen.has(id)) {
|
|
3707
|
+
out.push(id);
|
|
3708
|
+
seen.add(id);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
return out;
|
|
3712
|
+
}
|
|
3713
|
+
function buildRowSlots(originSlots, targetSlots) {
|
|
3714
|
+
const n = Math.max(originSlots.length, targetSlots.length);
|
|
3715
|
+
const rows = [];
|
|
3716
|
+
for (let i = 0; i < n; i++) {
|
|
3717
|
+
const originId = originSlots[i] ?? null;
|
|
3718
|
+
const targetId = targetSlots[i] ?? null;
|
|
3719
|
+
rows.push({ originId, targetId, type: rowType(originId !== null, targetId !== null) });
|
|
3720
|
+
}
|
|
3721
|
+
return rows;
|
|
3722
|
+
}
|
|
3723
|
+
function normalizeSlots(originSlots, targetSlots) {
|
|
3724
|
+
const rows = buildRowSlots(originSlots, targetSlots).filter(
|
|
3725
|
+
(r) => r.originId !== null || r.targetId !== null
|
|
3726
|
+
);
|
|
3727
|
+
const trimTrailing = (a) => {
|
|
3728
|
+
let end = a.length;
|
|
3729
|
+
while (end > 0 && a[end - 1] === null) end--;
|
|
3730
|
+
return a.slice(0, end);
|
|
3731
|
+
};
|
|
3732
|
+
return {
|
|
3733
|
+
originOrder: trimTrailing(rows.map((r) => r.originId)),
|
|
3734
|
+
targetOrder: trimTrailing(rows.map((r) => r.targetId))
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
function padSlots(slots, n) {
|
|
3738
|
+
if (slots.length >= n) return slots.slice();
|
|
3739
|
+
return [...slots, ...new Array(n - slots.length).fill(null)];
|
|
3740
|
+
}
|
|
3741
|
+
function padPair(originSlots, targetSlots) {
|
|
3742
|
+
const n = Math.max(originSlots.length, targetSlots.length);
|
|
3743
|
+
return [padSlots(originSlots, n), padSlots(targetSlots, n)];
|
|
3744
|
+
}
|
|
3745
|
+
function slotsEqual(a, b) {
|
|
3746
|
+
if (a.length !== b.length) return false;
|
|
3747
|
+
for (let i = 0; i < a.length; i++) {
|
|
3748
|
+
if (a[i] !== b[i]) return false;
|
|
3749
|
+
}
|
|
3750
|
+
return true;
|
|
3751
|
+
}
|
|
3752
|
+
function rowKey(row) {
|
|
3753
|
+
if (row.type === "crossfade") return `xf:${row.originId}|${row.targetId}`;
|
|
3754
|
+
if (row.type === "fade-out") return `fo:${row.originId}`;
|
|
3755
|
+
if (row.type === "fade-in") return `fi:${row.targetId}`;
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
function dbIdsFromKeys(keys) {
|
|
3759
|
+
const out = /* @__PURE__ */ new Set();
|
|
3760
|
+
for (const k of keys) {
|
|
3761
|
+
const body = k.slice(3);
|
|
3762
|
+
if (k.startsWith("xf:")) {
|
|
3763
|
+
const sep = body.indexOf("|");
|
|
3764
|
+
out.add(body.slice(0, sep));
|
|
3765
|
+
out.add(body.slice(sep + 1));
|
|
3766
|
+
} else {
|
|
3767
|
+
out.add(body);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
return out;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// src/components/TransitionDesigner.tsx
|
|
3530
3774
|
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3775
|
+
var CROSSFADE_ESTIMATE_MS = 15e3;
|
|
3776
|
+
var FADE_ESTIMATE_MS = 11e3;
|
|
3777
|
+
var CREATE_ALL_CONCURRENCY = 5;
|
|
3778
|
+
var TYPE_LABEL = {
|
|
3779
|
+
crossfade: "Crossfade",
|
|
3780
|
+
"fade-out": "Fade out",
|
|
3781
|
+
"fade-in": "Fade in"
|
|
3782
|
+
};
|
|
3783
|
+
function shortId3(dbId) {
|
|
3784
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3785
|
+
}
|
|
3786
|
+
function TransitionDesigner({
|
|
3787
|
+
host,
|
|
3788
|
+
fromSceneId,
|
|
3789
|
+
toSceneId,
|
|
3790
|
+
transitionSceneId,
|
|
3791
|
+
excludeSourceDbIds,
|
|
3792
|
+
onCreateCrossfade,
|
|
3793
|
+
onCreateFade,
|
|
3794
|
+
onCreateAudioTransition,
|
|
3795
|
+
familyLabel,
|
|
3796
|
+
testIdPrefix = "transition-designer"
|
|
3797
|
+
}) {
|
|
3798
|
+
const [load, setLoad] = (0, import_react16.useState)({ status: "loading" });
|
|
3799
|
+
const [fromName, setFromName] = (0, import_react16.useState)(null);
|
|
3800
|
+
const [toName, setToName] = (0, import_react16.useState)(null);
|
|
3801
|
+
const [originSlots, setOriginSlots] = (0, import_react16.useState)([]);
|
|
3802
|
+
const [targetSlots, setTargetSlots] = (0, import_react16.useState)([]);
|
|
3803
|
+
const [creatingKeys, setCreatingKeys] = (0, import_react16.useState)(() => /* @__PURE__ */ new Set());
|
|
3804
|
+
const [rowErrors, setRowErrors] = (0, import_react16.useState)({});
|
|
3805
|
+
const [rowEffects, setRowEffects] = (0, import_react16.useState)({});
|
|
3806
|
+
const rowEffectsRef = (0, import_react16.useRef)(rowEffects);
|
|
3807
|
+
rowEffectsRef.current = rowEffects;
|
|
3808
|
+
const audioEffectsEnabled = !!onCreateAudioTransition;
|
|
3809
|
+
const excludeRef = (0, import_react16.useRef)(excludeSourceDbIds);
|
|
3810
|
+
excludeRef.current = excludeSourceDbIds;
|
|
3811
|
+
const originSlotsRef = (0, import_react16.useRef)(originSlots);
|
|
3812
|
+
originSlotsRef.current = originSlots;
|
|
3813
|
+
const targetSlotsRef = (0, import_react16.useRef)(targetSlots);
|
|
3814
|
+
targetSlotsRef.current = targetSlots;
|
|
3815
|
+
const creatingKeysRef = (0, import_react16.useRef)(creatingKeys);
|
|
3816
|
+
creatingKeysRef.current = creatingKeys;
|
|
3817
|
+
const dragRef = (0, import_react16.useRef)(null);
|
|
3818
|
+
const [dragging, setDragging] = (0, import_react16.useState)(null);
|
|
3819
|
+
const [dragOver, setDragOver] = (0, import_react16.useState)(null);
|
|
3820
|
+
const excludeSet = (0, import_react16.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3821
|
+
const originPool = (0, import_react16.useMemo)(
|
|
3822
|
+
() => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3823
|
+
[load, excludeSet]
|
|
3824
|
+
);
|
|
3825
|
+
const targetPool = (0, import_react16.useMemo)(
|
|
3826
|
+
() => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3827
|
+
[load, excludeSet]
|
|
3828
|
+
);
|
|
3829
|
+
const originById = (0, import_react16.useMemo)(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
|
|
3830
|
+
const targetById = (0, import_react16.useMemo)(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
|
|
3831
|
+
const originByIdRef = (0, import_react16.useRef)(originById);
|
|
3832
|
+
originByIdRef.current = originById;
|
|
3833
|
+
const targetByIdRef = (0, import_react16.useRef)(targetById);
|
|
3834
|
+
targetByIdRef.current = targetById;
|
|
3835
|
+
const refresh = (0, import_react16.useCallback)(async () => {
|
|
3836
|
+
if (!host.listSceneFamilyTracks) {
|
|
3837
|
+
setLoad({ status: "error", message: "This host does not support transition tracks." });
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
setLoad({ status: "loading" });
|
|
3841
|
+
try {
|
|
3842
|
+
const [origin, target, fName, tName, draftRaw] = await Promise.all([
|
|
3843
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
3844
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
3845
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
3846
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
|
|
3847
|
+
host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
|
|
3848
|
+
]);
|
|
3849
|
+
const draft = asTransitionDesignerDraft(draftRaw);
|
|
3850
|
+
const exSet = new Set(excludeRef.current ?? []);
|
|
3851
|
+
const originIds = origin.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
|
|
3852
|
+
const targetIds = target.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
|
|
3853
|
+
const [po, pt] = padPair(
|
|
3854
|
+
reconcileSlots(draft?.originOrder, originIds),
|
|
3855
|
+
reconcileSlots(draft?.targetOrder, targetIds)
|
|
3856
|
+
);
|
|
3857
|
+
setOriginSlots(po);
|
|
3858
|
+
setTargetSlots(pt);
|
|
3859
|
+
setRowEffects(draft?.rowEffects ?? {});
|
|
3860
|
+
setFromName(fName);
|
|
3861
|
+
setToName(tName);
|
|
3862
|
+
setLoad({ status: "ready", origin, target });
|
|
3863
|
+
} catch (err) {
|
|
3864
|
+
setLoad({
|
|
3865
|
+
status: "error",
|
|
3866
|
+
message: err instanceof Error ? err.message : "Failed to load tracks."
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
}, [host, fromSceneId, toSceneId, transitionSceneId]);
|
|
3870
|
+
(0, import_react16.useEffect)(() => {
|
|
3871
|
+
void refresh();
|
|
3872
|
+
}, [refresh]);
|
|
3873
|
+
(0, import_react16.useEffect)(() => {
|
|
3874
|
+
if (load.status !== "ready") return;
|
|
3875
|
+
const [po, pt] = padPair(
|
|
3876
|
+
reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
|
|
3877
|
+
reconcileSlots(targetSlotsRef.current, targetPool.map((t) => t.dbId))
|
|
3878
|
+
);
|
|
3879
|
+
if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
|
|
3880
|
+
if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
|
|
3881
|
+
}, [originPool, targetPool, load.status]);
|
|
3882
|
+
const mutate = (0, import_react16.useCallback)(
|
|
3883
|
+
(nextOrigin, nextTarget) => {
|
|
3884
|
+
const norm = normalizeSlots(nextOrigin, nextTarget);
|
|
3885
|
+
const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
|
|
3886
|
+
setOriginSlots(po);
|
|
3887
|
+
setTargetSlots(pt);
|
|
3888
|
+
if (host.setSceneData) {
|
|
3889
|
+
host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: rowEffectsRef.current }).catch(() => {
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
},
|
|
3893
|
+
[host, transitionSceneId]
|
|
3894
|
+
);
|
|
3895
|
+
const setRowEffect = (0, import_react16.useCallback)(
|
|
3896
|
+
(sourceDbId, effect) => {
|
|
3897
|
+
setRowEffects((prev) => {
|
|
3898
|
+
const next = { ...prev, [sourceDbId]: effect };
|
|
3899
|
+
if (host.setSceneData) {
|
|
3900
|
+
const norm = normalizeSlots(originSlotsRef.current, targetSlotsRef.current);
|
|
3901
|
+
host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: next }).catch(() => {
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
return next;
|
|
3905
|
+
});
|
|
3906
|
+
},
|
|
3907
|
+
[host, transitionSceneId]
|
|
3908
|
+
);
|
|
3909
|
+
const insertGapAbove = (0, import_react16.useCallback)(
|
|
3910
|
+
(col, index) => {
|
|
3911
|
+
const slots = col === "origin" ? originSlots : targetSlots;
|
|
3912
|
+
const next = [...slots.slice(0, index), null, ...slots.slice(index)];
|
|
3913
|
+
if (col === "origin") mutate(next, targetSlots);
|
|
3914
|
+
else mutate(originSlots, next);
|
|
3915
|
+
},
|
|
3916
|
+
[originSlots, targetSlots, mutate]
|
|
3917
|
+
);
|
|
3918
|
+
const removeGap = (0, import_react16.useCallback)(
|
|
3919
|
+
(col, index) => {
|
|
3920
|
+
const slots = col === "origin" ? originSlots : targetSlots;
|
|
3921
|
+
const next = slots.filter((_, i) => i !== index);
|
|
3922
|
+
if (col === "origin") mutate(next, targetSlots);
|
|
3923
|
+
else mutate(originSlots, next);
|
|
3924
|
+
},
|
|
3925
|
+
[originSlots, targetSlots, mutate]
|
|
3926
|
+
);
|
|
3927
|
+
const handleDrop = (0, import_react16.useCallback)(
|
|
3928
|
+
(col, to) => {
|
|
3929
|
+
const from = dragRef.current;
|
|
3930
|
+
dragRef.current = null;
|
|
3931
|
+
setDragging(null);
|
|
3932
|
+
setDragOver(null);
|
|
3933
|
+
if (!from || from.col !== col || from.index === to) return;
|
|
3934
|
+
if (col === "origin") mutate(moveItem(originSlots, from.index, to), targetSlots);
|
|
3935
|
+
else mutate(originSlots, moveItem(targetSlots, from.index, to));
|
|
3936
|
+
},
|
|
3937
|
+
[originSlots, targetSlots, mutate]
|
|
3938
|
+
);
|
|
3939
|
+
const rows = (0, import_react16.useMemo)(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
|
|
3940
|
+
const creatingDbIds = (0, import_react16.useMemo)(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
|
|
3941
|
+
const eligibleCount = (0, import_react16.useMemo)(
|
|
3942
|
+
() => rows.filter((r) => {
|
|
3943
|
+
const k = rowKey(r);
|
|
3944
|
+
return k !== null && !creatingKeys.has(k);
|
|
3945
|
+
}).length,
|
|
3946
|
+
[rows, creatingKeys]
|
|
3947
|
+
);
|
|
3948
|
+
const createRow = (0, import_react16.useCallback)(
|
|
3949
|
+
async (row) => {
|
|
3950
|
+
const key = rowKey(row);
|
|
3951
|
+
if (!key || !row.type || creatingKeysRef.current.has(key)) return;
|
|
3952
|
+
setCreatingKeys((prev) => new Set(prev).add(key));
|
|
3953
|
+
setRowErrors((prev) => {
|
|
3954
|
+
if (!(key in prev)) return prev;
|
|
3955
|
+
const next = { ...prev };
|
|
3956
|
+
delete next[key];
|
|
3957
|
+
return next;
|
|
3958
|
+
});
|
|
3959
|
+
try {
|
|
3960
|
+
if (row.type === "crossfade") {
|
|
3961
|
+
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
3962
|
+
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
3963
|
+
if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3964
|
+
await onCreateCrossfade(
|
|
3965
|
+
{ dbId: o.dbId, name: o.name, role: o.role },
|
|
3966
|
+
{ dbId: t.dbId, name: t.name, role: t.role }
|
|
3967
|
+
);
|
|
3968
|
+
} else if (row.type === "fade-out") {
|
|
3969
|
+
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
3970
|
+
if (!o) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3971
|
+
const eff = rowEffectsRef.current[o.dbId] ?? "fade";
|
|
3972
|
+
if (eff !== "fade" && onCreateAudioTransition) {
|
|
3973
|
+
await onCreateAudioTransition({ dbId: o.dbId, name: o.name, role: o.role }, "out", eff);
|
|
3974
|
+
} else {
|
|
3975
|
+
await onCreateFade({ dbId: o.dbId, name: o.name, role: o.role }, "out", defaultFadeGesture(o.role));
|
|
3976
|
+
}
|
|
3977
|
+
} else {
|
|
3978
|
+
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
3979
|
+
if (!t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3980
|
+
const eff = rowEffectsRef.current[t.dbId] ?? "fade";
|
|
3981
|
+
if (eff !== "fade" && onCreateAudioTransition) {
|
|
3982
|
+
await onCreateAudioTransition({ dbId: t.dbId, name: t.name, role: t.role }, "in", eff);
|
|
3983
|
+
} else {
|
|
3984
|
+
await onCreateFade({ dbId: t.dbId, name: t.name, role: t.role }, "in", defaultFadeGesture(t.role));
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
} catch (err) {
|
|
3988
|
+
setRowErrors((prev) => ({
|
|
3989
|
+
...prev,
|
|
3990
|
+
[key]: err instanceof Error ? err.message : "Failed to create transition."
|
|
3991
|
+
}));
|
|
3992
|
+
} finally {
|
|
3993
|
+
setCreatingKeys((prev) => {
|
|
3994
|
+
const next = new Set(prev);
|
|
3995
|
+
next.delete(key);
|
|
3996
|
+
return next;
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
},
|
|
4000
|
+
[onCreateCrossfade, onCreateFade, onCreateAudioTransition]
|
|
4001
|
+
);
|
|
4002
|
+
const createAll = (0, import_react16.useCallback)(async () => {
|
|
4003
|
+
const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
|
|
4004
|
+
const k = rowKey(r);
|
|
4005
|
+
return k !== null && !creatingKeysRef.current.has(k);
|
|
4006
|
+
});
|
|
4007
|
+
if (eligible.length === 0) return;
|
|
4008
|
+
let cursor = 0;
|
|
4009
|
+
const worker = async () => {
|
|
4010
|
+
while (cursor < eligible.length) {
|
|
4011
|
+
const row = eligible[cursor];
|
|
4012
|
+
cursor += 1;
|
|
4013
|
+
await createRow(row);
|
|
4014
|
+
}
|
|
4015
|
+
};
|
|
4016
|
+
await Promise.all(
|
|
4017
|
+
Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
|
|
4018
|
+
);
|
|
4019
|
+
}, [createRow]);
|
|
4020
|
+
const fromLabel = fromName ?? "origin";
|
|
4021
|
+
const toLabel = toName ?? "target";
|
|
4022
|
+
const cellDragProps = (col, index, locked) => ({
|
|
4023
|
+
draggable: !locked,
|
|
4024
|
+
onDragStart: (e) => {
|
|
4025
|
+
if (locked) return;
|
|
4026
|
+
dragRef.current = { col, index };
|
|
4027
|
+
setDragging({ col, index });
|
|
4028
|
+
if (e.dataTransfer) {
|
|
4029
|
+
e.dataTransfer.effectAllowed = "move";
|
|
4030
|
+
try {
|
|
4031
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
4032
|
+
} catch {
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
},
|
|
4036
|
+
onDragEnd: () => {
|
|
4037
|
+
dragRef.current = null;
|
|
4038
|
+
setDragging(null);
|
|
4039
|
+
setDragOver(null);
|
|
4040
|
+
},
|
|
4041
|
+
onDragEnter: (e) => {
|
|
4042
|
+
const d = dragRef.current;
|
|
4043
|
+
if (!d || d.col !== col) return;
|
|
4044
|
+
e.preventDefault();
|
|
4045
|
+
setDragOver({ col, index });
|
|
4046
|
+
},
|
|
4047
|
+
onDragOver: (e) => {
|
|
4048
|
+
const d = dragRef.current;
|
|
4049
|
+
if (!d || d.col !== col) return;
|
|
4050
|
+
e.preventDefault();
|
|
4051
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
4052
|
+
},
|
|
4053
|
+
onDragLeave: () => {
|
|
4054
|
+
setDragOver((cur) => cur && cur.col === col && cur.index === index ? null : cur);
|
|
4055
|
+
},
|
|
4056
|
+
onDrop: (e) => {
|
|
4057
|
+
e.preventDefault();
|
|
4058
|
+
handleDrop(col, index);
|
|
4059
|
+
}
|
|
4060
|
+
});
|
|
4061
|
+
const renderCell = (col, index, slotId) => {
|
|
4062
|
+
const byId = col === "origin" ? originById : targetById;
|
|
4063
|
+
const track = slotId ? byId.get(slotId) : void 0;
|
|
4064
|
+
const locked = slotId !== null && creatingDbIds.has(slotId);
|
|
4065
|
+
const isDragging = dragging?.col === col && dragging.index === index;
|
|
4066
|
+
const isDragTarget = dragOver?.col === col && dragOver.index === index && !isDragging;
|
|
4067
|
+
const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
|
|
4068
|
+
const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
|
|
4069
|
+
if (slotId === null) {
|
|
4070
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4071
|
+
"div",
|
|
4072
|
+
{
|
|
4073
|
+
...cellDragProps(col, index, false),
|
|
4074
|
+
"data-testid": `${testIdPrefix}-${col}-gap-${index}`,
|
|
4075
|
+
className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
|
|
4076
|
+
children: [
|
|
4077
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
|
|
4078
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4079
|
+
"button",
|
|
4080
|
+
{
|
|
4081
|
+
type: "button",
|
|
4082
|
+
"data-testid": `${testIdPrefix}-${col}-remove-gap-${index}`,
|
|
4083
|
+
onClick: () => removeGap(col, index),
|
|
4084
|
+
title: "Remove gap",
|
|
4085
|
+
className: "text-[10px] text-sas-muted hover:text-sas-danger",
|
|
4086
|
+
children: "\u2715"
|
|
4087
|
+
}
|
|
4088
|
+
)
|
|
4089
|
+
]
|
|
4090
|
+
}
|
|
4091
|
+
);
|
|
4092
|
+
}
|
|
4093
|
+
const primary = track ? track.prompt?.trim() || track.name : slotId;
|
|
4094
|
+
const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
|
|
4095
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4096
|
+
"div",
|
|
4097
|
+
{
|
|
4098
|
+
...cellDragProps(col, index, locked),
|
|
4099
|
+
"data-testid": `${testIdPrefix}-${col}-cell-${slotId}`,
|
|
4100
|
+
"data-value": slotId,
|
|
4101
|
+
className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
|
|
4102
|
+
title: track ? track.dbId : "Track no longer available",
|
|
4103
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-start gap-1", children: [
|
|
4104
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
|
|
4105
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
4106
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-text truncate", children: primary }),
|
|
4107
|
+
meta && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
|
|
4108
|
+
] }),
|
|
4109
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4110
|
+
"button",
|
|
4111
|
+
{
|
|
4112
|
+
type: "button",
|
|
4113
|
+
"data-testid": `${testIdPrefix}-${col}-insert-gap-${index}`,
|
|
4114
|
+
onClick: () => insertGapAbove(col, index),
|
|
4115
|
+
disabled: locked,
|
|
4116
|
+
title: "Insert a gap above (make this a fade)",
|
|
4117
|
+
className: "text-[10px] text-sas-muted opacity-0 group-hover:opacity-100 hover:text-sas-accent disabled:opacity-30",
|
|
4118
|
+
children: "+gap"
|
|
4119
|
+
}
|
|
4120
|
+
)
|
|
4121
|
+
] })
|
|
4122
|
+
}
|
|
4123
|
+
);
|
|
4124
|
+
};
|
|
4125
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
|
|
4126
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
|
|
4127
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
|
|
4128
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: fromLabel }),
|
|
4129
|
+
" \u2192",
|
|
4130
|
+
" ",
|
|
4131
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: toLabel }),
|
|
4132
|
+
familyLabel ? ` \xB7 ${familyLabel}` : "",
|
|
4133
|
+
" \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
|
|
4134
|
+
] }),
|
|
4135
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
4136
|
+
creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
|
|
4137
|
+
creatingKeys.size,
|
|
4138
|
+
" creating\u2026"
|
|
4139
|
+
] }),
|
|
4140
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4141
|
+
"button",
|
|
4142
|
+
{
|
|
4143
|
+
type: "button",
|
|
4144
|
+
"data-testid": `${testIdPrefix}-create-all`,
|
|
4145
|
+
onClick: createAll,
|
|
4146
|
+
disabled: eligibleCount === 0,
|
|
4147
|
+
title: "Create every staged transition at once (runs several concurrently)",
|
|
4148
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors whitespace-nowrap ${eligibleCount > 0 ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
4149
|
+
children: [
|
|
4150
|
+
"Create all",
|
|
4151
|
+
eligibleCount > 0 ? ` (${eligibleCount})` : ""
|
|
4152
|
+
]
|
|
4153
|
+
}
|
|
4154
|
+
)
|
|
4155
|
+
] })
|
|
4156
|
+
] }),
|
|
4157
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
|
|
4158
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
|
|
4159
|
+
"Origin (",
|
|
4160
|
+
fromLabel,
|
|
4161
|
+
")"
|
|
4162
|
+
] }),
|
|
4163
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
|
|
4164
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
|
|
4165
|
+
"Target (",
|
|
4166
|
+
toLabel,
|
|
4167
|
+
")"
|
|
4168
|
+
] })
|
|
4169
|
+
] }),
|
|
4170
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
|
|
4171
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
4172
|
+
load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
|
|
4173
|
+
"No tracks to arrange in this panel for either scene. Add tracks to ",
|
|
4174
|
+
fromLabel,
|
|
4175
|
+
" or ",
|
|
4176
|
+
toLabel,
|
|
4177
|
+
" ",
|
|
4178
|
+
"first (or free one by deleting an existing crossfade/fade)."
|
|
4179
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "space-y-2", children: rows.map((row, i) => {
|
|
4180
|
+
const key = rowKey(row);
|
|
4181
|
+
const isCreatingThis = key !== null && creatingKeys.has(key);
|
|
4182
|
+
const errMsg = key !== null ? rowErrors[key] : void 0;
|
|
4183
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4184
|
+
"div",
|
|
4185
|
+
{
|
|
4186
|
+
"data-testid": `${testIdPrefix}-row-${i}`,
|
|
4187
|
+
className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
|
|
4188
|
+
children: [
|
|
4189
|
+
renderCell("origin", i, row.originId),
|
|
4190
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
|
|
4191
|
+
!row.type ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4192
|
+
"span",
|
|
4193
|
+
{
|
|
4194
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4195
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
|
|
4196
|
+
children: TYPE_LABEL[row.type]
|
|
4197
|
+
}
|
|
4198
|
+
) : audioEffectsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
|
|
4199
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4200
|
+
"select",
|
|
4201
|
+
{
|
|
4202
|
+
"data-testid": `${testIdPrefix}-effect-${i}`,
|
|
4203
|
+
value: rowEffects[row.originId ?? row.targetId] ?? "fade",
|
|
4204
|
+
onChange: (e) => {
|
|
4205
|
+
const id = row.originId ?? row.targetId;
|
|
4206
|
+
if (id) setRowEffect(id, e.target.value);
|
|
4207
|
+
},
|
|
4208
|
+
className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
|
|
4209
|
+
children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
|
|
4210
|
+
}
|
|
4211
|
+
),
|
|
4212
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
|
|
4213
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4214
|
+
"span",
|
|
4215
|
+
{
|
|
4216
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4217
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
|
|
4218
|
+
children: TYPE_LABEL[row.type]
|
|
4219
|
+
}
|
|
4220
|
+
),
|
|
4221
|
+
isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4222
|
+
SorceryProgressBar,
|
|
4223
|
+
{
|
|
4224
|
+
isLoading: true,
|
|
4225
|
+
heightClass: "h-5",
|
|
4226
|
+
statusText: "CREATING",
|
|
4227
|
+
estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
|
|
4228
|
+
}
|
|
4229
|
+
) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4230
|
+
"button",
|
|
4231
|
+
{
|
|
4232
|
+
type: "button",
|
|
4233
|
+
"data-testid": `${testIdPrefix}-create-${i}`,
|
|
4234
|
+
onClick: () => createRow(row),
|
|
4235
|
+
disabled: !row.type,
|
|
4236
|
+
className: `w-full px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${row.type ? "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"}`,
|
|
4237
|
+
children: "Create"
|
|
4238
|
+
}
|
|
4239
|
+
),
|
|
4240
|
+
errMsg && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4241
|
+
"span",
|
|
4242
|
+
{
|
|
4243
|
+
"data-testid": `${testIdPrefix}-row-error-${i}`,
|
|
4244
|
+
className: "text-[10px] text-sas-danger text-center leading-tight",
|
|
4245
|
+
children: errMsg
|
|
4246
|
+
}
|
|
4247
|
+
)
|
|
4248
|
+
] }),
|
|
4249
|
+
renderCell("target", i, row.targetId)
|
|
4250
|
+
]
|
|
4251
|
+
},
|
|
4252
|
+
i
|
|
4253
|
+
);
|
|
4254
|
+
}) }))
|
|
4255
|
+
] });
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
// src/components/DownloadPackButton.tsx
|
|
4259
|
+
var import_react17 = require("react");
|
|
4260
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3531
4261
|
function formatSize(bytes) {
|
|
3532
4262
|
if (!bytes || bytes <= 0) return "";
|
|
3533
4263
|
const gb = bytes / 1024 ** 3;
|
|
@@ -3543,10 +4273,10 @@ var DownloadPackButton = ({
|
|
|
3543
4273
|
variant = "compact",
|
|
3544
4274
|
onDownloadComplete
|
|
3545
4275
|
}) => {
|
|
3546
|
-
const [status, setStatus] = (0,
|
|
3547
|
-
const [progress, setProgress] = (0,
|
|
3548
|
-
const [errorMessage, setErrorMessage] = (0,
|
|
3549
|
-
(0,
|
|
4276
|
+
const [status, setStatus] = (0, import_react17.useState)("idle");
|
|
4277
|
+
const [progress, setProgress] = (0, import_react17.useState)(0);
|
|
4278
|
+
const [errorMessage, setErrorMessage] = (0, import_react17.useState)(null);
|
|
4279
|
+
(0, import_react17.useEffect)(() => {
|
|
3550
4280
|
const unsub = host.onSamplePackProgress(packId, (p) => {
|
|
3551
4281
|
setStatus(p.status);
|
|
3552
4282
|
setProgress(p.progress);
|
|
@@ -3561,7 +4291,7 @@ var DownloadPackButton = ({
|
|
|
3561
4291
|
});
|
|
3562
4292
|
return unsub;
|
|
3563
4293
|
}, [host, packId, onDownloadComplete]);
|
|
3564
|
-
const handleClick = (0,
|
|
4294
|
+
const handleClick = (0, import_react17.useCallback)(async () => {
|
|
3565
4295
|
if (status !== "idle" && status !== "error") return;
|
|
3566
4296
|
try {
|
|
3567
4297
|
setStatus("downloading");
|
|
@@ -3615,8 +4345,8 @@ var DownloadPackButton = ({
|
|
|
3615
4345
|
} else {
|
|
3616
4346
|
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
3617
4347
|
}
|
|
3618
|
-
return /* @__PURE__ */ (0,
|
|
3619
|
-
/* @__PURE__ */ (0,
|
|
4348
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
|
|
4349
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3620
4350
|
"button",
|
|
3621
4351
|
{
|
|
3622
4352
|
"data-testid": `download-pack-button-${packId}`,
|
|
@@ -3627,12 +4357,12 @@ var DownloadPackButton = ({
|
|
|
3627
4357
|
children: buttonLabel
|
|
3628
4358
|
}
|
|
3629
4359
|
),
|
|
3630
|
-
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0,
|
|
4360
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
3631
4361
|
] });
|
|
3632
4362
|
};
|
|
3633
4363
|
|
|
3634
4364
|
// src/components/SamplePackCTACard.tsx
|
|
3635
|
-
var
|
|
4365
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3636
4366
|
var SamplePackCTACard = ({
|
|
3637
4367
|
host,
|
|
3638
4368
|
pack,
|
|
@@ -3640,7 +4370,7 @@ var SamplePackCTACard = ({
|
|
|
3640
4370
|
onDownloadComplete
|
|
3641
4371
|
}) => {
|
|
3642
4372
|
if (status === "checking") {
|
|
3643
|
-
return /* @__PURE__ */ (0,
|
|
4373
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3644
4374
|
"div",
|
|
3645
4375
|
{
|
|
3646
4376
|
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
@@ -3651,16 +4381,16 @@ var SamplePackCTACard = ({
|
|
|
3651
4381
|
}
|
|
3652
4382
|
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
3653
4383
|
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
3654
|
-
return /* @__PURE__ */ (0,
|
|
4384
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
3655
4385
|
"div",
|
|
3656
4386
|
{
|
|
3657
4387
|
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
3658
4388
|
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
3659
4389
|
children: [
|
|
3660
|
-
/* @__PURE__ */ (0,
|
|
3661
|
-
/* @__PURE__ */ (0,
|
|
3662
|
-
/* @__PURE__ */ (0,
|
|
3663
|
-
/* @__PURE__ */ (0,
|
|
4390
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
4391
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
4392
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
4393
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3664
4394
|
DownloadPackButton,
|
|
3665
4395
|
{
|
|
3666
4396
|
host,
|
|
@@ -3677,7 +4407,7 @@ var SamplePackCTACard = ({
|
|
|
3677
4407
|
};
|
|
3678
4408
|
|
|
3679
4409
|
// src/components/WaveformView.tsx
|
|
3680
|
-
var
|
|
4410
|
+
var import_react18 = require("react");
|
|
3681
4411
|
|
|
3682
4412
|
// src/components/waveform.ts
|
|
3683
4413
|
function computePeaks(audioBuffer, bins, targetSamples) {
|
|
@@ -3740,7 +4470,7 @@ function drawWaveform(canvas, peaks, options = {}) {
|
|
|
3740
4470
|
}
|
|
3741
4471
|
|
|
3742
4472
|
// src/components/WaveformView.tsx
|
|
3743
|
-
var
|
|
4473
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
3744
4474
|
var WaveformView = ({
|
|
3745
4475
|
host,
|
|
3746
4476
|
filePath,
|
|
@@ -3749,9 +4479,9 @@ var WaveformView = ({
|
|
|
3749
4479
|
fillStyle,
|
|
3750
4480
|
targetSamples
|
|
3751
4481
|
}) => {
|
|
3752
|
-
const canvasRef = (0,
|
|
3753
|
-
const [peaks, setPeaks] = (0,
|
|
3754
|
-
(0,
|
|
4482
|
+
const canvasRef = (0, import_react18.useRef)(null);
|
|
4483
|
+
const [peaks, setPeaks] = (0, import_react18.useState)(null);
|
|
4484
|
+
(0, import_react18.useEffect)(() => {
|
|
3755
4485
|
let cancelled = false;
|
|
3756
4486
|
let audioContext = null;
|
|
3757
4487
|
(async () => {
|
|
@@ -3777,7 +4507,7 @@ var WaveformView = ({
|
|
|
3777
4507
|
cancelled = true;
|
|
3778
4508
|
};
|
|
3779
4509
|
}, [host, filePath, bins, targetSamples]);
|
|
3780
|
-
(0,
|
|
4510
|
+
(0, import_react18.useEffect)(() => {
|
|
3781
4511
|
if (!peaks) return;
|
|
3782
4512
|
const canvas = canvasRef.current;
|
|
3783
4513
|
if (!canvas) return;
|
|
@@ -3788,7 +4518,7 @@ var WaveformView = ({
|
|
|
3788
4518
|
observer.observe(canvas);
|
|
3789
4519
|
return () => observer.disconnect();
|
|
3790
4520
|
}, [peaks, fillStyle]);
|
|
3791
|
-
return /* @__PURE__ */ (0,
|
|
4521
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
3792
4522
|
"canvas",
|
|
3793
4523
|
{
|
|
3794
4524
|
ref: canvasRef,
|
|
@@ -3799,8 +4529,8 @@ var WaveformView = ({
|
|
|
3799
4529
|
};
|
|
3800
4530
|
|
|
3801
4531
|
// src/components/ScrollingWaveform.tsx
|
|
3802
|
-
var
|
|
3803
|
-
var
|
|
4532
|
+
var import_react19 = require("react");
|
|
4533
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
3804
4534
|
var ScrollingWaveform = ({
|
|
3805
4535
|
getPeakDb,
|
|
3806
4536
|
active,
|
|
@@ -3808,11 +4538,11 @@ var ScrollingWaveform = ({
|
|
|
3808
4538
|
className,
|
|
3809
4539
|
fillStyle
|
|
3810
4540
|
}) => {
|
|
3811
|
-
const canvasRef = (0,
|
|
3812
|
-
const ringRef = (0,
|
|
3813
|
-
const writeIdxRef = (0,
|
|
3814
|
-
const rafRef = (0,
|
|
3815
|
-
(0,
|
|
4541
|
+
const canvasRef = (0, import_react19.useRef)(null);
|
|
4542
|
+
const ringRef = (0, import_react19.useRef)(new Float32Array(columns));
|
|
4543
|
+
const writeIdxRef = (0, import_react19.useRef)(0);
|
|
4544
|
+
const rafRef = (0, import_react19.useRef)(null);
|
|
4545
|
+
(0, import_react19.useEffect)(() => {
|
|
3816
4546
|
if (ringRef.current.length !== columns) {
|
|
3817
4547
|
const next = new Float32Array(columns);
|
|
3818
4548
|
const prev = ringRef.current;
|
|
@@ -3824,7 +4554,7 @@ var ScrollingWaveform = ({
|
|
|
3824
4554
|
writeIdxRef.current = writeIdxRef.current % columns;
|
|
3825
4555
|
}
|
|
3826
4556
|
}, [columns]);
|
|
3827
|
-
(0,
|
|
4557
|
+
(0, import_react19.useEffect)(() => {
|
|
3828
4558
|
if (!active) {
|
|
3829
4559
|
if (rafRef.current !== null) {
|
|
3830
4560
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -3876,7 +4606,7 @@ var ScrollingWaveform = ({
|
|
|
3876
4606
|
}
|
|
3877
4607
|
};
|
|
3878
4608
|
}, [active, getPeakDb, fillStyle]);
|
|
3879
|
-
return /* @__PURE__ */ (0,
|
|
4609
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3880
4610
|
"canvas",
|
|
3881
4611
|
{
|
|
3882
4612
|
ref: canvasRef,
|
|
@@ -3887,8 +4617,8 @@ var ScrollingWaveform = ({
|
|
|
3887
4617
|
};
|
|
3888
4618
|
|
|
3889
4619
|
// src/components/OffsetScrubber.tsx
|
|
3890
|
-
var
|
|
3891
|
-
var
|
|
4620
|
+
var import_react20 = require("react");
|
|
4621
|
+
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
3892
4622
|
var SLIDER_HEIGHT_PX = 28;
|
|
3893
4623
|
var TICK_HEIGHT_PX = 14;
|
|
3894
4624
|
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
@@ -3901,40 +4631,40 @@ function OffsetScrubber({
|
|
|
3901
4631
|
onChange,
|
|
3902
4632
|
disabled = false
|
|
3903
4633
|
}) {
|
|
3904
|
-
const trackRef = (0,
|
|
3905
|
-
const [draftOffset, setDraftOffset] = (0,
|
|
3906
|
-
const [isDragging, setIsDragging] = (0,
|
|
3907
|
-
(0,
|
|
4634
|
+
const trackRef = (0, import_react20.useRef)(null);
|
|
4635
|
+
const [draftOffset, setDraftOffset] = (0, import_react20.useState)(offsetSamples);
|
|
4636
|
+
const [isDragging, setIsDragging] = (0, import_react20.useState)(false);
|
|
4637
|
+
(0, import_react20.useEffect)(() => {
|
|
3908
4638
|
if (!isDragging) setDraftOffset(offsetSamples);
|
|
3909
4639
|
}, [offsetSamples, isDragging]);
|
|
3910
4640
|
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
3911
4641
|
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
3912
|
-
const beatsForRange = (0,
|
|
4642
|
+
const beatsForRange = (0, import_react20.useMemo)(() => {
|
|
3913
4643
|
return Math.round(60 / projectBpm * sampleRate);
|
|
3914
4644
|
}, [projectBpm, sampleRate]);
|
|
3915
4645
|
const rangeSamples = beatsForRange * meter;
|
|
3916
|
-
const sampleToFraction = (0,
|
|
4646
|
+
const sampleToFraction = (0, import_react20.useCallback)(
|
|
3917
4647
|
(sample) => {
|
|
3918
4648
|
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
3919
4649
|
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
3920
4650
|
},
|
|
3921
4651
|
[rangeSamples]
|
|
3922
4652
|
);
|
|
3923
|
-
const fractionToSample = (0,
|
|
4653
|
+
const fractionToSample = (0, import_react20.useCallback)(
|
|
3924
4654
|
(fraction) => {
|
|
3925
4655
|
const clamped = Math.max(0, Math.min(1, fraction));
|
|
3926
4656
|
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
3927
4657
|
},
|
|
3928
4658
|
[rangeSamples]
|
|
3929
4659
|
);
|
|
3930
|
-
const snapTargets = (0,
|
|
4660
|
+
const snapTargets = (0, import_react20.useMemo)(() => {
|
|
3931
4661
|
if (!cuePoints || cuePoints.beats.length === 0) return [];
|
|
3932
4662
|
const downbeat = cuePoints.beats[0];
|
|
3933
4663
|
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
3934
4664
|
const negatives = positives.slice(1).map((p) => -p);
|
|
3935
4665
|
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
3936
4666
|
}, [cuePoints]);
|
|
3937
|
-
const snapToBeat = (0,
|
|
4667
|
+
const snapToBeat = (0, import_react20.useCallback)(
|
|
3938
4668
|
(sample) => {
|
|
3939
4669
|
if (snapTargets.length === 0) return sample;
|
|
3940
4670
|
let best = snapTargets[0];
|
|
@@ -3950,7 +4680,7 @@ function OffsetScrubber({
|
|
|
3950
4680
|
},
|
|
3951
4681
|
[snapTargets]
|
|
3952
4682
|
);
|
|
3953
|
-
const handlePointerDown = (0,
|
|
4683
|
+
const handlePointerDown = (0, import_react20.useCallback)(
|
|
3954
4684
|
(e) => {
|
|
3955
4685
|
if (disabled || !cuePoints) return;
|
|
3956
4686
|
e.preventDefault();
|
|
@@ -3984,7 +4714,7 @@ function OffsetScrubber({
|
|
|
3984
4714
|
},
|
|
3985
4715
|
[disabled, cuePoints, fractionToSample, onChange, snapToBeat]
|
|
3986
4716
|
);
|
|
3987
|
-
const handleResetToZero = (0,
|
|
4717
|
+
const handleResetToZero = (0, import_react20.useCallback)(() => {
|
|
3988
4718
|
if (disabled) return;
|
|
3989
4719
|
setDraftOffset(0);
|
|
3990
4720
|
onChange(0);
|
|
@@ -3992,7 +4722,7 @@ function OffsetScrubber({
|
|
|
3992
4722
|
const thumbFraction = sampleToFraction(draftOffset);
|
|
3993
4723
|
const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
|
|
3994
4724
|
const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
|
|
3995
|
-
const ticks = (0,
|
|
4725
|
+
const ticks = (0, import_react20.useMemo)(() => {
|
|
3996
4726
|
if (!cuePoints) return [];
|
|
3997
4727
|
const downbeat = cuePoints.beats[0] ?? 0;
|
|
3998
4728
|
return cuePoints.beats.map((b, i) => {
|
|
@@ -4003,9 +4733,9 @@ function OffsetScrubber({
|
|
|
4003
4733
|
});
|
|
4004
4734
|
}, [cuePoints, sampleToFraction]);
|
|
4005
4735
|
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
4006
|
-
return /* @__PURE__ */ (0,
|
|
4007
|
-
/* @__PURE__ */ (0,
|
|
4008
|
-
/* @__PURE__ */ (0,
|
|
4736
|
+
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
4737
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
4738
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
|
|
4009
4739
|
"div",
|
|
4010
4740
|
{
|
|
4011
4741
|
ref: trackRef,
|
|
@@ -4021,7 +4751,7 @@ function OffsetScrubber({
|
|
|
4021
4751
|
"aria-valuenow": draftOffset,
|
|
4022
4752
|
"aria-disabled": isDisabled,
|
|
4023
4753
|
children: [
|
|
4024
|
-
/* @__PURE__ */ (0,
|
|
4754
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4025
4755
|
"div",
|
|
4026
4756
|
{
|
|
4027
4757
|
"aria-hidden": "true",
|
|
@@ -4029,7 +4759,7 @@ function OffsetScrubber({
|
|
|
4029
4759
|
style: { left: "50%" }
|
|
4030
4760
|
}
|
|
4031
4761
|
),
|
|
4032
|
-
ticks.map((t) => /* @__PURE__ */ (0,
|
|
4762
|
+
ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4033
4763
|
"div",
|
|
4034
4764
|
{
|
|
4035
4765
|
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
@@ -4044,7 +4774,7 @@ function OffsetScrubber({
|
|
|
4044
4774
|
},
|
|
4045
4775
|
t.i
|
|
4046
4776
|
)),
|
|
4047
|
-
/* @__PURE__ */ (0,
|
|
4777
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4048
4778
|
"div",
|
|
4049
4779
|
{
|
|
4050
4780
|
"data-testid": "offset-scrubber-thumb",
|
|
@@ -4061,7 +4791,7 @@ function OffsetScrubber({
|
|
|
4061
4791
|
]
|
|
4062
4792
|
}
|
|
4063
4793
|
),
|
|
4064
|
-
/* @__PURE__ */ (0,
|
|
4794
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4065
4795
|
"span",
|
|
4066
4796
|
{
|
|
4067
4797
|
"data-testid": "offset-scrubber-readout",
|
|
@@ -4069,7 +4799,7 @@ function OffsetScrubber({
|
|
|
4069
4799
|
children: formatOffset(draftOffset, sampleRate)
|
|
4070
4800
|
}
|
|
4071
4801
|
),
|
|
4072
|
-
/* @__PURE__ */ (0,
|
|
4802
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4073
4803
|
"button",
|
|
4074
4804
|
{
|
|
4075
4805
|
type: "button",
|
|
@@ -4081,7 +4811,7 @@ function OffsetScrubber({
|
|
|
4081
4811
|
children: "\u2316"
|
|
4082
4812
|
}
|
|
4083
4813
|
),
|
|
4084
|
-
bpmMismatch && /* @__PURE__ */ (0,
|
|
4814
|
+
bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4085
4815
|
"span",
|
|
4086
4816
|
{
|
|
4087
4817
|
"data-testid": "offset-bpm-mismatch",
|
|
@@ -4153,13 +4883,13 @@ function synthesizeCuePoints({
|
|
|
4153
4883
|
}
|
|
4154
4884
|
|
|
4155
4885
|
// src/hooks/useSceneState.ts
|
|
4156
|
-
var
|
|
4886
|
+
var import_react21 = require("react");
|
|
4157
4887
|
function useSceneState(activeSceneId, initialValue) {
|
|
4158
|
-
const [stateMap, setStateMap] = (0,
|
|
4159
|
-
const activeSceneIdRef = (0,
|
|
4888
|
+
const [stateMap, setStateMap] = (0, import_react21.useState)(() => /* @__PURE__ */ new Map());
|
|
4889
|
+
const activeSceneIdRef = (0, import_react21.useRef)(activeSceneId);
|
|
4160
4890
|
activeSceneIdRef.current = activeSceneId;
|
|
4161
4891
|
const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
|
|
4162
|
-
const setForCurrentScene = (0,
|
|
4892
|
+
const setForCurrentScene = (0, import_react21.useCallback)((value) => {
|
|
4163
4893
|
const sid = activeSceneIdRef.current;
|
|
4164
4894
|
if (sid === null) return;
|
|
4165
4895
|
setStateMap((prev) => {
|
|
@@ -4170,7 +4900,7 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
4170
4900
|
return newMap;
|
|
4171
4901
|
});
|
|
4172
4902
|
}, [initialValue]);
|
|
4173
|
-
const setForScene = (0,
|
|
4903
|
+
const setForScene = (0, import_react21.useCallback)((sceneId, value) => {
|
|
4174
4904
|
setStateMap((prev) => {
|
|
4175
4905
|
const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
|
|
4176
4906
|
const next = typeof value === "function" ? value(current) : value;
|
|
@@ -4183,10 +4913,10 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
4183
4913
|
}
|
|
4184
4914
|
|
|
4185
4915
|
// src/hooks/useAnySolo.ts
|
|
4186
|
-
var
|
|
4916
|
+
var import_react22 = require("react");
|
|
4187
4917
|
function useAnySolo(host) {
|
|
4188
|
-
const [anySolo, setAnySolo] = (0,
|
|
4189
|
-
(0,
|
|
4918
|
+
const [anySolo, setAnySolo] = (0, import_react22.useState)(false);
|
|
4919
|
+
(0, import_react22.useEffect)(() => {
|
|
4190
4920
|
let active = true;
|
|
4191
4921
|
const refresh = () => {
|
|
4192
4922
|
host.isAnySoloActive().then((v) => {
|
|
@@ -4205,7 +4935,7 @@ function useAnySolo(host) {
|
|
|
4205
4935
|
}
|
|
4206
4936
|
|
|
4207
4937
|
// src/hooks/useSoundHistory.ts
|
|
4208
|
-
var
|
|
4938
|
+
var import_react23 = require("react");
|
|
4209
4939
|
var EMPTY = { entries: [], cursor: -1 };
|
|
4210
4940
|
function sameDescriptor(a, b) {
|
|
4211
4941
|
if (a === b) return true;
|
|
@@ -4217,14 +4947,14 @@ function sameDescriptor(a, b) {
|
|
|
4217
4947
|
}
|
|
4218
4948
|
function useSoundHistory(applySound, opts = {}) {
|
|
4219
4949
|
const max = Math.max(2, opts.max ?? 24);
|
|
4220
|
-
const applyRef = (0,
|
|
4950
|
+
const applyRef = (0, import_react23.useRef)(applySound);
|
|
4221
4951
|
applyRef.current = applySound;
|
|
4222
|
-
const onChangeRef = (0,
|
|
4952
|
+
const onChangeRef = (0, import_react23.useRef)(opts.onChange);
|
|
4223
4953
|
onChangeRef.current = opts.onChange;
|
|
4224
|
-
const dataRef = (0,
|
|
4225
|
-
const [, setVersion] = (0,
|
|
4226
|
-
const bump = (0,
|
|
4227
|
-
const commit = (0,
|
|
4954
|
+
const dataRef = (0, import_react23.useRef)({});
|
|
4955
|
+
const [, setVersion] = (0, import_react23.useState)(0);
|
|
4956
|
+
const bump = (0, import_react23.useCallback)(() => setVersion((v) => v + 1), []);
|
|
4957
|
+
const commit = (0, import_react23.useCallback)(
|
|
4228
4958
|
(trackId, next, notify) => {
|
|
4229
4959
|
dataRef.current = { ...dataRef.current, [trackId]: next };
|
|
4230
4960
|
bump();
|
|
@@ -4232,7 +4962,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4232
4962
|
},
|
|
4233
4963
|
[bump]
|
|
4234
4964
|
);
|
|
4235
|
-
const record = (0,
|
|
4965
|
+
const record = (0, import_react23.useCallback)(
|
|
4236
4966
|
(trackId, descriptor, label) => {
|
|
4237
4967
|
const h = dataRef.current[trackId];
|
|
4238
4968
|
const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
|
|
@@ -4247,7 +4977,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4247
4977
|
},
|
|
4248
4978
|
[max, commit]
|
|
4249
4979
|
);
|
|
4250
|
-
const restoreTo = (0,
|
|
4980
|
+
const restoreTo = (0, import_react23.useCallback)(
|
|
4251
4981
|
async (trackId, index) => {
|
|
4252
4982
|
const h = dataRef.current[trackId];
|
|
4253
4983
|
if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
|
|
@@ -4257,7 +4987,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4257
4987
|
},
|
|
4258
4988
|
[commit]
|
|
4259
4989
|
);
|
|
4260
|
-
const undo = (0,
|
|
4990
|
+
const undo = (0, import_react23.useCallback)(
|
|
4261
4991
|
(trackId) => {
|
|
4262
4992
|
const h = dataRef.current[trackId];
|
|
4263
4993
|
if (!h || h.cursor <= 0) return Promise.resolve(false);
|
|
@@ -4265,7 +4995,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4265
4995
|
},
|
|
4266
4996
|
[restoreTo]
|
|
4267
4997
|
);
|
|
4268
|
-
const toggleFavorite = (0,
|
|
4998
|
+
const toggleFavorite = (0, import_react23.useCallback)(
|
|
4269
4999
|
(trackId, index) => {
|
|
4270
5000
|
const h = dataRef.current[trackId];
|
|
4271
5001
|
if (!h || index < 0 || index >= h.entries.length) return;
|
|
@@ -4274,7 +5004,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4274
5004
|
},
|
|
4275
5005
|
[commit]
|
|
4276
5006
|
);
|
|
4277
|
-
const restore = (0,
|
|
5007
|
+
const restore = (0, import_react23.useCallback)(
|
|
4278
5008
|
(trackId, state) => {
|
|
4279
5009
|
const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
|
|
4280
5010
|
const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
|
|
@@ -4283,15 +5013,15 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4283
5013
|
},
|
|
4284
5014
|
[commit]
|
|
4285
5015
|
);
|
|
4286
|
-
const list = (0,
|
|
5016
|
+
const list = (0, import_react23.useCallback)(
|
|
4287
5017
|
(trackId) => dataRef.current[trackId] ?? EMPTY,
|
|
4288
5018
|
[]
|
|
4289
5019
|
);
|
|
4290
|
-
const canUndo = (0,
|
|
5020
|
+
const canUndo = (0, import_react23.useCallback)((trackId) => {
|
|
4291
5021
|
const h = dataRef.current[trackId];
|
|
4292
5022
|
return !!h && h.cursor > 0;
|
|
4293
5023
|
}, []);
|
|
4294
|
-
const clear = (0,
|
|
5024
|
+
const clear = (0, import_react23.useCallback)(
|
|
4295
5025
|
(trackId) => {
|
|
4296
5026
|
if (dataRef.current[trackId]) {
|
|
4297
5027
|
const next = { ...dataRef.current };
|
|
@@ -4303,102 +5033,18 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
4303
5033
|
},
|
|
4304
5034
|
[bump]
|
|
4305
5035
|
);
|
|
4306
|
-
const reset = (0,
|
|
5036
|
+
const reset = (0, import_react23.useCallback)(() => {
|
|
4307
5037
|
dataRef.current = {};
|
|
4308
5038
|
bump();
|
|
4309
5039
|
}, [bump]);
|
|
4310
|
-
return (0,
|
|
5040
|
+
return (0, import_react23.useMemo)(
|
|
4311
5041
|
() => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
|
|
4312
5042
|
[record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
|
|
4313
5043
|
);
|
|
4314
5044
|
}
|
|
4315
5045
|
|
|
4316
|
-
// src/hooks/useTrackReorder.ts
|
|
4317
|
-
var import_react22 = require("react");
|
|
4318
|
-
function moveItem(arr, from, to) {
|
|
4319
|
-
const next = arr.slice();
|
|
4320
|
-
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
4321
|
-
return next;
|
|
4322
|
-
}
|
|
4323
|
-
const [moved] = next.splice(from, 1);
|
|
4324
|
-
next.splice(to, 0, moved);
|
|
4325
|
-
return next;
|
|
4326
|
-
}
|
|
4327
|
-
function useTrackReorder({
|
|
4328
|
-
host,
|
|
4329
|
-
items,
|
|
4330
|
-
setItems,
|
|
4331
|
-
getId,
|
|
4332
|
-
onError
|
|
4333
|
-
}) {
|
|
4334
|
-
const [draggingIndex, setDraggingIndex] = (0, import_react22.useState)(null);
|
|
4335
|
-
const [dragOverIndex, setDragOverIndex] = (0, import_react22.useState)(null);
|
|
4336
|
-
const fromRef = (0, import_react22.useRef)(null);
|
|
4337
|
-
const itemsRef = (0, import_react22.useRef)(items);
|
|
4338
|
-
itemsRef.current = items;
|
|
4339
|
-
const dragPropsFor = (0, import_react22.useCallback)(
|
|
4340
|
-
(index) => ({
|
|
4341
|
-
handleProps: {
|
|
4342
|
-
draggable: true,
|
|
4343
|
-
onDragStart: (e) => {
|
|
4344
|
-
fromRef.current = index;
|
|
4345
|
-
setDraggingIndex(index);
|
|
4346
|
-
if (e.dataTransfer) {
|
|
4347
|
-
e.dataTransfer.effectAllowed = "move";
|
|
4348
|
-
try {
|
|
4349
|
-
e.dataTransfer.setData("text/plain", String(index));
|
|
4350
|
-
} catch {
|
|
4351
|
-
}
|
|
4352
|
-
}
|
|
4353
|
-
},
|
|
4354
|
-
onDragEnd: () => {
|
|
4355
|
-
fromRef.current = null;
|
|
4356
|
-
setDraggingIndex(null);
|
|
4357
|
-
setDragOverIndex(null);
|
|
4358
|
-
}
|
|
4359
|
-
},
|
|
4360
|
-
rowProps: {
|
|
4361
|
-
onDragEnter: (e) => {
|
|
4362
|
-
if (fromRef.current === null) return;
|
|
4363
|
-
e.preventDefault();
|
|
4364
|
-
setDragOverIndex(index);
|
|
4365
|
-
},
|
|
4366
|
-
onDragOver: (e) => {
|
|
4367
|
-
if (fromRef.current === null) return;
|
|
4368
|
-
e.preventDefault();
|
|
4369
|
-
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
4370
|
-
setDragOverIndex((cur) => cur === index ? cur : index);
|
|
4371
|
-
},
|
|
4372
|
-
onDragLeave: () => {
|
|
4373
|
-
setDragOverIndex((cur) => cur === index ? null : cur);
|
|
4374
|
-
},
|
|
4375
|
-
onDrop: (e) => {
|
|
4376
|
-
e.preventDefault();
|
|
4377
|
-
const from = fromRef.current;
|
|
4378
|
-
fromRef.current = null;
|
|
4379
|
-
setDraggingIndex(null);
|
|
4380
|
-
setDragOverIndex(null);
|
|
4381
|
-
if (from === null || from === index) return;
|
|
4382
|
-
const prev = itemsRef.current;
|
|
4383
|
-
const next = moveItem(prev, from, index);
|
|
4384
|
-
setItems(next);
|
|
4385
|
-
const ids = next.map(getId);
|
|
4386
|
-
Promise.resolve(host.reorderTracks(ids)).catch((err) => {
|
|
4387
|
-
setItems(prev);
|
|
4388
|
-
onError?.(err);
|
|
4389
|
-
});
|
|
4390
|
-
}
|
|
4391
|
-
},
|
|
4392
|
-
isDragging: draggingIndex === index,
|
|
4393
|
-
isDragTarget: dragOverIndex === index && draggingIndex !== index
|
|
4394
|
-
}),
|
|
4395
|
-
[host, setItems, getId, onError, draggingIndex, dragOverIndex]
|
|
4396
|
-
);
|
|
4397
|
-
return { dragPropsFor, draggingIndex, dragOverIndex };
|
|
4398
|
-
}
|
|
4399
|
-
|
|
4400
5046
|
// src/constants/sdk-version.ts
|
|
4401
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
5047
|
+
var PLUGIN_SDK_VERSION = "2.34.0";
|
|
4402
5048
|
|
|
4403
5049
|
// src/utils/format-concurrent-tracks.ts
|
|
4404
5050
|
function formatConcurrentTracks(ctx) {
|
|
@@ -4542,6 +5188,8 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4542
5188
|
}
|
|
4543
5189
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4544
5190
|
0 && (module.exports = {
|
|
5191
|
+
AUDIO_EFFECTS,
|
|
5192
|
+
AUDIO_EFFECT_LABEL,
|
|
4545
5193
|
ConfirmDialog,
|
|
4546
5194
|
CrossfadeModal,
|
|
4547
5195
|
CrossfadeTrackRow,
|
|
@@ -4580,34 +5228,49 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4580
5228
|
ScrollingWaveform,
|
|
4581
5229
|
SorceryProgressBar,
|
|
4582
5230
|
TEXTURAL_ROLES,
|
|
5231
|
+
TRANSITION_DESIGNER_DRAFT_KEY,
|
|
4583
5232
|
TrackDrawer,
|
|
4584
5233
|
TrackMeterStrip,
|
|
4585
5234
|
TrackRow,
|
|
5235
|
+
TransitionDesigner,
|
|
4586
5236
|
VolumeSlider,
|
|
4587
5237
|
WaveformView,
|
|
4588
5238
|
analyzeWavPeak,
|
|
5239
|
+
asAudioEffect,
|
|
4589
5240
|
asCrossfadeMeta,
|
|
4590
5241
|
asFadeMeta,
|
|
5242
|
+
asTransitionDesignerDraft,
|
|
4591
5243
|
buildCrossfadeInpaintPrompt,
|
|
4592
5244
|
buildCrossfadeVolumeCurves,
|
|
4593
5245
|
buildFadeVolumeCurve,
|
|
5246
|
+
buildRowSlots,
|
|
4594
5247
|
calculateTimeBasedTarget,
|
|
4595
5248
|
cellToPx,
|
|
4596
5249
|
centerScrollTop,
|
|
4597
5250
|
computePeaks,
|
|
5251
|
+
dbIdsFromKeys,
|
|
4598
5252
|
dbToSlider,
|
|
4599
5253
|
defaultFadeGesture,
|
|
4600
5254
|
drawWaveform,
|
|
4601
5255
|
formatConcurrentTracks,
|
|
5256
|
+
hashString,
|
|
4602
5257
|
moveItem,
|
|
5258
|
+
normalizeSlots,
|
|
5259
|
+
padPair,
|
|
5260
|
+
padSlots,
|
|
4603
5261
|
parseCrossfadePairs,
|
|
4604
5262
|
parseFades,
|
|
4605
5263
|
pickTopKWeighted,
|
|
4606
5264
|
pitchToName,
|
|
4607
5265
|
pxToCell,
|
|
5266
|
+
reconcileSlots,
|
|
4608
5267
|
resizeNoteDuration,
|
|
5268
|
+
rowKey,
|
|
5269
|
+
rowType,
|
|
4609
5270
|
scorePromptMatch,
|
|
4610
5271
|
sliderToDb,
|
|
5272
|
+
slotsEqual,
|
|
5273
|
+
soundIdentity,
|
|
4611
5274
|
synthesizeCuePoints,
|
|
4612
5275
|
tokenizePrompt,
|
|
4613
5276
|
transposeNotes,
|