@signalsandsorcery/plugin-sdk 2.26.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.mjs CHANGED
@@ -2570,9 +2570,448 @@ function buildCrossfadeInpaintPrompt(input) {
2570
2570
  return lines.join("\n");
2571
2571
  }
2572
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
+
2573
3012
  // src/components/ImportTrackModal.tsx
2574
- import { useCallback as useCallback4, useEffect as useEffect6, useState as useState7 } from "react";
2575
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
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";
2576
3015
  function ImportTrackModal({
2577
3016
  host,
2578
3017
  open,
@@ -2584,10 +3023,10 @@ function ImportTrackModal({
2584
3023
  onPick,
2585
3024
  onPortTrack
2586
3025
  }) {
2587
- const [load, setLoad] = useState7({ status: "loading" });
2588
- const [selectedSceneId, setSelectedSceneId] = useState7(null);
2589
- const [importingTrackId, setImportingTrackId] = useState7(null);
2590
- const refresh = useCallback4(async () => {
3026
+ const [load, setLoad] = useState8({ status: "loading" });
3027
+ const [selectedSceneId, setSelectedSceneId] = useState8(null);
3028
+ const [importingTrackId, setImportingTrackId] = useState8(null);
3029
+ const refresh = useCallback5(async () => {
2591
3030
  if (!host.listImportableTracks) {
2592
3031
  setLoad({ status: "error", message: "This host does not support importing tracks." });
2593
3032
  return;
@@ -2603,14 +3042,14 @@ function ImportTrackModal({
2603
3042
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
2604
3043
  }
2605
3044
  }, [host, mode, onPortTrack]);
2606
- useEffect6(() => {
3045
+ useEffect7(() => {
2607
3046
  if (open) {
2608
3047
  setSelectedSceneId(null);
2609
3048
  setImportingTrackId(null);
2610
3049
  void refresh();
2611
3050
  }
2612
3051
  }, [open, refresh]);
2613
- const handleImport = useCallback4(
3052
+ const handleImport = useCallback5(
2614
3053
  async (track, sourceSceneId, sceneName, isSameScene) => {
2615
3054
  if (isSameScene && onPortTrack) {
2616
3055
  if (!track.importable) return;
@@ -2651,16 +3090,16 @@ function ImportTrackModal({
2651
3090
  if (!open) return null;
2652
3091
  const scenes = load.status === "ready" ? load.scenes : [];
2653
3092
  const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
2654
- return /* @__PURE__ */ jsx13(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs9(
3093
+ return /* @__PURE__ */ jsx15(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs11(
2655
3094
  "div",
2656
3095
  {
2657
3096
  className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
2658
3097
  onClick: (e) => e.stopPropagation(),
2659
3098
  "data-testid": `${testIdPrefix}-modal`,
2660
3099
  children: [
2661
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
2662
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
2663
- selectedScene && /* @__PURE__ */ jsx13(
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(
2664
3103
  "button",
2665
3104
  {
2666
3105
  className: "text-sas-muted hover:text-sas-accent text-xs",
@@ -2669,9 +3108,9 @@ function ImportTrackModal({
2669
3108
  children: "\u2190"
2670
3109
  }
2671
3110
  ),
2672
- /* @__PURE__ */ jsx13("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3111
+ /* @__PURE__ */ jsx15("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
2673
3112
  ] }),
2674
- /* @__PURE__ */ jsx13(
3113
+ /* @__PURE__ */ jsx15(
2675
3114
  "button",
2676
3115
  {
2677
3116
  className: "text-sas-muted hover:text-sas-accent text-sm",
@@ -2681,30 +3120,30 @@ function ImportTrackModal({
2681
3120
  }
2682
3121
  )
2683
3122
  ] }),
2684
- /* @__PURE__ */ jsxs9("div", { className: "overflow-y-auto p-2 flex-1", children: [
2685
- load.status === "loading" && /* @__PURE__ */ jsx13("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
2686
- load.status === "error" && /* @__PURE__ */ jsx13("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
2687
- load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx13("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." }),
2688
- load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx13("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx13("li", { children: /* @__PURE__ */ jsxs9(
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(
2689
3128
  "button",
2690
3129
  {
2691
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",
2692
3131
  onClick: () => setSelectedSceneId(scene.sceneId),
2693
3132
  "data-testid": `${testIdPrefix}-scene`,
2694
3133
  children: [
2695
- /* @__PURE__ */ jsx13("span", { className: "truncate", children: scene.sceneName }),
2696
- /* @__PURE__ */ jsxs9("span", { className: "text-sas-muted", children: [
3134
+ /* @__PURE__ */ jsx15("span", { className: "truncate", children: scene.sceneName }),
3135
+ /* @__PURE__ */ jsxs11("span", { className: "text-sas-muted", children: [
2697
3136
  scene.tracks.length,
2698
3137
  " \u2192"
2699
3138
  ] })
2700
3139
  ]
2701
3140
  }
2702
3141
  ) }, scene.sceneId)) }),
2703
- selectedScene && /* @__PURE__ */ jsx13("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3142
+ selectedScene && /* @__PURE__ */ jsx15("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
2704
3143
  const busy = importingTrackId === track.trackId;
2705
3144
  const gated = mode === "track" && !track.importable;
2706
3145
  const disabled = gated || busy;
2707
- return /* @__PURE__ */ jsx13("li", { children: /* @__PURE__ */ jsxs9(
3146
+ return /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsxs11(
2708
3147
  "button",
2709
3148
  {
2710
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"}`,
@@ -2714,14 +3153,14 @@ function ImportTrackModal({
2714
3153
  "data-testid": `${testIdPrefix}-track`,
2715
3154
  "data-importable": mode === "sound" || track.importable ? "true" : "false",
2716
3155
  children: [
2717
- /* @__PURE__ */ jsxs9("span", { className: "truncate", children: [
3156
+ /* @__PURE__ */ jsxs11("span", { className: "truncate", children: [
2718
3157
  track.name,
2719
- track.role ? /* @__PURE__ */ jsxs9("span", { className: "text-sas-muted", children: [
3158
+ track.role ? /* @__PURE__ */ jsxs11("span", { className: "text-sas-muted", children: [
2720
3159
  " \xB7 ",
2721
3160
  track.role
2722
3161
  ] }) : null
2723
3162
  ] }),
2724
- busy ? /* @__PURE__ */ jsx13("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx13("span", { className: "text-sas-muted", children: "\u2298" }) : null
3163
+ busy ? /* @__PURE__ */ jsx15("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx15("span", { className: "text-sas-muted", children: "\u2298" }) : null
2725
3164
  ]
2726
3165
  }
2727
3166
  ) }, track.dbId);
@@ -2733,8 +3172,38 @@ function ImportTrackModal({
2733
3172
  }
2734
3173
 
2735
3174
  // src/components/CrossfadeModal.tsx
2736
- import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo3, useRef as useRef7, useState as useState8 } from "react";
2737
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
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
+ }
2738
3207
  function CrossfadeModal({
2739
3208
  host,
2740
3209
  open,
@@ -2747,15 +3216,15 @@ function CrossfadeModal({
2747
3216
  onCreate,
2748
3217
  testIdPrefix = "crossfade-modal"
2749
3218
  }) {
2750
- const [load, setLoad] = useState8({ status: "loading" });
2751
- const [originDbId, setOriginDbId] = useState8("");
2752
- const [targetDbId, setTargetDbId] = useState8("");
2753
- const [isCreating, setIsCreating] = useState8(false);
2754
- const [error, setError] = useState8(null);
2755
- const [fromName, setFromName] = useState8(null);
2756
- const [toName, setToName] = useState8(null);
2757
- const cancelRef = useRef7(null);
2758
- const refresh = useCallback5(async () => {
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 () => {
2759
3228
  if (!host.listSceneFamilyTracks) {
2760
3229
  setLoad({ status: "error", message: "This host does not support crossfade tracks." });
2761
3230
  return;
@@ -2775,7 +3244,7 @@ function CrossfadeModal({
2775
3244
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
2776
3245
  }
2777
3246
  }, [host, fromSceneId, toSceneId]);
2778
- useEffect7(() => {
3247
+ useEffect8(() => {
2779
3248
  if (open) {
2780
3249
  setError(null);
2781
3250
  setIsCreating(false);
@@ -2784,21 +3253,21 @@ function CrossfadeModal({
2784
3253
  void refresh();
2785
3254
  }
2786
3255
  }, [open, refresh]);
2787
- const excludeSet = useMemo3(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
2788
- const originCandidates = useMemo3(
3256
+ const excludeSet = useMemo4(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3257
+ const originCandidates = useMemo4(
2789
3258
  () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
2790
3259
  [load, excludeSet]
2791
3260
  );
2792
- const targetCandidates = useMemo3(
3261
+ const targetCandidates = useMemo4(
2793
3262
  () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
2794
3263
  [load, excludeSet]
2795
3264
  );
2796
- useEffect7(() => {
3265
+ useEffect8(() => {
2797
3266
  if (!originCandidates.some((t) => t.dbId === originDbId)) {
2798
3267
  setOriginDbId(originCandidates[0]?.dbId ?? "");
2799
3268
  }
2800
3269
  }, [originCandidates, originDbId]);
2801
- useEffect7(() => {
3270
+ useEffect8(() => {
2802
3271
  if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
2803
3272
  setTargetDbId(targetCandidates[0]?.dbId ?? "");
2804
3273
  }
@@ -2806,10 +3275,10 @@ function CrossfadeModal({
2806
3275
  const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
2807
3276
  const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
2808
3277
  const canCreate = !isCreating && !!originTrack && !!targetTrack;
2809
- const handleClose = useCallback5(() => {
3278
+ const handleClose = useCallback6(() => {
2810
3279
  if (!isCreating) onClose();
2811
3280
  }, [isCreating, onClose]);
2812
- const handleCreate = useCallback5(async () => {
3281
+ const handleCreate = useCallback6(async () => {
2813
3282
  if (!originTrack || !targetTrack) return;
2814
3283
  setIsCreating(true);
2815
3284
  setError(null);
@@ -2827,26 +3296,26 @@ function CrossfadeModal({
2827
3296
  const fromLabel = fromName ?? fromSceneName ?? null;
2828
3297
  const toLabel = toName ?? toSceneName ?? null;
2829
3298
  if (!open) return null;
2830
- return /* @__PURE__ */ jsx14(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs10(
3299
+ return /* @__PURE__ */ jsx16(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs12(
2831
3300
  "div",
2832
3301
  {
2833
3302
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
2834
3303
  onClick: (e) => e.stopPropagation(),
2835
3304
  "data-testid": `${testIdPrefix}-box`,
2836
3305
  children: [
2837
- /* @__PURE__ */ jsx14("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
2838
- /* @__PURE__ */ jsxs10("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
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: [
2839
3308
  "Bridge a track from",
2840
3309
  " ",
2841
- /* @__PURE__ */ jsx14("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3310
+ /* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
2842
3311
  " into one from",
2843
3312
  " ",
2844
- /* @__PURE__ */ jsx14("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3313
+ /* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
2845
3314
  ". Both layers share one generated part; each keeps its own preset."
2846
3315
  ] }),
2847
- load.status === "loading" && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
2848
- load.status === "error" && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
2849
- load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs10(
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(
2850
3319
  "div",
2851
3320
  {
2852
3321
  className: "text-xs text-sas-muted py-4 text-center",
@@ -2857,55 +3326,67 @@ function CrossfadeModal({
2857
3326
  ". Add one (or free one from another crossfade) first."
2858
3327
  ]
2859
3328
  }
2860
- ) : /* @__PURE__ */ jsxs10(Fragment3, { children: [
2861
- /* @__PURE__ */ jsxs10("label", { className: "block", children: [
2862
- /* @__PURE__ */ jsxs10("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
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: [
2863
3332
  "Origin ",
2864
3333
  fromLabel ? `(${fromLabel})` : "(top)"
2865
3334
  ] }),
2866
- /* @__PURE__ */ jsx14(
2867
- "select",
3335
+ /* @__PURE__ */ jsx16(
3336
+ "div",
2868
3337
  {
2869
- "data-testid": `${testIdPrefix}-origin-select`,
2870
- value: originDbId,
2871
- onChange: (e) => setOriginDbId(e.target.value),
2872
- disabled: isCreating,
2873
- className: "sas-input w-full mt-0.5 text-xs",
2874
- children: originCandidates.map((t) => /* @__PURE__ */ jsxs10("option", { value: t.dbId, children: [
2875
- t.name,
2876
- t.role ? ` \xB7 ${t.role}` : ""
2877
- ] }, t.dbId))
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
+ ))
2878
3353
  }
2879
3354
  )
2880
3355
  ] }),
2881
- /* @__PURE__ */ jsxs10("label", { className: "block", children: [
2882
- /* @__PURE__ */ jsxs10("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3356
+ /* @__PURE__ */ jsxs12("div", { className: "block", children: [
3357
+ /* @__PURE__ */ jsxs12("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
2883
3358
  "Target ",
2884
3359
  toLabel ? `(${toLabel})` : "(bottom)"
2885
3360
  ] }),
2886
- targetCandidates.length === 0 ? /* @__PURE__ */ jsxs10("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3361
+ targetCandidates.length === 0 ? /* @__PURE__ */ jsxs12("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
2887
3362
  "No available tracks in ",
2888
3363
  toLabel ?? "the target scene",
2889
3364
  " to crossfade into."
2890
- ] }) : /* @__PURE__ */ jsx14(
2891
- "select",
3365
+ ] }) : /* @__PURE__ */ jsx16(
3366
+ "div",
2892
3367
  {
2893
- "data-testid": `${testIdPrefix}-target-select`,
2894
- value: targetDbId,
2895
- onChange: (e) => setTargetDbId(e.target.value),
2896
- disabled: isCreating,
2897
- className: "sas-input w-full mt-0.5 text-xs",
2898
- children: targetCandidates.map((t) => /* @__PURE__ */ jsxs10("option", { value: t.dbId, children: [
2899
- t.name,
2900
- t.role ? ` \xB7 ${t.role}` : ""
2901
- ] }, t.dbId))
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
+ ))
2902
3383
  }
2903
3384
  )
2904
3385
  ] })
2905
3386
  ] })),
2906
- error && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
2907
- /* @__PURE__ */ jsxs10("div", { className: "flex justify-end gap-2 pt-1", children: [
2908
- /* @__PURE__ */ jsx14(
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(
2909
3390
  "button",
2910
3391
  {
2911
3392
  ref: cancelRef,
@@ -2916,7 +3397,7 @@ function CrossfadeModal({
2916
3397
  children: "Cancel"
2917
3398
  }
2918
3399
  ),
2919
- /* @__PURE__ */ jsx14(
3400
+ /* @__PURE__ */ jsx16(
2920
3401
  "button",
2921
3402
  {
2922
3403
  "data-testid": `${testIdPrefix}-confirm`,
@@ -2933,8 +3414,8 @@ function CrossfadeModal({
2933
3414
  }
2934
3415
 
2935
3416
  // src/components/DownloadPackButton.tsx
2936
- import { useCallback as useCallback6, useEffect as useEffect8, useState as useState9 } from "react";
2937
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
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";
2938
3419
  function formatSize(bytes) {
2939
3420
  if (!bytes || bytes <= 0) return "";
2940
3421
  const gb = bytes / 1024 ** 3;
@@ -2950,10 +3431,10 @@ var DownloadPackButton = ({
2950
3431
  variant = "compact",
2951
3432
  onDownloadComplete
2952
3433
  }) => {
2953
- const [status, setStatus] = useState9("idle");
2954
- const [progress, setProgress] = useState9(0);
2955
- const [errorMessage, setErrorMessage] = useState9(null);
2956
- useEffect8(() => {
3434
+ const [status, setStatus] = useState10("idle");
3435
+ const [progress, setProgress] = useState10(0);
3436
+ const [errorMessage, setErrorMessage] = useState10(null);
3437
+ useEffect9(() => {
2957
3438
  const unsub = host.onSamplePackProgress(packId, (p) => {
2958
3439
  setStatus(p.status);
2959
3440
  setProgress(p.progress);
@@ -2968,7 +3449,7 @@ var DownloadPackButton = ({
2968
3449
  });
2969
3450
  return unsub;
2970
3451
  }, [host, packId, onDownloadComplete]);
2971
- const handleClick = useCallback6(async () => {
3452
+ const handleClick = useCallback7(async () => {
2972
3453
  if (status !== "idle" && status !== "error") return;
2973
3454
  try {
2974
3455
  setStatus("downloading");
@@ -3022,8 +3503,8 @@ var DownloadPackButton = ({
3022
3503
  } else {
3023
3504
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
3024
3505
  }
3025
- return /* @__PURE__ */ jsxs11("div", { children: [
3026
- /* @__PURE__ */ jsx15(
3506
+ return /* @__PURE__ */ jsxs13("div", { children: [
3507
+ /* @__PURE__ */ jsx17(
3027
3508
  "button",
3028
3509
  {
3029
3510
  "data-testid": `download-pack-button-${packId}`,
@@ -3034,12 +3515,12 @@ var DownloadPackButton = ({
3034
3515
  children: buttonLabel
3035
3516
  }
3036
3517
  ),
3037
- variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx15("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
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 })
3038
3519
  ] });
3039
3520
  };
3040
3521
 
3041
3522
  // src/components/SamplePackCTACard.tsx
3042
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
3523
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
3043
3524
  var SamplePackCTACard = ({
3044
3525
  host,
3045
3526
  pack,
@@ -3047,7 +3528,7 @@ var SamplePackCTACard = ({
3047
3528
  onDownloadComplete
3048
3529
  }) => {
3049
3530
  if (status === "checking") {
3050
- return /* @__PURE__ */ jsx16(
3531
+ return /* @__PURE__ */ jsx18(
3051
3532
  "div",
3052
3533
  {
3053
3534
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -3058,16 +3539,16 @@ var SamplePackCTACard = ({
3058
3539
  }
3059
3540
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
3060
3541
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
3061
- return /* @__PURE__ */ jsxs12(
3542
+ return /* @__PURE__ */ jsxs14(
3062
3543
  "div",
3063
3544
  {
3064
3545
  "data-testid": `sample-pack-cta-${pack.packId}`,
3065
3546
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
3066
3547
  children: [
3067
- /* @__PURE__ */ jsx16("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
3068
- /* @__PURE__ */ jsx16("div", { className: "text-base text-sas-text mb-1", children: headline }),
3069
- /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
3070
- /* @__PURE__ */ jsx16(
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(
3071
3552
  DownloadPackButton,
3072
3553
  {
3073
3554
  host,
@@ -3084,7 +3565,7 @@ var SamplePackCTACard = ({
3084
3565
  };
3085
3566
 
3086
3567
  // src/components/WaveformView.tsx
3087
- import { useEffect as useEffect9, useRef as useRef8, useState as useState10 } from "react";
3568
+ import { useEffect as useEffect10, useRef as useRef9, useState as useState11 } from "react";
3088
3569
 
3089
3570
  // src/components/waveform.ts
3090
3571
  function computePeaks(audioBuffer, bins, targetSamples) {
@@ -3147,7 +3628,7 @@ function drawWaveform(canvas, peaks, options = {}) {
3147
3628
  }
3148
3629
 
3149
3630
  // src/components/WaveformView.tsx
3150
- import { jsx as jsx17 } from "react/jsx-runtime";
3631
+ import { jsx as jsx19 } from "react/jsx-runtime";
3151
3632
  var WaveformView = ({
3152
3633
  host,
3153
3634
  filePath,
@@ -3156,9 +3637,9 @@ var WaveformView = ({
3156
3637
  fillStyle,
3157
3638
  targetSamples
3158
3639
  }) => {
3159
- const canvasRef = useRef8(null);
3160
- const [peaks, setPeaks] = useState10(null);
3161
- useEffect9(() => {
3640
+ const canvasRef = useRef9(null);
3641
+ const [peaks, setPeaks] = useState11(null);
3642
+ useEffect10(() => {
3162
3643
  let cancelled = false;
3163
3644
  let audioContext = null;
3164
3645
  (async () => {
@@ -3184,7 +3665,7 @@ var WaveformView = ({
3184
3665
  cancelled = true;
3185
3666
  };
3186
3667
  }, [host, filePath, bins, targetSamples]);
3187
- useEffect9(() => {
3668
+ useEffect10(() => {
3188
3669
  if (!peaks) return;
3189
3670
  const canvas = canvasRef.current;
3190
3671
  if (!canvas) return;
@@ -3195,7 +3676,7 @@ var WaveformView = ({
3195
3676
  observer.observe(canvas);
3196
3677
  return () => observer.disconnect();
3197
3678
  }, [peaks, fillStyle]);
3198
- return /* @__PURE__ */ jsx17(
3679
+ return /* @__PURE__ */ jsx19(
3199
3680
  "canvas",
3200
3681
  {
3201
3682
  ref: canvasRef,
@@ -3206,8 +3687,8 @@ var WaveformView = ({
3206
3687
  };
3207
3688
 
3208
3689
  // src/components/ScrollingWaveform.tsx
3209
- import { useEffect as useEffect10, useRef as useRef9 } from "react";
3210
- import { jsx as jsx18 } from "react/jsx-runtime";
3690
+ import { useEffect as useEffect11, useRef as useRef10 } from "react";
3691
+ import { jsx as jsx20 } from "react/jsx-runtime";
3211
3692
  var ScrollingWaveform = ({
3212
3693
  getPeakDb,
3213
3694
  active,
@@ -3215,11 +3696,11 @@ var ScrollingWaveform = ({
3215
3696
  className,
3216
3697
  fillStyle
3217
3698
  }) => {
3218
- const canvasRef = useRef9(null);
3219
- const ringRef = useRef9(new Float32Array(columns));
3220
- const writeIdxRef = useRef9(0);
3221
- const rafRef = useRef9(null);
3222
- useEffect10(() => {
3699
+ const canvasRef = useRef10(null);
3700
+ const ringRef = useRef10(new Float32Array(columns));
3701
+ const writeIdxRef = useRef10(0);
3702
+ const rafRef = useRef10(null);
3703
+ useEffect11(() => {
3223
3704
  if (ringRef.current.length !== columns) {
3224
3705
  const next = new Float32Array(columns);
3225
3706
  const prev = ringRef.current;
@@ -3231,7 +3712,7 @@ var ScrollingWaveform = ({
3231
3712
  writeIdxRef.current = writeIdxRef.current % columns;
3232
3713
  }
3233
3714
  }, [columns]);
3234
- useEffect10(() => {
3715
+ useEffect11(() => {
3235
3716
  if (!active) {
3236
3717
  if (rafRef.current !== null) {
3237
3718
  cancelAnimationFrame(rafRef.current);
@@ -3283,7 +3764,7 @@ var ScrollingWaveform = ({
3283
3764
  }
3284
3765
  };
3285
3766
  }, [active, getPeakDb, fillStyle]);
3286
- return /* @__PURE__ */ jsx18(
3767
+ return /* @__PURE__ */ jsx20(
3287
3768
  "canvas",
3288
3769
  {
3289
3770
  ref: canvasRef,
@@ -3294,8 +3775,8 @@ var ScrollingWaveform = ({
3294
3775
  };
3295
3776
 
3296
3777
  // src/components/OffsetScrubber.tsx
3297
- import { useCallback as useCallback7, useEffect as useEffect11, useMemo as useMemo4, useRef as useRef10, useState as useState11 } from "react";
3298
- import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
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";
3299
3780
  var SLIDER_HEIGHT_PX = 28;
3300
3781
  var TICK_HEIGHT_PX = 14;
3301
3782
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -3308,40 +3789,40 @@ function OffsetScrubber({
3308
3789
  onChange,
3309
3790
  disabled = false
3310
3791
  }) {
3311
- const trackRef = useRef10(null);
3312
- const [draftOffset, setDraftOffset] = useState11(offsetSamples);
3313
- const [isDragging, setIsDragging] = useState11(false);
3314
- useEffect11(() => {
3792
+ const trackRef = useRef11(null);
3793
+ const [draftOffset, setDraftOffset] = useState12(offsetSamples);
3794
+ const [isDragging, setIsDragging] = useState12(false);
3795
+ useEffect12(() => {
3315
3796
  if (!isDragging) setDraftOffset(offsetSamples);
3316
3797
  }, [offsetSamples, isDragging]);
3317
3798
  const sampleRate = cuePoints?.sample_rate ?? 44100;
3318
3799
  const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
3319
- const beatsForRange = useMemo4(() => {
3800
+ const beatsForRange = useMemo5(() => {
3320
3801
  return Math.round(60 / projectBpm * sampleRate);
3321
3802
  }, [projectBpm, sampleRate]);
3322
3803
  const rangeSamples = beatsForRange * meter;
3323
- const sampleToFraction = useCallback7(
3804
+ const sampleToFraction = useCallback8(
3324
3805
  (sample) => {
3325
3806
  const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
3326
3807
  return (clamped + rangeSamples) / (2 * rangeSamples);
3327
3808
  },
3328
3809
  [rangeSamples]
3329
3810
  );
3330
- const fractionToSample = useCallback7(
3811
+ const fractionToSample = useCallback8(
3331
3812
  (fraction) => {
3332
3813
  const clamped = Math.max(0, Math.min(1, fraction));
3333
3814
  return Math.round(clamped * 2 * rangeSamples - rangeSamples);
3334
3815
  },
3335
3816
  [rangeSamples]
3336
3817
  );
3337
- const snapTargets = useMemo4(() => {
3818
+ const snapTargets = useMemo5(() => {
3338
3819
  if (!cuePoints || cuePoints.beats.length === 0) return [];
3339
3820
  const downbeat = cuePoints.beats[0];
3340
3821
  const positives = cuePoints.beats.map((b) => b - downbeat);
3341
3822
  const negatives = positives.slice(1).map((p) => -p);
3342
3823
  return [...negatives, ...positives].sort((a, b) => a - b);
3343
3824
  }, [cuePoints]);
3344
- const snapToBeat = useCallback7(
3825
+ const snapToBeat = useCallback8(
3345
3826
  (sample) => {
3346
3827
  if (snapTargets.length === 0) return sample;
3347
3828
  let best = snapTargets[0];
@@ -3357,7 +3838,7 @@ function OffsetScrubber({
3357
3838
  },
3358
3839
  [snapTargets]
3359
3840
  );
3360
- const handlePointerDown = useCallback7(
3841
+ const handlePointerDown = useCallback8(
3361
3842
  (e) => {
3362
3843
  if (disabled || !cuePoints) return;
3363
3844
  e.preventDefault();
@@ -3391,7 +3872,7 @@ function OffsetScrubber({
3391
3872
  },
3392
3873
  [disabled, cuePoints, fractionToSample, onChange, snapToBeat]
3393
3874
  );
3394
- const handleResetToZero = useCallback7(() => {
3875
+ const handleResetToZero = useCallback8(() => {
3395
3876
  if (disabled) return;
3396
3877
  setDraftOffset(0);
3397
3878
  onChange(0);
@@ -3399,7 +3880,7 @@ function OffsetScrubber({
3399
3880
  const thumbFraction = sampleToFraction(draftOffset);
3400
3881
  const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
3401
3882
  const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
3402
- const ticks = useMemo4(() => {
3883
+ const ticks = useMemo5(() => {
3403
3884
  if (!cuePoints) return [];
3404
3885
  const downbeat = cuePoints.beats[0] ?? 0;
3405
3886
  return cuePoints.beats.map((b, i) => {
@@ -3410,9 +3891,9 @@ function OffsetScrubber({
3410
3891
  });
3411
3892
  }, [cuePoints, sampleToFraction]);
3412
3893
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
3413
- return /* @__PURE__ */ jsxs13("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
3414
- /* @__PURE__ */ jsx19("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
3415
- /* @__PURE__ */ jsxs13(
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(
3416
3897
  "div",
3417
3898
  {
3418
3899
  ref: trackRef,
@@ -3428,7 +3909,7 @@ function OffsetScrubber({
3428
3909
  "aria-valuenow": draftOffset,
3429
3910
  "aria-disabled": isDisabled,
3430
3911
  children: [
3431
- /* @__PURE__ */ jsx19(
3912
+ /* @__PURE__ */ jsx21(
3432
3913
  "div",
3433
3914
  {
3434
3915
  "aria-hidden": "true",
@@ -3436,7 +3917,7 @@ function OffsetScrubber({
3436
3917
  style: { left: "50%" }
3437
3918
  }
3438
3919
  ),
3439
- ticks.map((t) => /* @__PURE__ */ jsx19(
3920
+ ticks.map((t) => /* @__PURE__ */ jsx21(
3440
3921
  "div",
3441
3922
  {
3442
3923
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -3451,7 +3932,7 @@ function OffsetScrubber({
3451
3932
  },
3452
3933
  t.i
3453
3934
  )),
3454
- /* @__PURE__ */ jsx19(
3935
+ /* @__PURE__ */ jsx21(
3455
3936
  "div",
3456
3937
  {
3457
3938
  "data-testid": "offset-scrubber-thumb",
@@ -3468,7 +3949,7 @@ function OffsetScrubber({
3468
3949
  ]
3469
3950
  }
3470
3951
  ),
3471
- /* @__PURE__ */ jsx19(
3952
+ /* @__PURE__ */ jsx21(
3472
3953
  "span",
3473
3954
  {
3474
3955
  "data-testid": "offset-scrubber-readout",
@@ -3476,7 +3957,7 @@ function OffsetScrubber({
3476
3957
  children: formatOffset(draftOffset, sampleRate)
3477
3958
  }
3478
3959
  ),
3479
- /* @__PURE__ */ jsx19(
3960
+ /* @__PURE__ */ jsx21(
3480
3961
  "button",
3481
3962
  {
3482
3963
  type: "button",
@@ -3488,7 +3969,7 @@ function OffsetScrubber({
3488
3969
  children: "\u2316"
3489
3970
  }
3490
3971
  ),
3491
- bpmMismatch && /* @__PURE__ */ jsx19(
3972
+ bpmMismatch && /* @__PURE__ */ jsx21(
3492
3973
  "span",
3493
3974
  {
3494
3975
  "data-testid": "offset-bpm-mismatch",
@@ -3560,13 +4041,13 @@ function synthesizeCuePoints({
3560
4041
  }
3561
4042
 
3562
4043
  // src/hooks/useSceneState.ts
3563
- import { useState as useState12, useCallback as useCallback8, useRef as useRef11 } from "react";
4044
+ import { useState as useState13, useCallback as useCallback9, useRef as useRef12 } from "react";
3564
4045
  function useSceneState(activeSceneId, initialValue) {
3565
- const [stateMap, setStateMap] = useState12(() => /* @__PURE__ */ new Map());
3566
- const activeSceneIdRef = useRef11(activeSceneId);
4046
+ const [stateMap, setStateMap] = useState13(() => /* @__PURE__ */ new Map());
4047
+ const activeSceneIdRef = useRef12(activeSceneId);
3567
4048
  activeSceneIdRef.current = activeSceneId;
3568
4049
  const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
3569
- const setForCurrentScene = useCallback8((value) => {
4050
+ const setForCurrentScene = useCallback9((value) => {
3570
4051
  const sid = activeSceneIdRef.current;
3571
4052
  if (sid === null) return;
3572
4053
  setStateMap((prev) => {
@@ -3577,7 +4058,7 @@ function useSceneState(activeSceneId, initialValue) {
3577
4058
  return newMap;
3578
4059
  });
3579
4060
  }, [initialValue]);
3580
- const setForScene = useCallback8((sceneId, value) => {
4061
+ const setForScene = useCallback9((sceneId, value) => {
3581
4062
  setStateMap((prev) => {
3582
4063
  const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
3583
4064
  const next = typeof value === "function" ? value(current) : value;
@@ -3590,10 +4071,10 @@ function useSceneState(activeSceneId, initialValue) {
3590
4071
  }
3591
4072
 
3592
4073
  // src/hooks/useAnySolo.ts
3593
- import { useEffect as useEffect12, useState as useState13 } from "react";
4074
+ import { useEffect as useEffect13, useState as useState14 } from "react";
3594
4075
  function useAnySolo(host) {
3595
- const [anySolo, setAnySolo] = useState13(false);
3596
- useEffect12(() => {
4076
+ const [anySolo, setAnySolo] = useState14(false);
4077
+ useEffect13(() => {
3597
4078
  let active = true;
3598
4079
  const refresh = () => {
3599
4080
  host.isAnySoloActive().then((v) => {
@@ -3612,7 +4093,7 @@ function useAnySolo(host) {
3612
4093
  }
3613
4094
 
3614
4095
  // src/hooks/useSoundHistory.ts
3615
- import { useCallback as useCallback9, useMemo as useMemo5, useRef as useRef12, useState as useState14 } from "react";
4096
+ import { useCallback as useCallback10, useMemo as useMemo6, useRef as useRef13, useState as useState15 } from "react";
3616
4097
  var EMPTY = { entries: [], cursor: -1 };
3617
4098
  function sameDescriptor(a, b) {
3618
4099
  if (a === b) return true;
@@ -3624,14 +4105,14 @@ function sameDescriptor(a, b) {
3624
4105
  }
3625
4106
  function useSoundHistory(applySound, opts = {}) {
3626
4107
  const max = Math.max(2, opts.max ?? 24);
3627
- const applyRef = useRef12(applySound);
4108
+ const applyRef = useRef13(applySound);
3628
4109
  applyRef.current = applySound;
3629
- const onChangeRef = useRef12(opts.onChange);
4110
+ const onChangeRef = useRef13(opts.onChange);
3630
4111
  onChangeRef.current = opts.onChange;
3631
- const dataRef = useRef12({});
3632
- const [, setVersion] = useState14(0);
3633
- const bump = useCallback9(() => setVersion((v) => v + 1), []);
3634
- const commit = useCallback9(
4112
+ const dataRef = useRef13({});
4113
+ const [, setVersion] = useState15(0);
4114
+ const bump = useCallback10(() => setVersion((v) => v + 1), []);
4115
+ const commit = useCallback10(
3635
4116
  (trackId, next, notify) => {
3636
4117
  dataRef.current = { ...dataRef.current, [trackId]: next };
3637
4118
  bump();
@@ -3639,7 +4120,7 @@ function useSoundHistory(applySound, opts = {}) {
3639
4120
  },
3640
4121
  [bump]
3641
4122
  );
3642
- const record = useCallback9(
4123
+ const record = useCallback10(
3643
4124
  (trackId, descriptor, label) => {
3644
4125
  const h = dataRef.current[trackId];
3645
4126
  const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
@@ -3654,7 +4135,7 @@ function useSoundHistory(applySound, opts = {}) {
3654
4135
  },
3655
4136
  [max, commit]
3656
4137
  );
3657
- const restoreTo = useCallback9(
4138
+ const restoreTo = useCallback10(
3658
4139
  async (trackId, index) => {
3659
4140
  const h = dataRef.current[trackId];
3660
4141
  if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
@@ -3664,7 +4145,7 @@ function useSoundHistory(applySound, opts = {}) {
3664
4145
  },
3665
4146
  [commit]
3666
4147
  );
3667
- const undo = useCallback9(
4148
+ const undo = useCallback10(
3668
4149
  (trackId) => {
3669
4150
  const h = dataRef.current[trackId];
3670
4151
  if (!h || h.cursor <= 0) return Promise.resolve(false);
@@ -3672,7 +4153,7 @@ function useSoundHistory(applySound, opts = {}) {
3672
4153
  },
3673
4154
  [restoreTo]
3674
4155
  );
3675
- const toggleFavorite = useCallback9(
4156
+ const toggleFavorite = useCallback10(
3676
4157
  (trackId, index) => {
3677
4158
  const h = dataRef.current[trackId];
3678
4159
  if (!h || index < 0 || index >= h.entries.length) return;
@@ -3681,7 +4162,7 @@ function useSoundHistory(applySound, opts = {}) {
3681
4162
  },
3682
4163
  [commit]
3683
4164
  );
3684
- const restore = useCallback9(
4165
+ const restore = useCallback10(
3685
4166
  (trackId, state) => {
3686
4167
  const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
3687
4168
  const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
@@ -3690,15 +4171,15 @@ function useSoundHistory(applySound, opts = {}) {
3690
4171
  },
3691
4172
  [commit]
3692
4173
  );
3693
- const list = useCallback9(
4174
+ const list = useCallback10(
3694
4175
  (trackId) => dataRef.current[trackId] ?? EMPTY,
3695
4176
  []
3696
4177
  );
3697
- const canUndo = useCallback9((trackId) => {
4178
+ const canUndo = useCallback10((trackId) => {
3698
4179
  const h = dataRef.current[trackId];
3699
4180
  return !!h && h.cursor > 0;
3700
4181
  }, []);
3701
- const clear = useCallback9(
4182
+ const clear = useCallback10(
3702
4183
  (trackId) => {
3703
4184
  if (dataRef.current[trackId]) {
3704
4185
  const next = { ...dataRef.current };
@@ -3710,18 +4191,18 @@ function useSoundHistory(applySound, opts = {}) {
3710
4191
  },
3711
4192
  [bump]
3712
4193
  );
3713
- const reset = useCallback9(() => {
4194
+ const reset = useCallback10(() => {
3714
4195
  dataRef.current = {};
3715
4196
  bump();
3716
4197
  }, [bump]);
3717
- return useMemo5(
4198
+ return useMemo6(
3718
4199
  () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
3719
4200
  [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
3720
4201
  );
3721
4202
  }
3722
4203
 
3723
4204
  // src/hooks/useTrackReorder.ts
3724
- import { useCallback as useCallback10, useRef as useRef13, useState as useState15 } from "react";
4205
+ import { useCallback as useCallback11, useRef as useRef14, useState as useState16 } from "react";
3725
4206
  function moveItem(arr, from, to) {
3726
4207
  const next = arr.slice();
3727
4208
  if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
@@ -3738,12 +4219,12 @@ function useTrackReorder({
3738
4219
  getId,
3739
4220
  onError
3740
4221
  }) {
3741
- const [draggingIndex, setDraggingIndex] = useState15(null);
3742
- const [dragOverIndex, setDragOverIndex] = useState15(null);
3743
- const fromRef = useRef13(null);
3744
- const itemsRef = useRef13(items);
4222
+ const [draggingIndex, setDraggingIndex] = useState16(null);
4223
+ const [dragOverIndex, setDragOverIndex] = useState16(null);
4224
+ const fromRef = useRef14(null);
4225
+ const itemsRef = useRef14(items);
3745
4226
  itemsRef.current = items;
3746
- const dragPropsFor = useCallback10(
4227
+ const dragPropsFor = useCallback11(
3747
4228
  (index) => ({
3748
4229
  handleProps: {
3749
4230
  draggable: true,
@@ -3805,7 +4286,7 @@ function useTrackReorder({
3805
4286
  }
3806
4287
 
3807
4288
  // src/constants/sdk-version.ts
3808
- var PLUGIN_SDK_VERSION = "2.26.0";
4289
+ var PLUGIN_SDK_VERSION = "2.28.0";
3809
4290
 
3810
4291
  // src/utils/format-concurrent-tracks.ts
3811
4292
  function formatConcurrentTracks(ctx) {
@@ -3965,6 +4446,8 @@ export {
3965
4446
  FX_DISPLAY_LABELS,
3966
4447
  FX_ENGINE_PLUGIN_NAMES,
3967
4448
  FX_PRESET_CONFIGS,
4449
+ FadeModal,
4450
+ FadeTrackRow,
3968
4451
  FxToggleBar,
3969
4452
  GUTTER_W,
3970
4453
  ImportTrackModal,
@@ -3983,6 +4466,7 @@ export {
3983
4466
  SamplePackCTACard,
3984
4467
  ScrollingWaveform,
3985
4468
  SorceryProgressBar,
4469
+ TEXTURAL_ROLES,
3986
4470
  TrackDrawer,
3987
4471
  TrackMeterStrip,
3988
4472
  TrackRow,
@@ -3990,17 +4474,21 @@ export {
3990
4474
  WaveformView,
3991
4475
  analyzeWavPeak,
3992
4476
  asCrossfadeMeta,
4477
+ asFadeMeta,
3993
4478
  buildCrossfadeInpaintPrompt,
3994
4479
  buildCrossfadeVolumeCurves,
4480
+ buildFadeVolumeCurve,
3995
4481
  calculateTimeBasedTarget,
3996
4482
  cellToPx,
3997
4483
  centerScrollTop,
3998
4484
  computePeaks,
3999
4485
  dbToSlider,
4486
+ defaultFadeGesture,
4000
4487
  drawWaveform,
4001
4488
  formatConcurrentTracks,
4002
4489
  moveItem,
4003
4490
  parseCrossfadePairs,
4491
+ parseFades,
4004
4492
  pickTopKWeighted,
4005
4493
  pitchToName,
4006
4494
  pxToCell,