@signalsandsorcery/plugin-sdk 2.25.1 → 2.28.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 +235 -6
- package/dist/index.d.ts +235 -6
- package/dist/index.js +702 -185
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +695 -185
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2465,6 +2465,8 @@ function parseCrossfadePairs(sceneData) {
|
|
|
2465
2465
|
sliderPos: g.origin.meta.sliderPos,
|
|
2466
2466
|
originDbId: g.origin.dbId,
|
|
2467
2467
|
targetDbId: g.target.dbId,
|
|
2468
|
+
originSourceDbId: g.origin.meta.sourceTrackDbId,
|
|
2469
|
+
targetSourceDbId: g.target.meta.sourceTrackDbId,
|
|
2468
2470
|
originSourceName: g.origin.meta.sourceName,
|
|
2469
2471
|
originSoundLabel: g.origin.meta.soundLabel,
|
|
2470
2472
|
targetSourceName: g.target.meta.sourceName,
|
|
@@ -2568,9 +2570,448 @@ function buildCrossfadeInpaintPrompt(input) {
|
|
|
2568
2570
|
return lines.join("\n");
|
|
2569
2571
|
}
|
|
2570
2572
|
|
|
2573
|
+
// src/fade-meta.ts
|
|
2574
|
+
function asFadeMeta(val) {
|
|
2575
|
+
if (!val || typeof val !== "object") return null;
|
|
2576
|
+
const m = val;
|
|
2577
|
+
if (m.direction !== "in" && m.direction !== "out") return null;
|
|
2578
|
+
if (m.gesture !== "volume" && m.gesture !== "build") return null;
|
|
2579
|
+
return {
|
|
2580
|
+
direction: m.direction,
|
|
2581
|
+
gesture: m.gesture,
|
|
2582
|
+
sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
|
|
2583
|
+
sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
|
|
2584
|
+
sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
|
|
2585
|
+
soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
|
|
2586
|
+
sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
function parseFades(sceneData) {
|
|
2590
|
+
const out = [];
|
|
2591
|
+
for (const [key, val] of Object.entries(sceneData)) {
|
|
2592
|
+
const match = /^track:(.+):fade$/.exec(key);
|
|
2593
|
+
if (!match) continue;
|
|
2594
|
+
const meta = asFadeMeta(val);
|
|
2595
|
+
if (!meta) continue;
|
|
2596
|
+
out.push({ dbId: match[1], meta });
|
|
2597
|
+
}
|
|
2598
|
+
return out;
|
|
2599
|
+
}
|
|
2600
|
+
function buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture, steps = 32) {
|
|
2601
|
+
const durationSeconds = bars * 4 * 60 / Math.max(1, bpm);
|
|
2602
|
+
if (gesture === "build") {
|
|
2603
|
+
return [
|
|
2604
|
+
{ time: 0, db: 0 },
|
|
2605
|
+
{ time: Math.round(durationSeconds * 1e3) / 1e3, db: 0 }
|
|
2606
|
+
];
|
|
2607
|
+
}
|
|
2608
|
+
const s = Math.min(0.98, Math.max(0.02, sliderPos));
|
|
2609
|
+
const round = (n) => Math.round(n * 1e3) / 1e3;
|
|
2610
|
+
const points = [];
|
|
2611
|
+
for (let i = 0; i <= steps; i++) {
|
|
2612
|
+
const x = i / steps;
|
|
2613
|
+
const time = round(x * durationSeconds);
|
|
2614
|
+
const theta = x <= s ? x / s * (Math.PI / 4) : Math.PI / 4 + (x - s) / (1 - s) * (Math.PI / 4);
|
|
2615
|
+
const gain = direction === "out" ? Math.cos(theta) : Math.sin(theta);
|
|
2616
|
+
points.push({ time, db: Math.round(gainToDb(gain) * 100) / 100 });
|
|
2617
|
+
}
|
|
2618
|
+
return points;
|
|
2619
|
+
}
|
|
2620
|
+
var TEXTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2621
|
+
"pads",
|
|
2622
|
+
"pad",
|
|
2623
|
+
"strings",
|
|
2624
|
+
"atmospheres",
|
|
2625
|
+
"atmosphere",
|
|
2626
|
+
"atmos",
|
|
2627
|
+
"drones",
|
|
2628
|
+
"drone",
|
|
2629
|
+
"soundscapes",
|
|
2630
|
+
"soundscape"
|
|
2631
|
+
]);
|
|
2632
|
+
function defaultFadeGesture(role) {
|
|
2633
|
+
if (!role) return "build";
|
|
2634
|
+
const norm = role.toLowerCase().replace(/[\s_-]+/g, " ").trim();
|
|
2635
|
+
if (TEXTURAL_ROLES.has(norm)) return "volume";
|
|
2636
|
+
for (const token of norm.split(" ")) {
|
|
2637
|
+
if (TEXTURAL_ROLES.has(token)) return "volume";
|
|
2638
|
+
}
|
|
2639
|
+
return "build";
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
// src/components/FadeTrackRow.tsx
|
|
2643
|
+
import React10 from "react";
|
|
2644
|
+
import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2645
|
+
function FadeCaption({
|
|
2646
|
+
layer,
|
|
2647
|
+
direction,
|
|
2648
|
+
gesture
|
|
2649
|
+
}) {
|
|
2650
|
+
const tag = direction === "in" ? "Fade in" : "Fade out";
|
|
2651
|
+
return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
|
|
2652
|
+
/* @__PURE__ */ jsx13("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
|
|
2653
|
+
/* @__PURE__ */ jsx13("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
|
|
2654
|
+
layer.soundLabel && /* @__PURE__ */ jsxs9("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
|
|
2655
|
+
"\xB7 ",
|
|
2656
|
+
layer.soundLabel
|
|
2657
|
+
] }),
|
|
2658
|
+
/* @__PURE__ */ jsxs9("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", title: `Fade gesture: ${gesture}`, children: [
|
|
2659
|
+
"\xB7 ",
|
|
2660
|
+
gesture
|
|
2661
|
+
] })
|
|
2662
|
+
] });
|
|
2663
|
+
}
|
|
2664
|
+
function FadeTrackRow({
|
|
2665
|
+
layer,
|
|
2666
|
+
direction,
|
|
2667
|
+
gesture,
|
|
2668
|
+
sliderPos = 0.5,
|
|
2669
|
+
onMuteToggle,
|
|
2670
|
+
onSoloToggle,
|
|
2671
|
+
onVolumeChange,
|
|
2672
|
+
onPanChange,
|
|
2673
|
+
onDelete,
|
|
2674
|
+
onSliderChange,
|
|
2675
|
+
levels,
|
|
2676
|
+
accentColor = "#9333EA"
|
|
2677
|
+
}) {
|
|
2678
|
+
const [confirmDelete, setConfirmDelete] = React10.useState(false);
|
|
2679
|
+
const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
|
|
2680
|
+
const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
|
|
2681
|
+
const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
|
|
2682
|
+
return /* @__PURE__ */ jsxs9(
|
|
2683
|
+
"div",
|
|
2684
|
+
{
|
|
2685
|
+
"data-testid": "fade-track-row",
|
|
2686
|
+
className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
|
|
2687
|
+
style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
|
|
2688
|
+
children: [
|
|
2689
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
|
|
2690
|
+
/* @__PURE__ */ jsx13(
|
|
2691
|
+
"span",
|
|
2692
|
+
{
|
|
2693
|
+
"data-testid": "fade-direction-badge",
|
|
2694
|
+
className: "text-[10px] font-bold uppercase tracking-wide",
|
|
2695
|
+
style: { color: accentColor },
|
|
2696
|
+
children: badge
|
|
2697
|
+
}
|
|
2698
|
+
),
|
|
2699
|
+
/* @__PURE__ */ jsx13(
|
|
2700
|
+
"button",
|
|
2701
|
+
{
|
|
2702
|
+
"data-testid": "fade-delete-button",
|
|
2703
|
+
onClick: () => setConfirmDelete(true),
|
|
2704
|
+
className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
|
|
2705
|
+
title: "Delete fade",
|
|
2706
|
+
"aria-label": "Delete fade",
|
|
2707
|
+
children: "x"
|
|
2708
|
+
}
|
|
2709
|
+
)
|
|
2710
|
+
] }),
|
|
2711
|
+
/* @__PURE__ */ jsx13(
|
|
2712
|
+
TrackRow,
|
|
2713
|
+
{
|
|
2714
|
+
track: { id: layer.trackId, name: "", role: layer.role },
|
|
2715
|
+
runtimeState: layer.runtimeState,
|
|
2716
|
+
fxDetailState: EMPTY_FX_DETAIL_STATE,
|
|
2717
|
+
drawerOpen: false,
|
|
2718
|
+
drawerTab: "fx",
|
|
2719
|
+
levels,
|
|
2720
|
+
accentColor,
|
|
2721
|
+
contentSlot: /* @__PURE__ */ jsx13(FadeCaption, { layer, direction, gesture }),
|
|
2722
|
+
onMuteToggle,
|
|
2723
|
+
onSoloToggle,
|
|
2724
|
+
onVolumeChange,
|
|
2725
|
+
onPanChange
|
|
2726
|
+
}
|
|
2727
|
+
),
|
|
2728
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "fade-slider-row", children: [
|
|
2729
|
+
/* @__PURE__ */ jsx13(
|
|
2730
|
+
"span",
|
|
2731
|
+
{
|
|
2732
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
|
|
2733
|
+
title: leftLabel,
|
|
2734
|
+
children: leftLabel
|
|
2735
|
+
}
|
|
2736
|
+
),
|
|
2737
|
+
/* @__PURE__ */ jsx13(
|
|
2738
|
+
"input",
|
|
2739
|
+
{
|
|
2740
|
+
type: "range",
|
|
2741
|
+
"data-testid": "fade-slider",
|
|
2742
|
+
min: 0,
|
|
2743
|
+
max: 1,
|
|
2744
|
+
step: 0.01,
|
|
2745
|
+
value: sliderPos,
|
|
2746
|
+
disabled: !onSliderChange,
|
|
2747
|
+
onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
|
|
2748
|
+
style: { accentColor },
|
|
2749
|
+
className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
|
|
2750
|
+
"aria-label": "Fade position"
|
|
2751
|
+
}
|
|
2752
|
+
),
|
|
2753
|
+
/* @__PURE__ */ jsx13(
|
|
2754
|
+
"span",
|
|
2755
|
+
{
|
|
2756
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
|
|
2757
|
+
title: rightLabel,
|
|
2758
|
+
children: rightLabel
|
|
2759
|
+
}
|
|
2760
|
+
)
|
|
2761
|
+
] }),
|
|
2762
|
+
/* @__PURE__ */ jsx13(
|
|
2763
|
+
ConfirmDialog,
|
|
2764
|
+
{
|
|
2765
|
+
open: confirmDelete,
|
|
2766
|
+
title: "Delete fade?",
|
|
2767
|
+
message: /* @__PURE__ */ jsx13(Fragment3, { children: "This fade track will be permanently removed from this scene. This cannot be undone." }),
|
|
2768
|
+
confirmLabel: "Delete",
|
|
2769
|
+
onConfirm: () => {
|
|
2770
|
+
setConfirmDelete(false);
|
|
2771
|
+
onDelete();
|
|
2772
|
+
},
|
|
2773
|
+
onCancel: () => setConfirmDelete(false),
|
|
2774
|
+
testIdPrefix: "fade-delete-confirm"
|
|
2775
|
+
}
|
|
2776
|
+
)
|
|
2777
|
+
]
|
|
2778
|
+
}
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// src/components/FadeModal.tsx
|
|
2783
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef7, useState as useState7 } from "react";
|
|
2784
|
+
import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2785
|
+
function shortId(dbId) {
|
|
2786
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
2787
|
+
}
|
|
2788
|
+
var normRole = (r) => (r ?? "").toLowerCase().trim();
|
|
2789
|
+
function computeOrphans(from, to, excludeSet) {
|
|
2790
|
+
const bucket = (list) => {
|
|
2791
|
+
const m = /* @__PURE__ */ new Map();
|
|
2792
|
+
for (const t of list) {
|
|
2793
|
+
const k = normRole(t.role);
|
|
2794
|
+
const arr = m.get(k);
|
|
2795
|
+
if (arr) arr.push(t);
|
|
2796
|
+
else m.set(k, [t]);
|
|
2797
|
+
}
|
|
2798
|
+
return m;
|
|
2799
|
+
};
|
|
2800
|
+
const fromByRole = bucket(from);
|
|
2801
|
+
const toByRole = bucket(to);
|
|
2802
|
+
const roles = /* @__PURE__ */ new Set([...fromByRole.keys(), ...toByRole.keys()]);
|
|
2803
|
+
const fadeOut = [];
|
|
2804
|
+
const fadeIn = [];
|
|
2805
|
+
for (const role of roles) {
|
|
2806
|
+
const f = fromByRole.get(role) ?? [];
|
|
2807
|
+
const t = toByRole.get(role) ?? [];
|
|
2808
|
+
const shared = Math.min(f.length, t.length);
|
|
2809
|
+
fadeOut.push(...f.slice(shared));
|
|
2810
|
+
fadeIn.push(...t.slice(shared));
|
|
2811
|
+
}
|
|
2812
|
+
return {
|
|
2813
|
+
fadeOut: fadeOut.filter((x) => !excludeSet.has(x.dbId)),
|
|
2814
|
+
fadeIn: fadeIn.filter((x) => !excludeSet.has(x.dbId))
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
function OrphanRow({
|
|
2818
|
+
track,
|
|
2819
|
+
gesture,
|
|
2820
|
+
selected,
|
|
2821
|
+
disabled,
|
|
2822
|
+
onSelect,
|
|
2823
|
+
testId
|
|
2824
|
+
}) {
|
|
2825
|
+
const primary = track.prompt?.trim() || track.name;
|
|
2826
|
+
const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
|
|
2827
|
+
return /* @__PURE__ */ jsxs10(
|
|
2828
|
+
"button",
|
|
2829
|
+
{
|
|
2830
|
+
type: "button",
|
|
2831
|
+
role: "radio",
|
|
2832
|
+
"aria-checked": selected,
|
|
2833
|
+
"data-testid": testId,
|
|
2834
|
+
"data-value": track.dbId,
|
|
2835
|
+
onClick: onSelect,
|
|
2836
|
+
disabled,
|
|
2837
|
+
className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
|
|
2838
|
+
children: [
|
|
2839
|
+
/* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
2840
|
+
meta && /* @__PURE__ */ jsx14("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
2841
|
+
]
|
|
2842
|
+
}
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
function FadeModal({
|
|
2846
|
+
host,
|
|
2847
|
+
open,
|
|
2848
|
+
fromSceneId,
|
|
2849
|
+
toSceneId,
|
|
2850
|
+
fromSceneName,
|
|
2851
|
+
toSceneName,
|
|
2852
|
+
excludeSourceDbIds,
|
|
2853
|
+
onClose,
|
|
2854
|
+
onCreate,
|
|
2855
|
+
testIdPrefix = "fade-modal"
|
|
2856
|
+
}) {
|
|
2857
|
+
const [load, setLoad] = useState7({ status: "loading" });
|
|
2858
|
+
const [selectedDbId, setSelectedDbId] = useState7("");
|
|
2859
|
+
const [isCreating, setIsCreating] = useState7(false);
|
|
2860
|
+
const [error, setError] = useState7(null);
|
|
2861
|
+
const [fromName, setFromName] = useState7(null);
|
|
2862
|
+
const [toName, setToName] = useState7(null);
|
|
2863
|
+
const cancelRef = useRef7(null);
|
|
2864
|
+
const refresh = useCallback4(async () => {
|
|
2865
|
+
if (!host.listSceneFamilyTracks) {
|
|
2866
|
+
setLoad({ status: "error", message: "This host does not support fades." });
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
setLoad({ status: "loading" });
|
|
2870
|
+
try {
|
|
2871
|
+
const [from, to, fName, tName] = await Promise.all([
|
|
2872
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
2873
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
2874
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
2875
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null)
|
|
2876
|
+
]);
|
|
2877
|
+
setFromName(fName);
|
|
2878
|
+
setToName(tName);
|
|
2879
|
+
setLoad({ status: "ready", from, to });
|
|
2880
|
+
} catch (err) {
|
|
2881
|
+
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
2882
|
+
}
|
|
2883
|
+
}, [host, fromSceneId, toSceneId]);
|
|
2884
|
+
useEffect6(() => {
|
|
2885
|
+
if (open) {
|
|
2886
|
+
setError(null);
|
|
2887
|
+
setIsCreating(false);
|
|
2888
|
+
setSelectedDbId("");
|
|
2889
|
+
void refresh();
|
|
2890
|
+
}
|
|
2891
|
+
}, [open, refresh]);
|
|
2892
|
+
const excludeSet = useMemo3(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
2893
|
+
const { fadeOut, fadeIn } = useMemo3(
|
|
2894
|
+
() => load.status === "ready" ? computeOrphans(load.from, load.to, excludeSet) : { fadeOut: [], fadeIn: [] },
|
|
2895
|
+
[load, excludeSet]
|
|
2896
|
+
);
|
|
2897
|
+
const allOrphans = useMemo3(
|
|
2898
|
+
() => [
|
|
2899
|
+
...fadeOut.map((t) => ({ track: t, direction: "out" })),
|
|
2900
|
+
...fadeIn.map((t) => ({ track: t, direction: "in" }))
|
|
2901
|
+
],
|
|
2902
|
+
[fadeOut, fadeIn]
|
|
2903
|
+
);
|
|
2904
|
+
useEffect6(() => {
|
|
2905
|
+
if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {
|
|
2906
|
+
setSelectedDbId(allOrphans[0]?.track.dbId ?? "");
|
|
2907
|
+
}
|
|
2908
|
+
}, [allOrphans, selectedDbId]);
|
|
2909
|
+
const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;
|
|
2910
|
+
const canCreate = !isCreating && !!selected;
|
|
2911
|
+
const handleClose = useCallback4(() => {
|
|
2912
|
+
if (!isCreating) onClose();
|
|
2913
|
+
}, [isCreating, onClose]);
|
|
2914
|
+
const handleCreate = useCallback4(async () => {
|
|
2915
|
+
if (!selected) return;
|
|
2916
|
+
setIsCreating(true);
|
|
2917
|
+
setError(null);
|
|
2918
|
+
try {
|
|
2919
|
+
await onCreate(
|
|
2920
|
+
{ dbId: selected.track.dbId, name: selected.track.name, role: selected.track.role },
|
|
2921
|
+
selected.direction,
|
|
2922
|
+
defaultFadeGesture(selected.track.role)
|
|
2923
|
+
);
|
|
2924
|
+
onClose();
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
setError(err instanceof Error ? err.message : "Failed to create fade.");
|
|
2927
|
+
setIsCreating(false);
|
|
2928
|
+
}
|
|
2929
|
+
}, [selected, onCreate, onClose]);
|
|
2930
|
+
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
2931
|
+
const toLabel = toName ?? toSceneName ?? null;
|
|
2932
|
+
if (!open) return null;
|
|
2933
|
+
const renderSection = (heading, list, section) => {
|
|
2934
|
+
if (list.length === 0) return null;
|
|
2935
|
+
return /* @__PURE__ */ jsxs10("div", { className: "block", children: [
|
|
2936
|
+
/* @__PURE__ */ jsx14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
|
|
2937
|
+
/* @__PURE__ */ jsx14(
|
|
2938
|
+
"div",
|
|
2939
|
+
{
|
|
2940
|
+
role: "radiogroup",
|
|
2941
|
+
"aria-label": heading,
|
|
2942
|
+
"data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
|
|
2943
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
2944
|
+
children: list.map((t) => /* @__PURE__ */ jsx14(
|
|
2945
|
+
OrphanRow,
|
|
2946
|
+
{
|
|
2947
|
+
track: t,
|
|
2948
|
+
gesture: defaultFadeGesture(t.role),
|
|
2949
|
+
selected: t.dbId === selectedDbId,
|
|
2950
|
+
disabled: isCreating,
|
|
2951
|
+
onSelect: () => setSelectedDbId(t.dbId),
|
|
2952
|
+
testId: `${testIdPrefix}-option-${t.dbId}`
|
|
2953
|
+
},
|
|
2954
|
+
t.dbId
|
|
2955
|
+
))
|
|
2956
|
+
}
|
|
2957
|
+
)
|
|
2958
|
+
] });
|
|
2959
|
+
};
|
|
2960
|
+
return /* @__PURE__ */ jsx14(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs10(
|
|
2961
|
+
"div",
|
|
2962
|
+
{
|
|
2963
|
+
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
2964
|
+
onClick: (e) => e.stopPropagation(),
|
|
2965
|
+
"data-testid": `${testIdPrefix}-box`,
|
|
2966
|
+
children: [
|
|
2967
|
+
/* @__PURE__ */ jsx14("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
|
|
2968
|
+
/* @__PURE__ */ jsxs10("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
2969
|
+
"Tracks with no counterpart between",
|
|
2970
|
+
" ",
|
|
2971
|
+
/* @__PURE__ */ jsx14("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
2972
|
+
" and",
|
|
2973
|
+
" ",
|
|
2974
|
+
/* @__PURE__ */ jsx14("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
2975
|
+
" can gracefully fade out (leaving) or fade in (entering) across this transition."
|
|
2976
|
+
] }),
|
|
2977
|
+
load.status === "loading" && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
2978
|
+
load.status === "error" && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
2979
|
+
load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-muted py-4 text-center", "data-testid": `${testIdPrefix}-empty`, children: "Every track has a counterpart in the other scene \u2014 nothing to fade. Use \u201C+ Crossfade\u201D to bridge matching tracks." }) : /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
2980
|
+
renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
|
|
2981
|
+
renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
|
|
2982
|
+
] })),
|
|
2983
|
+
error && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
2984
|
+
/* @__PURE__ */ jsxs10("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
2985
|
+
/* @__PURE__ */ jsx14(
|
|
2986
|
+
"button",
|
|
2987
|
+
{
|
|
2988
|
+
ref: cancelRef,
|
|
2989
|
+
"data-testid": `${testIdPrefix}-cancel`,
|
|
2990
|
+
onClick: onClose,
|
|
2991
|
+
disabled: isCreating,
|
|
2992
|
+
className: "px-3 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-text disabled:opacity-50",
|
|
2993
|
+
children: "Cancel"
|
|
2994
|
+
}
|
|
2995
|
+
),
|
|
2996
|
+
/* @__PURE__ */ jsx14(
|
|
2997
|
+
"button",
|
|
2998
|
+
{
|
|
2999
|
+
"data-testid": `${testIdPrefix}-confirm`,
|
|
3000
|
+
onClick: handleCreate,
|
|
3001
|
+
disabled: !canCreate,
|
|
3002
|
+
className: `px-3 py-1 text-xs rounded-sm border transition-colors ${canCreate ? "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"}`,
|
|
3003
|
+
children: isCreating ? "Generating fade\u2026" : "Create fade"
|
|
3004
|
+
}
|
|
3005
|
+
)
|
|
3006
|
+
] })
|
|
3007
|
+
]
|
|
3008
|
+
}
|
|
3009
|
+
) });
|
|
3010
|
+
}
|
|
3011
|
+
|
|
2571
3012
|
// src/components/ImportTrackModal.tsx
|
|
2572
|
-
import { useCallback as
|
|
2573
|
-
import { jsx as
|
|
3013
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useState as useState8 } from "react";
|
|
3014
|
+
import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2574
3015
|
function ImportTrackModal({
|
|
2575
3016
|
host,
|
|
2576
3017
|
open,
|
|
@@ -2582,10 +3023,10 @@ function ImportTrackModal({
|
|
|
2582
3023
|
onPick,
|
|
2583
3024
|
onPortTrack
|
|
2584
3025
|
}) {
|
|
2585
|
-
const [load, setLoad] =
|
|
2586
|
-
const [selectedSceneId, setSelectedSceneId] =
|
|
2587
|
-
const [importingTrackId, setImportingTrackId] =
|
|
2588
|
-
const refresh =
|
|
3026
|
+
const [load, setLoad] = useState8({ status: "loading" });
|
|
3027
|
+
const [selectedSceneId, setSelectedSceneId] = useState8(null);
|
|
3028
|
+
const [importingTrackId, setImportingTrackId] = useState8(null);
|
|
3029
|
+
const refresh = useCallback5(async () => {
|
|
2589
3030
|
if (!host.listImportableTracks) {
|
|
2590
3031
|
setLoad({ status: "error", message: "This host does not support importing tracks." });
|
|
2591
3032
|
return;
|
|
@@ -2601,14 +3042,14 @@ function ImportTrackModal({
|
|
|
2601
3042
|
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
|
|
2602
3043
|
}
|
|
2603
3044
|
}, [host, mode, onPortTrack]);
|
|
2604
|
-
|
|
3045
|
+
useEffect7(() => {
|
|
2605
3046
|
if (open) {
|
|
2606
3047
|
setSelectedSceneId(null);
|
|
2607
3048
|
setImportingTrackId(null);
|
|
2608
3049
|
void refresh();
|
|
2609
3050
|
}
|
|
2610
3051
|
}, [open, refresh]);
|
|
2611
|
-
const handleImport =
|
|
3052
|
+
const handleImport = useCallback5(
|
|
2612
3053
|
async (track, sourceSceneId, sceneName, isSameScene) => {
|
|
2613
3054
|
if (isSameScene && onPortTrack) {
|
|
2614
3055
|
if (!track.importable) return;
|
|
@@ -2649,16 +3090,16 @@ function ImportTrackModal({
|
|
|
2649
3090
|
if (!open) return null;
|
|
2650
3091
|
const scenes = load.status === "ready" ? load.scenes : [];
|
|
2651
3092
|
const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
|
|
2652
|
-
return /* @__PURE__ */
|
|
3093
|
+
return /* @__PURE__ */ jsx15(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs11(
|
|
2653
3094
|
"div",
|
|
2654
3095
|
{
|
|
2655
3096
|
className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
|
|
2656
3097
|
onClick: (e) => e.stopPropagation(),
|
|
2657
3098
|
"data-testid": `${testIdPrefix}-modal`,
|
|
2658
3099
|
children: [
|
|
2659
|
-
/* @__PURE__ */
|
|
2660
|
-
/* @__PURE__ */
|
|
2661
|
-
selectedScene && /* @__PURE__ */
|
|
3100
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
|
|
3101
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
|
|
3102
|
+
selectedScene && /* @__PURE__ */ jsx15(
|
|
2662
3103
|
"button",
|
|
2663
3104
|
{
|
|
2664
3105
|
className: "text-sas-muted hover:text-sas-accent text-xs",
|
|
@@ -2667,9 +3108,9 @@ function ImportTrackModal({
|
|
|
2667
3108
|
children: "\u2190"
|
|
2668
3109
|
}
|
|
2669
3110
|
),
|
|
2670
|
-
/* @__PURE__ */
|
|
3111
|
+
/* @__PURE__ */ jsx15("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
|
|
2671
3112
|
] }),
|
|
2672
|
-
/* @__PURE__ */
|
|
3113
|
+
/* @__PURE__ */ jsx15(
|
|
2673
3114
|
"button",
|
|
2674
3115
|
{
|
|
2675
3116
|
className: "text-sas-muted hover:text-sas-accent text-sm",
|
|
@@ -2679,30 +3120,30 @@ function ImportTrackModal({
|
|
|
2679
3120
|
}
|
|
2680
3121
|
)
|
|
2681
3122
|
] }),
|
|
2682
|
-
/* @__PURE__ */
|
|
2683
|
-
load.status === "loading" && /* @__PURE__ */
|
|
2684
|
-
load.status === "error" && /* @__PURE__ */
|
|
2685
|
-
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */
|
|
2686
|
-
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */
|
|
3123
|
+
/* @__PURE__ */ jsxs11("div", { className: "overflow-y-auto p-2 flex-1", children: [
|
|
3124
|
+
load.status === "loading" && /* @__PURE__ */ jsx15("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
|
|
3125
|
+
load.status === "error" && /* @__PURE__ */ jsx15("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
3126
|
+
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx15("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
|
|
3127
|
+
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx15("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsxs11(
|
|
2687
3128
|
"button",
|
|
2688
3129
|
{
|
|
2689
3130
|
className: "w-full flex items-center justify-between px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt text-left text-xs text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors",
|
|
2690
3131
|
onClick: () => setSelectedSceneId(scene.sceneId),
|
|
2691
3132
|
"data-testid": `${testIdPrefix}-scene`,
|
|
2692
3133
|
children: [
|
|
2693
|
-
/* @__PURE__ */
|
|
2694
|
-
/* @__PURE__ */
|
|
3134
|
+
/* @__PURE__ */ jsx15("span", { className: "truncate", children: scene.sceneName }),
|
|
3135
|
+
/* @__PURE__ */ jsxs11("span", { className: "text-sas-muted", children: [
|
|
2695
3136
|
scene.tracks.length,
|
|
2696
3137
|
" \u2192"
|
|
2697
3138
|
] })
|
|
2698
3139
|
]
|
|
2699
3140
|
}
|
|
2700
3141
|
) }, scene.sceneId)) }),
|
|
2701
|
-
selectedScene && /* @__PURE__ */
|
|
3142
|
+
selectedScene && /* @__PURE__ */ jsx15("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
|
|
2702
3143
|
const busy = importingTrackId === track.trackId;
|
|
2703
3144
|
const gated = mode === "track" && !track.importable;
|
|
2704
3145
|
const disabled = gated || busy;
|
|
2705
|
-
return /* @__PURE__ */
|
|
3146
|
+
return /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsxs11(
|
|
2706
3147
|
"button",
|
|
2707
3148
|
{
|
|
2708
3149
|
className: `w-full flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${disabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-text hover:border-sas-accent hover:text-sas-accent"}`,
|
|
@@ -2712,14 +3153,14 @@ function ImportTrackModal({
|
|
|
2712
3153
|
"data-testid": `${testIdPrefix}-track`,
|
|
2713
3154
|
"data-importable": mode === "sound" || track.importable ? "true" : "false",
|
|
2714
3155
|
children: [
|
|
2715
|
-
/* @__PURE__ */
|
|
3156
|
+
/* @__PURE__ */ jsxs11("span", { className: "truncate", children: [
|
|
2716
3157
|
track.name,
|
|
2717
|
-
track.role ? /* @__PURE__ */
|
|
3158
|
+
track.role ? /* @__PURE__ */ jsxs11("span", { className: "text-sas-muted", children: [
|
|
2718
3159
|
" \xB7 ",
|
|
2719
3160
|
track.role
|
|
2720
3161
|
] }) : null
|
|
2721
3162
|
] }),
|
|
2722
|
-
busy ? /* @__PURE__ */
|
|
3163
|
+
busy ? /* @__PURE__ */ jsx15("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx15("span", { className: "text-sas-muted", children: "\u2298" }) : null
|
|
2723
3164
|
]
|
|
2724
3165
|
}
|
|
2725
3166
|
) }, track.dbId);
|
|
@@ -2731,8 +3172,38 @@ function ImportTrackModal({
|
|
|
2731
3172
|
}
|
|
2732
3173
|
|
|
2733
3174
|
// src/components/CrossfadeModal.tsx
|
|
2734
|
-
import { useCallback as
|
|
2735
|
-
import { Fragment as
|
|
3175
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo4, useRef as useRef8, useState as useState9 } from "react";
|
|
3176
|
+
import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3177
|
+
function shortId2(dbId) {
|
|
3178
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3179
|
+
}
|
|
3180
|
+
function CandidateRow({
|
|
3181
|
+
track,
|
|
3182
|
+
selected,
|
|
3183
|
+
disabled,
|
|
3184
|
+
onSelect,
|
|
3185
|
+
testId
|
|
3186
|
+
}) {
|
|
3187
|
+
const primary = track.prompt?.trim() || track.name;
|
|
3188
|
+
const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
|
|
3189
|
+
return /* @__PURE__ */ jsxs12(
|
|
3190
|
+
"button",
|
|
3191
|
+
{
|
|
3192
|
+
type: "button",
|
|
3193
|
+
role: "radio",
|
|
3194
|
+
"aria-checked": selected,
|
|
3195
|
+
"data-testid": testId,
|
|
3196
|
+
"data-value": track.dbId,
|
|
3197
|
+
onClick: onSelect,
|
|
3198
|
+
disabled,
|
|
3199
|
+
className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
|
|
3200
|
+
children: [
|
|
3201
|
+
/* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
3202
|
+
meta && /* @__PURE__ */ jsx16("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
3203
|
+
]
|
|
3204
|
+
}
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
2736
3207
|
function CrossfadeModal({
|
|
2737
3208
|
host,
|
|
2738
3209
|
open,
|
|
@@ -2740,34 +3211,40 @@ function CrossfadeModal({
|
|
|
2740
3211
|
toSceneId,
|
|
2741
3212
|
fromSceneName,
|
|
2742
3213
|
toSceneName,
|
|
3214
|
+
excludeSourceDbIds,
|
|
2743
3215
|
onClose,
|
|
2744
3216
|
onCreate,
|
|
2745
3217
|
testIdPrefix = "crossfade-modal"
|
|
2746
3218
|
}) {
|
|
2747
|
-
const [load, setLoad] =
|
|
2748
|
-
const [originDbId, setOriginDbId] =
|
|
2749
|
-
const [targetDbId, setTargetDbId] =
|
|
2750
|
-
const [isCreating, setIsCreating] =
|
|
2751
|
-
const [error, setError] =
|
|
2752
|
-
const
|
|
2753
|
-
const
|
|
3219
|
+
const [load, setLoad] = useState9({ status: "loading" });
|
|
3220
|
+
const [originDbId, setOriginDbId] = useState9("");
|
|
3221
|
+
const [targetDbId, setTargetDbId] = useState9("");
|
|
3222
|
+
const [isCreating, setIsCreating] = useState9(false);
|
|
3223
|
+
const [error, setError] = useState9(null);
|
|
3224
|
+
const [fromName, setFromName] = useState9(null);
|
|
3225
|
+
const [toName, setToName] = useState9(null);
|
|
3226
|
+
const cancelRef = useRef8(null);
|
|
3227
|
+
const refresh = useCallback6(async () => {
|
|
2754
3228
|
if (!host.listSceneFamilyTracks) {
|
|
2755
3229
|
setLoad({ status: "error", message: "This host does not support crossfade tracks." });
|
|
2756
3230
|
return;
|
|
2757
3231
|
}
|
|
2758
3232
|
setLoad({ status: "loading" });
|
|
2759
3233
|
try {
|
|
2760
|
-
const [origin, target] = await Promise.all([
|
|
3234
|
+
const [origin, target, fName, tName] = await Promise.all([
|
|
2761
3235
|
host.listSceneFamilyTracks(fromSceneId),
|
|
2762
|
-
host.listSceneFamilyTracks(toSceneId)
|
|
3236
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
3237
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
3238
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null)
|
|
2763
3239
|
]);
|
|
3240
|
+
setFromName(fName);
|
|
3241
|
+
setToName(tName);
|
|
2764
3242
|
setLoad({ status: "ready", origin, target });
|
|
2765
|
-
setOriginDbId(origin[0]?.dbId ?? "");
|
|
2766
3243
|
} catch (err) {
|
|
2767
3244
|
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
2768
3245
|
}
|
|
2769
3246
|
}, [host, fromSceneId, toSceneId]);
|
|
2770
|
-
|
|
3247
|
+
useEffect8(() => {
|
|
2771
3248
|
if (open) {
|
|
2772
3249
|
setError(null);
|
|
2773
3250
|
setIsCreating(false);
|
|
@@ -2776,27 +3253,32 @@ function CrossfadeModal({
|
|
|
2776
3253
|
void refresh();
|
|
2777
3254
|
}
|
|
2778
3255
|
}, [open, refresh]);
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
3256
|
+
const excludeSet = useMemo4(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3257
|
+
const originCandidates = useMemo4(
|
|
3258
|
+
() => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3259
|
+
[load, excludeSet]
|
|
2782
3260
|
);
|
|
2783
|
-
const
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
3261
|
+
const targetCandidates = useMemo4(
|
|
3262
|
+
() => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3263
|
+
[load, excludeSet]
|
|
3264
|
+
);
|
|
3265
|
+
useEffect8(() => {
|
|
3266
|
+
if (!originCandidates.some((t) => t.dbId === originDbId)) {
|
|
3267
|
+
setOriginDbId(originCandidates[0]?.dbId ?? "");
|
|
3268
|
+
}
|
|
3269
|
+
}, [originCandidates, originDbId]);
|
|
3270
|
+
useEffect8(() => {
|
|
2790
3271
|
if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
|
|
2791
3272
|
setTargetDbId(targetCandidates[0]?.dbId ?? "");
|
|
2792
3273
|
}
|
|
2793
3274
|
}, [targetCandidates, targetDbId]);
|
|
3275
|
+
const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
|
|
2794
3276
|
const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
|
|
2795
3277
|
const canCreate = !isCreating && !!originTrack && !!targetTrack;
|
|
2796
|
-
const handleClose =
|
|
3278
|
+
const handleClose = useCallback6(() => {
|
|
2797
3279
|
if (!isCreating) onClose();
|
|
2798
3280
|
}, [isCreating, onClose]);
|
|
2799
|
-
const handleCreate =
|
|
3281
|
+
const handleCreate = useCallback6(async () => {
|
|
2800
3282
|
if (!originTrack || !targetTrack) return;
|
|
2801
3283
|
setIsCreating(true);
|
|
2802
3284
|
setError(null);
|
|
@@ -2811,79 +3293,100 @@ function CrossfadeModal({
|
|
|
2811
3293
|
setIsCreating(false);
|
|
2812
3294
|
}
|
|
2813
3295
|
}, [originTrack, targetTrack, onCreate, onClose]);
|
|
3296
|
+
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
3297
|
+
const toLabel = toName ?? toSceneName ?? null;
|
|
2814
3298
|
if (!open) return null;
|
|
2815
|
-
return /* @__PURE__ */
|
|
3299
|
+
return /* @__PURE__ */ jsx16(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs12(
|
|
2816
3300
|
"div",
|
|
2817
3301
|
{
|
|
2818
3302
|
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
2819
3303
|
onClick: (e) => e.stopPropagation(),
|
|
2820
3304
|
"data-testid": `${testIdPrefix}-box`,
|
|
2821
3305
|
children: [
|
|
2822
|
-
/* @__PURE__ */
|
|
2823
|
-
/* @__PURE__ */
|
|
3306
|
+
/* @__PURE__ */ jsx16("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
|
|
3307
|
+
/* @__PURE__ */ jsxs12("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
2824
3308
|
"Bridge a track from",
|
|
2825
3309
|
" ",
|
|
2826
|
-
/* @__PURE__ */
|
|
3310
|
+
/* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
2827
3311
|
" into one from",
|
|
2828
3312
|
" ",
|
|
2829
|
-
/* @__PURE__ */
|
|
3313
|
+
/* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
2830
3314
|
". Both layers share one generated part; each keeps its own preset."
|
|
2831
3315
|
] }),
|
|
2832
|
-
load.status === "loading" && /* @__PURE__ */
|
|
2833
|
-
load.status === "error" && /* @__PURE__ */
|
|
2834
|
-
load.status === "ready" && (
|
|
3316
|
+
load.status === "loading" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
3317
|
+
load.status === "error" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3318
|
+
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs12(
|
|
2835
3319
|
"div",
|
|
2836
3320
|
{
|
|
2837
3321
|
className: "text-xs text-sas-muted py-4 text-center",
|
|
2838
3322
|
"data-testid": `${testIdPrefix}-empty-origin`,
|
|
2839
|
-
children:
|
|
3323
|
+
children: [
|
|
3324
|
+
"No available tracks in ",
|
|
3325
|
+
fromLabel ?? "the origin scene",
|
|
3326
|
+
". Add one (or free one from another crossfade) first."
|
|
3327
|
+
]
|
|
2840
3328
|
}
|
|
2841
|
-
) : /* @__PURE__ */
|
|
2842
|
-
/* @__PURE__ */
|
|
2843
|
-
/* @__PURE__ */
|
|
2844
|
-
|
|
2845
|
-
"
|
|
3329
|
+
) : /* @__PURE__ */ jsxs12(Fragment5, { children: [
|
|
3330
|
+
/* @__PURE__ */ jsxs12("div", { className: "block", children: [
|
|
3331
|
+
/* @__PURE__ */ jsxs12("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
3332
|
+
"Origin ",
|
|
3333
|
+
fromLabel ? `(${fromLabel})` : "(top)"
|
|
3334
|
+
] }),
|
|
3335
|
+
/* @__PURE__ */ jsx16(
|
|
3336
|
+
"div",
|
|
2846
3337
|
{
|
|
2847
|
-
"
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3338
|
+
role: "radiogroup",
|
|
3339
|
+
"aria-label": "Origin track",
|
|
3340
|
+
"data-testid": `${testIdPrefix}-origin-list`,
|
|
3341
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3342
|
+
children: originCandidates.map((t) => /* @__PURE__ */ jsx16(
|
|
3343
|
+
CandidateRow,
|
|
3344
|
+
{
|
|
3345
|
+
track: t,
|
|
3346
|
+
selected: t.dbId === originDbId,
|
|
3347
|
+
disabled: isCreating,
|
|
3348
|
+
onSelect: () => setOriginDbId(t.dbId),
|
|
3349
|
+
testId: `${testIdPrefix}-origin-option-${t.dbId}`
|
|
3350
|
+
},
|
|
3351
|
+
t.dbId
|
|
3352
|
+
))
|
|
2856
3353
|
}
|
|
2857
3354
|
)
|
|
2858
3355
|
] }),
|
|
2859
|
-
/* @__PURE__ */
|
|
2860
|
-
/* @__PURE__ */
|
|
2861
|
-
"Target
|
|
2862
|
-
|
|
3356
|
+
/* @__PURE__ */ jsxs12("div", { className: "block", children: [
|
|
3357
|
+
/* @__PURE__ */ jsxs12("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
3358
|
+
"Target ",
|
|
3359
|
+
toLabel ? `(${toLabel})` : "(bottom)"
|
|
2863
3360
|
] }),
|
|
2864
|
-
targetCandidates.length === 0 ? /* @__PURE__ */
|
|
2865
|
-
"No ",
|
|
2866
|
-
|
|
2867
|
-
"
|
|
2868
|
-
] }) : /* @__PURE__ */
|
|
2869
|
-
"
|
|
3361
|
+
targetCandidates.length === 0 ? /* @__PURE__ */ jsxs12("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
|
|
3362
|
+
"No available tracks in ",
|
|
3363
|
+
toLabel ?? "the target scene",
|
|
3364
|
+
" to crossfade into."
|
|
3365
|
+
] }) : /* @__PURE__ */ jsx16(
|
|
3366
|
+
"div",
|
|
2870
3367
|
{
|
|
2871
|
-
"
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
3368
|
+
role: "radiogroup",
|
|
3369
|
+
"aria-label": "Target track",
|
|
3370
|
+
"data-testid": `${testIdPrefix}-target-list`,
|
|
3371
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3372
|
+
children: targetCandidates.map((t) => /* @__PURE__ */ jsx16(
|
|
3373
|
+
CandidateRow,
|
|
3374
|
+
{
|
|
3375
|
+
track: t,
|
|
3376
|
+
selected: t.dbId === targetDbId,
|
|
3377
|
+
disabled: isCreating,
|
|
3378
|
+
onSelect: () => setTargetDbId(t.dbId),
|
|
3379
|
+
testId: `${testIdPrefix}-target-option-${t.dbId}`
|
|
3380
|
+
},
|
|
3381
|
+
t.dbId
|
|
3382
|
+
))
|
|
2880
3383
|
}
|
|
2881
3384
|
)
|
|
2882
3385
|
] })
|
|
2883
3386
|
] })),
|
|
2884
|
-
error && /* @__PURE__ */
|
|
2885
|
-
/* @__PURE__ */
|
|
2886
|
-
/* @__PURE__ */
|
|
3387
|
+
error && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3388
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3389
|
+
/* @__PURE__ */ jsx16(
|
|
2887
3390
|
"button",
|
|
2888
3391
|
{
|
|
2889
3392
|
ref: cancelRef,
|
|
@@ -2894,7 +3397,7 @@ function CrossfadeModal({
|
|
|
2894
3397
|
children: "Cancel"
|
|
2895
3398
|
}
|
|
2896
3399
|
),
|
|
2897
|
-
/* @__PURE__ */
|
|
3400
|
+
/* @__PURE__ */ jsx16(
|
|
2898
3401
|
"button",
|
|
2899
3402
|
{
|
|
2900
3403
|
"data-testid": `${testIdPrefix}-confirm`,
|
|
@@ -2911,8 +3414,8 @@ function CrossfadeModal({
|
|
|
2911
3414
|
}
|
|
2912
3415
|
|
|
2913
3416
|
// src/components/DownloadPackButton.tsx
|
|
2914
|
-
import { useCallback as
|
|
2915
|
-
import { jsx as
|
|
3417
|
+
import { useCallback as useCallback7, useEffect as useEffect9, useState as useState10 } from "react";
|
|
3418
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2916
3419
|
function formatSize(bytes) {
|
|
2917
3420
|
if (!bytes || bytes <= 0) return "";
|
|
2918
3421
|
const gb = bytes / 1024 ** 3;
|
|
@@ -2928,10 +3431,10 @@ var DownloadPackButton = ({
|
|
|
2928
3431
|
variant = "compact",
|
|
2929
3432
|
onDownloadComplete
|
|
2930
3433
|
}) => {
|
|
2931
|
-
const [status, setStatus] =
|
|
2932
|
-
const [progress, setProgress] =
|
|
2933
|
-
const [errorMessage, setErrorMessage] =
|
|
2934
|
-
|
|
3434
|
+
const [status, setStatus] = useState10("idle");
|
|
3435
|
+
const [progress, setProgress] = useState10(0);
|
|
3436
|
+
const [errorMessage, setErrorMessage] = useState10(null);
|
|
3437
|
+
useEffect9(() => {
|
|
2935
3438
|
const unsub = host.onSamplePackProgress(packId, (p) => {
|
|
2936
3439
|
setStatus(p.status);
|
|
2937
3440
|
setProgress(p.progress);
|
|
@@ -2946,7 +3449,7 @@ var DownloadPackButton = ({
|
|
|
2946
3449
|
});
|
|
2947
3450
|
return unsub;
|
|
2948
3451
|
}, [host, packId, onDownloadComplete]);
|
|
2949
|
-
const handleClick =
|
|
3452
|
+
const handleClick = useCallback7(async () => {
|
|
2950
3453
|
if (status !== "idle" && status !== "error") return;
|
|
2951
3454
|
try {
|
|
2952
3455
|
setStatus("downloading");
|
|
@@ -3000,8 +3503,8 @@ var DownloadPackButton = ({
|
|
|
3000
3503
|
} else {
|
|
3001
3504
|
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
3002
3505
|
}
|
|
3003
|
-
return /* @__PURE__ */
|
|
3004
|
-
/* @__PURE__ */
|
|
3506
|
+
return /* @__PURE__ */ jsxs13("div", { children: [
|
|
3507
|
+
/* @__PURE__ */ jsx17(
|
|
3005
3508
|
"button",
|
|
3006
3509
|
{
|
|
3007
3510
|
"data-testid": `download-pack-button-${packId}`,
|
|
@@ -3012,12 +3515,12 @@ var DownloadPackButton = ({
|
|
|
3012
3515
|
children: buttonLabel
|
|
3013
3516
|
}
|
|
3014
3517
|
),
|
|
3015
|
-
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */
|
|
3518
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
3016
3519
|
] });
|
|
3017
3520
|
};
|
|
3018
3521
|
|
|
3019
3522
|
// src/components/SamplePackCTACard.tsx
|
|
3020
|
-
import { jsx as
|
|
3523
|
+
import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3021
3524
|
var SamplePackCTACard = ({
|
|
3022
3525
|
host,
|
|
3023
3526
|
pack,
|
|
@@ -3025,7 +3528,7 @@ var SamplePackCTACard = ({
|
|
|
3025
3528
|
onDownloadComplete
|
|
3026
3529
|
}) => {
|
|
3027
3530
|
if (status === "checking") {
|
|
3028
|
-
return /* @__PURE__ */
|
|
3531
|
+
return /* @__PURE__ */ jsx18(
|
|
3029
3532
|
"div",
|
|
3030
3533
|
{
|
|
3031
3534
|
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
@@ -3036,16 +3539,16 @@ var SamplePackCTACard = ({
|
|
|
3036
3539
|
}
|
|
3037
3540
|
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
3038
3541
|
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
3039
|
-
return /* @__PURE__ */
|
|
3542
|
+
return /* @__PURE__ */ jsxs14(
|
|
3040
3543
|
"div",
|
|
3041
3544
|
{
|
|
3042
3545
|
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
3043
3546
|
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
3044
3547
|
children: [
|
|
3045
|
-
/* @__PURE__ */
|
|
3046
|
-
/* @__PURE__ */
|
|
3047
|
-
/* @__PURE__ */
|
|
3048
|
-
/* @__PURE__ */
|
|
3548
|
+
/* @__PURE__ */ jsx18("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
3549
|
+
/* @__PURE__ */ jsx18("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
3550
|
+
/* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
3551
|
+
/* @__PURE__ */ jsx18(
|
|
3049
3552
|
DownloadPackButton,
|
|
3050
3553
|
{
|
|
3051
3554
|
host,
|
|
@@ -3062,7 +3565,7 @@ var SamplePackCTACard = ({
|
|
|
3062
3565
|
};
|
|
3063
3566
|
|
|
3064
3567
|
// src/components/WaveformView.tsx
|
|
3065
|
-
import { useEffect as
|
|
3568
|
+
import { useEffect as useEffect10, useRef as useRef9, useState as useState11 } from "react";
|
|
3066
3569
|
|
|
3067
3570
|
// src/components/waveform.ts
|
|
3068
3571
|
function computePeaks(audioBuffer, bins, targetSamples) {
|
|
@@ -3125,7 +3628,7 @@ function drawWaveform(canvas, peaks, options = {}) {
|
|
|
3125
3628
|
}
|
|
3126
3629
|
|
|
3127
3630
|
// src/components/WaveformView.tsx
|
|
3128
|
-
import { jsx as
|
|
3631
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
3129
3632
|
var WaveformView = ({
|
|
3130
3633
|
host,
|
|
3131
3634
|
filePath,
|
|
@@ -3134,9 +3637,9 @@ var WaveformView = ({
|
|
|
3134
3637
|
fillStyle,
|
|
3135
3638
|
targetSamples
|
|
3136
3639
|
}) => {
|
|
3137
|
-
const canvasRef =
|
|
3138
|
-
const [peaks, setPeaks] =
|
|
3139
|
-
|
|
3640
|
+
const canvasRef = useRef9(null);
|
|
3641
|
+
const [peaks, setPeaks] = useState11(null);
|
|
3642
|
+
useEffect10(() => {
|
|
3140
3643
|
let cancelled = false;
|
|
3141
3644
|
let audioContext = null;
|
|
3142
3645
|
(async () => {
|
|
@@ -3162,7 +3665,7 @@ var WaveformView = ({
|
|
|
3162
3665
|
cancelled = true;
|
|
3163
3666
|
};
|
|
3164
3667
|
}, [host, filePath, bins, targetSamples]);
|
|
3165
|
-
|
|
3668
|
+
useEffect10(() => {
|
|
3166
3669
|
if (!peaks) return;
|
|
3167
3670
|
const canvas = canvasRef.current;
|
|
3168
3671
|
if (!canvas) return;
|
|
@@ -3173,7 +3676,7 @@ var WaveformView = ({
|
|
|
3173
3676
|
observer.observe(canvas);
|
|
3174
3677
|
return () => observer.disconnect();
|
|
3175
3678
|
}, [peaks, fillStyle]);
|
|
3176
|
-
return /* @__PURE__ */
|
|
3679
|
+
return /* @__PURE__ */ jsx19(
|
|
3177
3680
|
"canvas",
|
|
3178
3681
|
{
|
|
3179
3682
|
ref: canvasRef,
|
|
@@ -3184,8 +3687,8 @@ var WaveformView = ({
|
|
|
3184
3687
|
};
|
|
3185
3688
|
|
|
3186
3689
|
// src/components/ScrollingWaveform.tsx
|
|
3187
|
-
import { useEffect as
|
|
3188
|
-
import { jsx as
|
|
3690
|
+
import { useEffect as useEffect11, useRef as useRef10 } from "react";
|
|
3691
|
+
import { jsx as jsx20 } from "react/jsx-runtime";
|
|
3189
3692
|
var ScrollingWaveform = ({
|
|
3190
3693
|
getPeakDb,
|
|
3191
3694
|
active,
|
|
@@ -3193,11 +3696,11 @@ var ScrollingWaveform = ({
|
|
|
3193
3696
|
className,
|
|
3194
3697
|
fillStyle
|
|
3195
3698
|
}) => {
|
|
3196
|
-
const canvasRef =
|
|
3197
|
-
const ringRef =
|
|
3198
|
-
const writeIdxRef =
|
|
3199
|
-
const rafRef =
|
|
3200
|
-
|
|
3699
|
+
const canvasRef = useRef10(null);
|
|
3700
|
+
const ringRef = useRef10(new Float32Array(columns));
|
|
3701
|
+
const writeIdxRef = useRef10(0);
|
|
3702
|
+
const rafRef = useRef10(null);
|
|
3703
|
+
useEffect11(() => {
|
|
3201
3704
|
if (ringRef.current.length !== columns) {
|
|
3202
3705
|
const next = new Float32Array(columns);
|
|
3203
3706
|
const prev = ringRef.current;
|
|
@@ -3209,7 +3712,7 @@ var ScrollingWaveform = ({
|
|
|
3209
3712
|
writeIdxRef.current = writeIdxRef.current % columns;
|
|
3210
3713
|
}
|
|
3211
3714
|
}, [columns]);
|
|
3212
|
-
|
|
3715
|
+
useEffect11(() => {
|
|
3213
3716
|
if (!active) {
|
|
3214
3717
|
if (rafRef.current !== null) {
|
|
3215
3718
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -3261,7 +3764,7 @@ var ScrollingWaveform = ({
|
|
|
3261
3764
|
}
|
|
3262
3765
|
};
|
|
3263
3766
|
}, [active, getPeakDb, fillStyle]);
|
|
3264
|
-
return /* @__PURE__ */
|
|
3767
|
+
return /* @__PURE__ */ jsx20(
|
|
3265
3768
|
"canvas",
|
|
3266
3769
|
{
|
|
3267
3770
|
ref: canvasRef,
|
|
@@ -3272,8 +3775,8 @@ var ScrollingWaveform = ({
|
|
|
3272
3775
|
};
|
|
3273
3776
|
|
|
3274
3777
|
// src/components/OffsetScrubber.tsx
|
|
3275
|
-
import { useCallback as
|
|
3276
|
-
import { jsx as
|
|
3778
|
+
import { useCallback as useCallback8, useEffect as useEffect12, useMemo as useMemo5, useRef as useRef11, useState as useState12 } from "react";
|
|
3779
|
+
import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3277
3780
|
var SLIDER_HEIGHT_PX = 28;
|
|
3278
3781
|
var TICK_HEIGHT_PX = 14;
|
|
3279
3782
|
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
@@ -3286,40 +3789,40 @@ function OffsetScrubber({
|
|
|
3286
3789
|
onChange,
|
|
3287
3790
|
disabled = false
|
|
3288
3791
|
}) {
|
|
3289
|
-
const trackRef =
|
|
3290
|
-
const [draftOffset, setDraftOffset] =
|
|
3291
|
-
const [isDragging, setIsDragging] =
|
|
3292
|
-
|
|
3792
|
+
const trackRef = useRef11(null);
|
|
3793
|
+
const [draftOffset, setDraftOffset] = useState12(offsetSamples);
|
|
3794
|
+
const [isDragging, setIsDragging] = useState12(false);
|
|
3795
|
+
useEffect12(() => {
|
|
3293
3796
|
if (!isDragging) setDraftOffset(offsetSamples);
|
|
3294
3797
|
}, [offsetSamples, isDragging]);
|
|
3295
3798
|
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
3296
3799
|
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
3297
|
-
const beatsForRange =
|
|
3800
|
+
const beatsForRange = useMemo5(() => {
|
|
3298
3801
|
return Math.round(60 / projectBpm * sampleRate);
|
|
3299
3802
|
}, [projectBpm, sampleRate]);
|
|
3300
3803
|
const rangeSamples = beatsForRange * meter;
|
|
3301
|
-
const sampleToFraction =
|
|
3804
|
+
const sampleToFraction = useCallback8(
|
|
3302
3805
|
(sample) => {
|
|
3303
3806
|
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
3304
3807
|
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
3305
3808
|
},
|
|
3306
3809
|
[rangeSamples]
|
|
3307
3810
|
);
|
|
3308
|
-
const fractionToSample =
|
|
3811
|
+
const fractionToSample = useCallback8(
|
|
3309
3812
|
(fraction) => {
|
|
3310
3813
|
const clamped = Math.max(0, Math.min(1, fraction));
|
|
3311
3814
|
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
3312
3815
|
},
|
|
3313
3816
|
[rangeSamples]
|
|
3314
3817
|
);
|
|
3315
|
-
const snapTargets =
|
|
3818
|
+
const snapTargets = useMemo5(() => {
|
|
3316
3819
|
if (!cuePoints || cuePoints.beats.length === 0) return [];
|
|
3317
3820
|
const downbeat = cuePoints.beats[0];
|
|
3318
3821
|
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
3319
3822
|
const negatives = positives.slice(1).map((p) => -p);
|
|
3320
3823
|
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
3321
3824
|
}, [cuePoints]);
|
|
3322
|
-
const snapToBeat =
|
|
3825
|
+
const snapToBeat = useCallback8(
|
|
3323
3826
|
(sample) => {
|
|
3324
3827
|
if (snapTargets.length === 0) return sample;
|
|
3325
3828
|
let best = snapTargets[0];
|
|
@@ -3335,7 +3838,7 @@ function OffsetScrubber({
|
|
|
3335
3838
|
},
|
|
3336
3839
|
[snapTargets]
|
|
3337
3840
|
);
|
|
3338
|
-
const handlePointerDown =
|
|
3841
|
+
const handlePointerDown = useCallback8(
|
|
3339
3842
|
(e) => {
|
|
3340
3843
|
if (disabled || !cuePoints) return;
|
|
3341
3844
|
e.preventDefault();
|
|
@@ -3369,7 +3872,7 @@ function OffsetScrubber({
|
|
|
3369
3872
|
},
|
|
3370
3873
|
[disabled, cuePoints, fractionToSample, onChange, snapToBeat]
|
|
3371
3874
|
);
|
|
3372
|
-
const handleResetToZero =
|
|
3875
|
+
const handleResetToZero = useCallback8(() => {
|
|
3373
3876
|
if (disabled) return;
|
|
3374
3877
|
setDraftOffset(0);
|
|
3375
3878
|
onChange(0);
|
|
@@ -3377,7 +3880,7 @@ function OffsetScrubber({
|
|
|
3377
3880
|
const thumbFraction = sampleToFraction(draftOffset);
|
|
3378
3881
|
const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
|
|
3379
3882
|
const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
|
|
3380
|
-
const ticks =
|
|
3883
|
+
const ticks = useMemo5(() => {
|
|
3381
3884
|
if (!cuePoints) return [];
|
|
3382
3885
|
const downbeat = cuePoints.beats[0] ?? 0;
|
|
3383
3886
|
return cuePoints.beats.map((b, i) => {
|
|
@@ -3388,9 +3891,9 @@ function OffsetScrubber({
|
|
|
3388
3891
|
});
|
|
3389
3892
|
}, [cuePoints, sampleToFraction]);
|
|
3390
3893
|
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
3391
|
-
return /* @__PURE__ */
|
|
3392
|
-
/* @__PURE__ */
|
|
3393
|
-
/* @__PURE__ */
|
|
3894
|
+
return /* @__PURE__ */ jsxs15("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
3895
|
+
/* @__PURE__ */ jsx21("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
3896
|
+
/* @__PURE__ */ jsxs15(
|
|
3394
3897
|
"div",
|
|
3395
3898
|
{
|
|
3396
3899
|
ref: trackRef,
|
|
@@ -3406,7 +3909,7 @@ function OffsetScrubber({
|
|
|
3406
3909
|
"aria-valuenow": draftOffset,
|
|
3407
3910
|
"aria-disabled": isDisabled,
|
|
3408
3911
|
children: [
|
|
3409
|
-
/* @__PURE__ */
|
|
3912
|
+
/* @__PURE__ */ jsx21(
|
|
3410
3913
|
"div",
|
|
3411
3914
|
{
|
|
3412
3915
|
"aria-hidden": "true",
|
|
@@ -3414,7 +3917,7 @@ function OffsetScrubber({
|
|
|
3414
3917
|
style: { left: "50%" }
|
|
3415
3918
|
}
|
|
3416
3919
|
),
|
|
3417
|
-
ticks.map((t) => /* @__PURE__ */
|
|
3920
|
+
ticks.map((t) => /* @__PURE__ */ jsx21(
|
|
3418
3921
|
"div",
|
|
3419
3922
|
{
|
|
3420
3923
|
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
@@ -3429,7 +3932,7 @@ function OffsetScrubber({
|
|
|
3429
3932
|
},
|
|
3430
3933
|
t.i
|
|
3431
3934
|
)),
|
|
3432
|
-
/* @__PURE__ */
|
|
3935
|
+
/* @__PURE__ */ jsx21(
|
|
3433
3936
|
"div",
|
|
3434
3937
|
{
|
|
3435
3938
|
"data-testid": "offset-scrubber-thumb",
|
|
@@ -3446,7 +3949,7 @@ function OffsetScrubber({
|
|
|
3446
3949
|
]
|
|
3447
3950
|
}
|
|
3448
3951
|
),
|
|
3449
|
-
/* @__PURE__ */
|
|
3952
|
+
/* @__PURE__ */ jsx21(
|
|
3450
3953
|
"span",
|
|
3451
3954
|
{
|
|
3452
3955
|
"data-testid": "offset-scrubber-readout",
|
|
@@ -3454,7 +3957,7 @@ function OffsetScrubber({
|
|
|
3454
3957
|
children: formatOffset(draftOffset, sampleRate)
|
|
3455
3958
|
}
|
|
3456
3959
|
),
|
|
3457
|
-
/* @__PURE__ */
|
|
3960
|
+
/* @__PURE__ */ jsx21(
|
|
3458
3961
|
"button",
|
|
3459
3962
|
{
|
|
3460
3963
|
type: "button",
|
|
@@ -3466,7 +3969,7 @@ function OffsetScrubber({
|
|
|
3466
3969
|
children: "\u2316"
|
|
3467
3970
|
}
|
|
3468
3971
|
),
|
|
3469
|
-
bpmMismatch && /* @__PURE__ */
|
|
3972
|
+
bpmMismatch && /* @__PURE__ */ jsx21(
|
|
3470
3973
|
"span",
|
|
3471
3974
|
{
|
|
3472
3975
|
"data-testid": "offset-bpm-mismatch",
|
|
@@ -3538,13 +4041,13 @@ function synthesizeCuePoints({
|
|
|
3538
4041
|
}
|
|
3539
4042
|
|
|
3540
4043
|
// src/hooks/useSceneState.ts
|
|
3541
|
-
import { useState as
|
|
4044
|
+
import { useState as useState13, useCallback as useCallback9, useRef as useRef12 } from "react";
|
|
3542
4045
|
function useSceneState(activeSceneId, initialValue) {
|
|
3543
|
-
const [stateMap, setStateMap] =
|
|
3544
|
-
const activeSceneIdRef =
|
|
4046
|
+
const [stateMap, setStateMap] = useState13(() => /* @__PURE__ */ new Map());
|
|
4047
|
+
const activeSceneIdRef = useRef12(activeSceneId);
|
|
3545
4048
|
activeSceneIdRef.current = activeSceneId;
|
|
3546
4049
|
const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
|
|
3547
|
-
const setForCurrentScene =
|
|
4050
|
+
const setForCurrentScene = useCallback9((value) => {
|
|
3548
4051
|
const sid = activeSceneIdRef.current;
|
|
3549
4052
|
if (sid === null) return;
|
|
3550
4053
|
setStateMap((prev) => {
|
|
@@ -3555,7 +4058,7 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
3555
4058
|
return newMap;
|
|
3556
4059
|
});
|
|
3557
4060
|
}, [initialValue]);
|
|
3558
|
-
const setForScene =
|
|
4061
|
+
const setForScene = useCallback9((sceneId, value) => {
|
|
3559
4062
|
setStateMap((prev) => {
|
|
3560
4063
|
const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
|
|
3561
4064
|
const next = typeof value === "function" ? value(current) : value;
|
|
@@ -3568,10 +4071,10 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
3568
4071
|
}
|
|
3569
4072
|
|
|
3570
4073
|
// src/hooks/useAnySolo.ts
|
|
3571
|
-
import { useEffect as
|
|
4074
|
+
import { useEffect as useEffect13, useState as useState14 } from "react";
|
|
3572
4075
|
function useAnySolo(host) {
|
|
3573
|
-
const [anySolo, setAnySolo] =
|
|
3574
|
-
|
|
4076
|
+
const [anySolo, setAnySolo] = useState14(false);
|
|
4077
|
+
useEffect13(() => {
|
|
3575
4078
|
let active = true;
|
|
3576
4079
|
const refresh = () => {
|
|
3577
4080
|
host.isAnySoloActive().then((v) => {
|
|
@@ -3590,7 +4093,7 @@ function useAnySolo(host) {
|
|
|
3590
4093
|
}
|
|
3591
4094
|
|
|
3592
4095
|
// src/hooks/useSoundHistory.ts
|
|
3593
|
-
import { useCallback as
|
|
4096
|
+
import { useCallback as useCallback10, useMemo as useMemo6, useRef as useRef13, useState as useState15 } from "react";
|
|
3594
4097
|
var EMPTY = { entries: [], cursor: -1 };
|
|
3595
4098
|
function sameDescriptor(a, b) {
|
|
3596
4099
|
if (a === b) return true;
|
|
@@ -3602,14 +4105,14 @@ function sameDescriptor(a, b) {
|
|
|
3602
4105
|
}
|
|
3603
4106
|
function useSoundHistory(applySound, opts = {}) {
|
|
3604
4107
|
const max = Math.max(2, opts.max ?? 24);
|
|
3605
|
-
const applyRef =
|
|
4108
|
+
const applyRef = useRef13(applySound);
|
|
3606
4109
|
applyRef.current = applySound;
|
|
3607
|
-
const onChangeRef =
|
|
4110
|
+
const onChangeRef = useRef13(opts.onChange);
|
|
3608
4111
|
onChangeRef.current = opts.onChange;
|
|
3609
|
-
const dataRef =
|
|
3610
|
-
const [, setVersion] =
|
|
3611
|
-
const bump =
|
|
3612
|
-
const commit =
|
|
4112
|
+
const dataRef = useRef13({});
|
|
4113
|
+
const [, setVersion] = useState15(0);
|
|
4114
|
+
const bump = useCallback10(() => setVersion((v) => v + 1), []);
|
|
4115
|
+
const commit = useCallback10(
|
|
3613
4116
|
(trackId, next, notify) => {
|
|
3614
4117
|
dataRef.current = { ...dataRef.current, [trackId]: next };
|
|
3615
4118
|
bump();
|
|
@@ -3617,7 +4120,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3617
4120
|
},
|
|
3618
4121
|
[bump]
|
|
3619
4122
|
);
|
|
3620
|
-
const record =
|
|
4123
|
+
const record = useCallback10(
|
|
3621
4124
|
(trackId, descriptor, label) => {
|
|
3622
4125
|
const h = dataRef.current[trackId];
|
|
3623
4126
|
const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
|
|
@@ -3632,7 +4135,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3632
4135
|
},
|
|
3633
4136
|
[max, commit]
|
|
3634
4137
|
);
|
|
3635
|
-
const restoreTo =
|
|
4138
|
+
const restoreTo = useCallback10(
|
|
3636
4139
|
async (trackId, index) => {
|
|
3637
4140
|
const h = dataRef.current[trackId];
|
|
3638
4141
|
if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
|
|
@@ -3642,7 +4145,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3642
4145
|
},
|
|
3643
4146
|
[commit]
|
|
3644
4147
|
);
|
|
3645
|
-
const undo =
|
|
4148
|
+
const undo = useCallback10(
|
|
3646
4149
|
(trackId) => {
|
|
3647
4150
|
const h = dataRef.current[trackId];
|
|
3648
4151
|
if (!h || h.cursor <= 0) return Promise.resolve(false);
|
|
@@ -3650,7 +4153,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3650
4153
|
},
|
|
3651
4154
|
[restoreTo]
|
|
3652
4155
|
);
|
|
3653
|
-
const toggleFavorite =
|
|
4156
|
+
const toggleFavorite = useCallback10(
|
|
3654
4157
|
(trackId, index) => {
|
|
3655
4158
|
const h = dataRef.current[trackId];
|
|
3656
4159
|
if (!h || index < 0 || index >= h.entries.length) return;
|
|
@@ -3659,7 +4162,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3659
4162
|
},
|
|
3660
4163
|
[commit]
|
|
3661
4164
|
);
|
|
3662
|
-
const restore =
|
|
4165
|
+
const restore = useCallback10(
|
|
3663
4166
|
(trackId, state) => {
|
|
3664
4167
|
const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
|
|
3665
4168
|
const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
|
|
@@ -3668,15 +4171,15 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3668
4171
|
},
|
|
3669
4172
|
[commit]
|
|
3670
4173
|
);
|
|
3671
|
-
const list =
|
|
4174
|
+
const list = useCallback10(
|
|
3672
4175
|
(trackId) => dataRef.current[trackId] ?? EMPTY,
|
|
3673
4176
|
[]
|
|
3674
4177
|
);
|
|
3675
|
-
const canUndo =
|
|
4178
|
+
const canUndo = useCallback10((trackId) => {
|
|
3676
4179
|
const h = dataRef.current[trackId];
|
|
3677
4180
|
return !!h && h.cursor > 0;
|
|
3678
4181
|
}, []);
|
|
3679
|
-
const clear =
|
|
4182
|
+
const clear = useCallback10(
|
|
3680
4183
|
(trackId) => {
|
|
3681
4184
|
if (dataRef.current[trackId]) {
|
|
3682
4185
|
const next = { ...dataRef.current };
|
|
@@ -3688,18 +4191,18 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3688
4191
|
},
|
|
3689
4192
|
[bump]
|
|
3690
4193
|
);
|
|
3691
|
-
const reset =
|
|
4194
|
+
const reset = useCallback10(() => {
|
|
3692
4195
|
dataRef.current = {};
|
|
3693
4196
|
bump();
|
|
3694
4197
|
}, [bump]);
|
|
3695
|
-
return
|
|
4198
|
+
return useMemo6(
|
|
3696
4199
|
() => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
|
|
3697
4200
|
[record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
|
|
3698
4201
|
);
|
|
3699
4202
|
}
|
|
3700
4203
|
|
|
3701
4204
|
// src/hooks/useTrackReorder.ts
|
|
3702
|
-
import { useCallback as
|
|
4205
|
+
import { useCallback as useCallback11, useRef as useRef14, useState as useState16 } from "react";
|
|
3703
4206
|
function moveItem(arr, from, to) {
|
|
3704
4207
|
const next = arr.slice();
|
|
3705
4208
|
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
@@ -3716,12 +4219,12 @@ function useTrackReorder({
|
|
|
3716
4219
|
getId,
|
|
3717
4220
|
onError
|
|
3718
4221
|
}) {
|
|
3719
|
-
const [draggingIndex, setDraggingIndex] =
|
|
3720
|
-
const [dragOverIndex, setDragOverIndex] =
|
|
3721
|
-
const fromRef =
|
|
3722
|
-
const itemsRef =
|
|
4222
|
+
const [draggingIndex, setDraggingIndex] = useState16(null);
|
|
4223
|
+
const [dragOverIndex, setDragOverIndex] = useState16(null);
|
|
4224
|
+
const fromRef = useRef14(null);
|
|
4225
|
+
const itemsRef = useRef14(items);
|
|
3723
4226
|
itemsRef.current = items;
|
|
3724
|
-
const dragPropsFor =
|
|
4227
|
+
const dragPropsFor = useCallback11(
|
|
3725
4228
|
(index) => ({
|
|
3726
4229
|
handleProps: {
|
|
3727
4230
|
draggable: true,
|
|
@@ -3783,7 +4286,7 @@ function useTrackReorder({
|
|
|
3783
4286
|
}
|
|
3784
4287
|
|
|
3785
4288
|
// src/constants/sdk-version.ts
|
|
3786
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
4289
|
+
var PLUGIN_SDK_VERSION = "2.28.0";
|
|
3787
4290
|
|
|
3788
4291
|
// src/utils/format-concurrent-tracks.ts
|
|
3789
4292
|
function formatConcurrentTracks(ctx) {
|
|
@@ -3943,6 +4446,8 @@ export {
|
|
|
3943
4446
|
FX_DISPLAY_LABELS,
|
|
3944
4447
|
FX_ENGINE_PLUGIN_NAMES,
|
|
3945
4448
|
FX_PRESET_CONFIGS,
|
|
4449
|
+
FadeModal,
|
|
4450
|
+
FadeTrackRow,
|
|
3946
4451
|
FxToggleBar,
|
|
3947
4452
|
GUTTER_W,
|
|
3948
4453
|
ImportTrackModal,
|
|
@@ -3961,6 +4466,7 @@ export {
|
|
|
3961
4466
|
SamplePackCTACard,
|
|
3962
4467
|
ScrollingWaveform,
|
|
3963
4468
|
SorceryProgressBar,
|
|
4469
|
+
TEXTURAL_ROLES,
|
|
3964
4470
|
TrackDrawer,
|
|
3965
4471
|
TrackMeterStrip,
|
|
3966
4472
|
TrackRow,
|
|
@@ -3968,17 +4474,21 @@ export {
|
|
|
3968
4474
|
WaveformView,
|
|
3969
4475
|
analyzeWavPeak,
|
|
3970
4476
|
asCrossfadeMeta,
|
|
4477
|
+
asFadeMeta,
|
|
3971
4478
|
buildCrossfadeInpaintPrompt,
|
|
3972
4479
|
buildCrossfadeVolumeCurves,
|
|
4480
|
+
buildFadeVolumeCurve,
|
|
3973
4481
|
calculateTimeBasedTarget,
|
|
3974
4482
|
cellToPx,
|
|
3975
4483
|
centerScrollTop,
|
|
3976
4484
|
computePeaks,
|
|
3977
4485
|
dbToSlider,
|
|
4486
|
+
defaultFadeGesture,
|
|
3978
4487
|
drawWaveform,
|
|
3979
4488
|
formatConcurrentTracks,
|
|
3980
4489
|
moveItem,
|
|
3981
4490
|
parseCrossfadePairs,
|
|
4491
|
+
parseFades,
|
|
3982
4492
|
pickTopKWeighted,
|
|
3983
4493
|
pitchToName,
|
|
3984
4494
|
pxToCell,
|