@signalsandsorcery/plugin-sdk 2.28.1 → 2.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1343,6 +1343,7 @@ var LevelMeter = ({
1343
1343
 
1344
1344
  // src/hooks/useTrackLevels.ts
1345
1345
  import { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
1346
+ var meterDiagRLast = /* @__PURE__ */ new Map();
1346
1347
  var POLL_INTERVAL_MS = 33;
1347
1348
  var HIDDEN_RECHECK_MS = 250;
1348
1349
  var METER_FLOOR_DB = -120;
@@ -1474,6 +1475,11 @@ function useTrackMeter(handle, trackId) {
1474
1475
  }
1475
1476
  const update = () => {
1476
1477
  const level = handle.getLevel(trackId);
1478
+ const dNow = Date.now();
1479
+ if ((meterDiagRLast.get(trackId) ?? 0) < dNow - 3e3) {
1480
+ meterDiagRLast.set(trackId, dNow);
1481
+ console.log(`[MeterDiagR] lookup trackId=${trackId} \u2192 ${level === null ? "MISS (no level for this id)" : "hit"}`);
1482
+ }
1477
1483
  const now = performance.now();
1478
1484
  const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1e3) : 0;
1479
1485
  lastTickRef.current = now;
@@ -2175,8 +2181,7 @@ function TrackRow({
2175
2181
  {
2176
2182
  "data-testid": "sdk-mute-button",
2177
2183
  onClick: onMuteToggle,
2178
- disabled: isGenerating,
2179
- className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
2184
+ className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
2180
2185
  title: isMuted ? "Unmute track" : "Mute track",
2181
2186
  children: "M"
2182
2187
  }
@@ -2427,6 +2432,18 @@ function CrossfadeTrackRow({
2427
2432
  }
2428
2433
 
2429
2434
  // src/crossfade-meta.ts
2435
+ function hashString(s) {
2436
+ let h = 5381;
2437
+ for (let i = 0; i < s.length; i++) h = (h << 5) + h ^ s.charCodeAt(i) | 0;
2438
+ return (h >>> 0).toString(36);
2439
+ }
2440
+ function soundIdentity(snap) {
2441
+ if (!snap) return "";
2442
+ if (snap.kind === "preset") return `p:${hashString(snap.state)}`;
2443
+ if (snap.kind === "sample") return `s:${snap.samplePath}`;
2444
+ if (snap.kind === "instrument") return `i:${snap.instrumentId ?? ""}:${hashString(JSON.stringify(snap.zones))}`;
2445
+ return "";
2446
+ }
2430
2447
  var EQUAL_POWER_GAIN = 0.707;
2431
2448
  function asCrossfadeMeta(val) {
2432
2449
  if (!val || typeof val !== "object") return null;
@@ -2576,9 +2593,11 @@ function asFadeMeta(val) {
2576
2593
  const m = val;
2577
2594
  if (m.direction !== "in" && m.direction !== "out") return null;
2578
2595
  if (m.gesture !== "volume" && m.gesture !== "build") return null;
2596
+ const effect = m.effect === "stutter" || m.effect === "chopped" || m.effect === "delay" || m.effect === "fade" ? m.effect : void 0;
2579
2597
  return {
2580
2598
  direction: m.direction,
2581
2599
  gesture: m.gesture,
2600
+ effect,
2582
2601
  sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
2583
2602
  sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
2584
2603
  sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
@@ -2665,6 +2684,7 @@ function FadeTrackRow({
2665
2684
  layer,
2666
2685
  direction,
2667
2686
  gesture,
2687
+ effect,
2668
2688
  sliderPos = 0.5,
2669
2689
  onMuteToggle,
2670
2690
  onSoloToggle,
@@ -2678,7 +2698,8 @@ function FadeTrackRow({
2678
2698
  const [confirmDelete, setConfirmDelete] = React10.useState(false);
2679
2699
  const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
2680
2700
  const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
2681
- const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
2701
+ const verb = effect && effect !== "fade" ? effect.charAt(0).toUpperCase() + effect.slice(1) : "Fade";
2702
+ const badge = direction === "in" ? `\u2197 ${verb} in` : `\u2198 ${verb} out`;
2682
2703
  return /* @__PURE__ */ jsxs9(
2683
2704
  "div",
2684
2705
  {
@@ -3413,9 +3434,701 @@ function CrossfadeModal({
3413
3434
  ) });
3414
3435
  }
3415
3436
 
3416
- // src/components/DownloadPackButton.tsx
3417
- import { useCallback as useCallback7, useEffect as useEffect9, useState as useState10 } from "react";
3437
+ // src/components/TransitionDesigner.tsx
3438
+ import { useCallback as useCallback8, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef10, useState as useState11 } from "react";
3439
+
3440
+ // src/hooks/useTrackReorder.ts
3441
+ import { useCallback as useCallback7, useRef as useRef9, useState as useState10 } from "react";
3442
+ function moveItem(arr, from, to) {
3443
+ const next = arr.slice();
3444
+ if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
3445
+ return next;
3446
+ }
3447
+ const [moved] = next.splice(from, 1);
3448
+ next.splice(to, 0, moved);
3449
+ return next;
3450
+ }
3451
+ function useTrackReorder({
3452
+ host,
3453
+ items,
3454
+ setItems,
3455
+ getId,
3456
+ onError
3457
+ }) {
3458
+ const [draggingIndex, setDraggingIndex] = useState10(null);
3459
+ const [dragOverIndex, setDragOverIndex] = useState10(null);
3460
+ const fromRef = useRef9(null);
3461
+ const itemsRef = useRef9(items);
3462
+ itemsRef.current = items;
3463
+ const dragPropsFor = useCallback7(
3464
+ (index) => ({
3465
+ handleProps: {
3466
+ draggable: true,
3467
+ onDragStart: (e) => {
3468
+ fromRef.current = index;
3469
+ setDraggingIndex(index);
3470
+ if (e.dataTransfer) {
3471
+ e.dataTransfer.effectAllowed = "move";
3472
+ try {
3473
+ e.dataTransfer.setData("text/plain", String(index));
3474
+ } catch {
3475
+ }
3476
+ }
3477
+ },
3478
+ onDragEnd: () => {
3479
+ fromRef.current = null;
3480
+ setDraggingIndex(null);
3481
+ setDragOverIndex(null);
3482
+ }
3483
+ },
3484
+ rowProps: {
3485
+ onDragEnter: (e) => {
3486
+ if (fromRef.current === null) return;
3487
+ e.preventDefault();
3488
+ setDragOverIndex(index);
3489
+ },
3490
+ onDragOver: (e) => {
3491
+ if (fromRef.current === null) return;
3492
+ e.preventDefault();
3493
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
3494
+ setDragOverIndex((cur) => cur === index ? cur : index);
3495
+ },
3496
+ onDragLeave: () => {
3497
+ setDragOverIndex((cur) => cur === index ? null : cur);
3498
+ },
3499
+ onDrop: (e) => {
3500
+ e.preventDefault();
3501
+ const from = fromRef.current;
3502
+ fromRef.current = null;
3503
+ setDraggingIndex(null);
3504
+ setDragOverIndex(null);
3505
+ if (from === null || from === index) return;
3506
+ const prev = itemsRef.current;
3507
+ const next = moveItem(prev, from, index);
3508
+ setItems(next);
3509
+ const ids = next.map(getId);
3510
+ Promise.resolve(host.reorderTracks(ids)).catch((err) => {
3511
+ setItems(prev);
3512
+ onError?.(err);
3513
+ });
3514
+ }
3515
+ },
3516
+ isDragging: draggingIndex === index,
3517
+ isDragTarget: dragOverIndex === index && draggingIndex !== index
3518
+ }),
3519
+ [host, setItems, getId, onError, draggingIndex, dragOverIndex]
3520
+ );
3521
+ return { dragPropsFor, draggingIndex, dragOverIndex };
3522
+ }
3523
+
3524
+ // src/transition-designer-meta.ts
3525
+ var TRANSITION_DESIGNER_DRAFT_KEY = "transitionDesigner:draft";
3526
+ var AUDIO_EFFECTS = ["fade", "stutter", "chopped", "delay"];
3527
+ var AUDIO_EFFECT_LABEL = {
3528
+ fade: "Fade",
3529
+ stutter: "Stutter",
3530
+ chopped: "Chopped",
3531
+ delay: "Delay"
3532
+ };
3533
+ function asAudioEffect(v) {
3534
+ return v === "fade" || v === "stutter" || v === "chopped" || v === "delay" ? v : null;
3535
+ }
3536
+ function rowType(hasOrigin, hasTarget) {
3537
+ if (hasOrigin && hasTarget) return "crossfade";
3538
+ if (hasOrigin) return "fade-out";
3539
+ if (hasTarget) return "fade-in";
3540
+ return null;
3541
+ }
3542
+ function asTransitionDesignerDraft(val) {
3543
+ if (!val || typeof val !== "object") return null;
3544
+ const d = val;
3545
+ const clean = (a) => Array.isArray(a) ? a.filter((x) => x === null || typeof x === "string") : [];
3546
+ const cleanEffects = (e) => {
3547
+ const out = {};
3548
+ if (e && typeof e === "object") {
3549
+ for (const [k, v] of Object.entries(e)) {
3550
+ const eff = asAudioEffect(v);
3551
+ if (eff) out[k] = eff;
3552
+ }
3553
+ }
3554
+ return out;
3555
+ };
3556
+ return {
3557
+ originOrder: clean(d.originOrder),
3558
+ targetOrder: clean(d.targetOrder),
3559
+ rowEffects: cleanEffects(d.rowEffects)
3560
+ };
3561
+ }
3562
+ function reconcileSlots(saved, poolIds) {
3563
+ const pool = new Set(poolIds);
3564
+ const seen = /* @__PURE__ */ new Set();
3565
+ const out = [];
3566
+ for (const slot of saved ?? []) {
3567
+ if (slot === null) {
3568
+ out.push(null);
3569
+ continue;
3570
+ }
3571
+ if (pool.has(slot) && !seen.has(slot)) {
3572
+ out.push(slot);
3573
+ seen.add(slot);
3574
+ }
3575
+ }
3576
+ for (const id of poolIds) {
3577
+ if (!seen.has(id)) {
3578
+ out.push(id);
3579
+ seen.add(id);
3580
+ }
3581
+ }
3582
+ return out;
3583
+ }
3584
+ function buildRowSlots(originSlots, targetSlots) {
3585
+ const n = Math.max(originSlots.length, targetSlots.length);
3586
+ const rows = [];
3587
+ for (let i = 0; i < n; i++) {
3588
+ const originId = originSlots[i] ?? null;
3589
+ const targetId = targetSlots[i] ?? null;
3590
+ rows.push({ originId, targetId, type: rowType(originId !== null, targetId !== null) });
3591
+ }
3592
+ return rows;
3593
+ }
3594
+ function normalizeSlots(originSlots, targetSlots) {
3595
+ const rows = buildRowSlots(originSlots, targetSlots).filter(
3596
+ (r) => r.originId !== null || r.targetId !== null
3597
+ );
3598
+ const trimTrailing = (a) => {
3599
+ let end = a.length;
3600
+ while (end > 0 && a[end - 1] === null) end--;
3601
+ return a.slice(0, end);
3602
+ };
3603
+ return {
3604
+ originOrder: trimTrailing(rows.map((r) => r.originId)),
3605
+ targetOrder: trimTrailing(rows.map((r) => r.targetId))
3606
+ };
3607
+ }
3608
+ function padSlots(slots, n) {
3609
+ if (slots.length >= n) return slots.slice();
3610
+ return [...slots, ...new Array(n - slots.length).fill(null)];
3611
+ }
3612
+ function padPair(originSlots, targetSlots) {
3613
+ const n = Math.max(originSlots.length, targetSlots.length);
3614
+ return [padSlots(originSlots, n), padSlots(targetSlots, n)];
3615
+ }
3616
+ function slotsEqual(a, b) {
3617
+ if (a.length !== b.length) return false;
3618
+ for (let i = 0; i < a.length; i++) {
3619
+ if (a[i] !== b[i]) return false;
3620
+ }
3621
+ return true;
3622
+ }
3623
+ function rowKey(row) {
3624
+ if (row.type === "crossfade") return `xf:${row.originId}|${row.targetId}`;
3625
+ if (row.type === "fade-out") return `fo:${row.originId}`;
3626
+ if (row.type === "fade-in") return `fi:${row.targetId}`;
3627
+ return null;
3628
+ }
3629
+ function dbIdsFromKeys(keys) {
3630
+ const out = /* @__PURE__ */ new Set();
3631
+ for (const k of keys) {
3632
+ const body = k.slice(3);
3633
+ if (k.startsWith("xf:")) {
3634
+ const sep = body.indexOf("|");
3635
+ out.add(body.slice(0, sep));
3636
+ out.add(body.slice(sep + 1));
3637
+ } else {
3638
+ out.add(body);
3639
+ }
3640
+ }
3641
+ return out;
3642
+ }
3643
+
3644
+ // src/components/TransitionDesigner.tsx
3418
3645
  import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
3646
+ var CROSSFADE_ESTIMATE_MS = 15e3;
3647
+ var FADE_ESTIMATE_MS = 11e3;
3648
+ var CREATE_ALL_CONCURRENCY = 5;
3649
+ var TYPE_LABEL = {
3650
+ crossfade: "Crossfade",
3651
+ "fade-out": "Fade out",
3652
+ "fade-in": "Fade in"
3653
+ };
3654
+ function shortId3(dbId) {
3655
+ return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3656
+ }
3657
+ function TransitionDesigner({
3658
+ host,
3659
+ fromSceneId,
3660
+ toSceneId,
3661
+ transitionSceneId,
3662
+ excludeSourceDbIds,
3663
+ onCreateCrossfade,
3664
+ onCreateFade,
3665
+ onCreateAudioTransition,
3666
+ familyLabel,
3667
+ testIdPrefix = "transition-designer"
3668
+ }) {
3669
+ const [load, setLoad] = useState11({ status: "loading" });
3670
+ const [fromName, setFromName] = useState11(null);
3671
+ const [toName, setToName] = useState11(null);
3672
+ const [originSlots, setOriginSlots] = useState11([]);
3673
+ const [targetSlots, setTargetSlots] = useState11([]);
3674
+ const [creatingKeys, setCreatingKeys] = useState11(() => /* @__PURE__ */ new Set());
3675
+ const [rowErrors, setRowErrors] = useState11({});
3676
+ const [rowEffects, setRowEffects] = useState11({});
3677
+ const rowEffectsRef = useRef10(rowEffects);
3678
+ rowEffectsRef.current = rowEffects;
3679
+ const audioEffectsEnabled = !!onCreateAudioTransition;
3680
+ const excludeRef = useRef10(excludeSourceDbIds);
3681
+ excludeRef.current = excludeSourceDbIds;
3682
+ const originSlotsRef = useRef10(originSlots);
3683
+ originSlotsRef.current = originSlots;
3684
+ const targetSlotsRef = useRef10(targetSlots);
3685
+ targetSlotsRef.current = targetSlots;
3686
+ const creatingKeysRef = useRef10(creatingKeys);
3687
+ creatingKeysRef.current = creatingKeys;
3688
+ const dragRef = useRef10(null);
3689
+ const [dragging, setDragging] = useState11(null);
3690
+ const [dragOver, setDragOver] = useState11(null);
3691
+ const excludeSet = useMemo5(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3692
+ const originPool = useMemo5(
3693
+ () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
3694
+ [load, excludeSet]
3695
+ );
3696
+ const targetPool = useMemo5(
3697
+ () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
3698
+ [load, excludeSet]
3699
+ );
3700
+ const originById = useMemo5(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
3701
+ const targetById = useMemo5(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
3702
+ const originByIdRef = useRef10(originById);
3703
+ originByIdRef.current = originById;
3704
+ const targetByIdRef = useRef10(targetById);
3705
+ targetByIdRef.current = targetById;
3706
+ const refresh = useCallback8(async () => {
3707
+ if (!host.listSceneFamilyTracks) {
3708
+ setLoad({ status: "error", message: "This host does not support transition tracks." });
3709
+ return;
3710
+ }
3711
+ setLoad({ status: "loading" });
3712
+ try {
3713
+ const [origin, target, fName, tName, draftRaw] = await Promise.all([
3714
+ host.listSceneFamilyTracks(fromSceneId),
3715
+ host.listSceneFamilyTracks(toSceneId),
3716
+ host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
3717
+ host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
3718
+ host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
3719
+ ]);
3720
+ const draft = asTransitionDesignerDraft(draftRaw);
3721
+ const exSet = new Set(excludeRef.current ?? []);
3722
+ const originIds = origin.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
3723
+ const targetIds = target.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
3724
+ const [po, pt] = padPair(
3725
+ reconcileSlots(draft?.originOrder, originIds),
3726
+ reconcileSlots(draft?.targetOrder, targetIds)
3727
+ );
3728
+ setOriginSlots(po);
3729
+ setTargetSlots(pt);
3730
+ setRowEffects(draft?.rowEffects ?? {});
3731
+ setFromName(fName);
3732
+ setToName(tName);
3733
+ setLoad({ status: "ready", origin, target });
3734
+ } catch (err) {
3735
+ setLoad({
3736
+ status: "error",
3737
+ message: err instanceof Error ? err.message : "Failed to load tracks."
3738
+ });
3739
+ }
3740
+ }, [host, fromSceneId, toSceneId, transitionSceneId]);
3741
+ useEffect9(() => {
3742
+ void refresh();
3743
+ }, [refresh]);
3744
+ useEffect9(() => {
3745
+ if (load.status !== "ready") return;
3746
+ const [po, pt] = padPair(
3747
+ reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
3748
+ reconcileSlots(targetSlotsRef.current, targetPool.map((t) => t.dbId))
3749
+ );
3750
+ if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
3751
+ if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
3752
+ }, [originPool, targetPool, load.status]);
3753
+ const mutate = useCallback8(
3754
+ (nextOrigin, nextTarget) => {
3755
+ const norm = normalizeSlots(nextOrigin, nextTarget);
3756
+ const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
3757
+ setOriginSlots(po);
3758
+ setTargetSlots(pt);
3759
+ if (host.setSceneData) {
3760
+ host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: rowEffectsRef.current }).catch(() => {
3761
+ });
3762
+ }
3763
+ },
3764
+ [host, transitionSceneId]
3765
+ );
3766
+ const setRowEffect = useCallback8(
3767
+ (sourceDbId, effect) => {
3768
+ setRowEffects((prev) => {
3769
+ const next = { ...prev, [sourceDbId]: effect };
3770
+ if (host.setSceneData) {
3771
+ const norm = normalizeSlots(originSlotsRef.current, targetSlotsRef.current);
3772
+ host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: next }).catch(() => {
3773
+ });
3774
+ }
3775
+ return next;
3776
+ });
3777
+ },
3778
+ [host, transitionSceneId]
3779
+ );
3780
+ const insertGapAbove = useCallback8(
3781
+ (col, index) => {
3782
+ const slots = col === "origin" ? originSlots : targetSlots;
3783
+ const next = [...slots.slice(0, index), null, ...slots.slice(index)];
3784
+ if (col === "origin") mutate(next, targetSlots);
3785
+ else mutate(originSlots, next);
3786
+ },
3787
+ [originSlots, targetSlots, mutate]
3788
+ );
3789
+ const removeGap = useCallback8(
3790
+ (col, index) => {
3791
+ const slots = col === "origin" ? originSlots : targetSlots;
3792
+ const next = slots.filter((_, i) => i !== index);
3793
+ if (col === "origin") mutate(next, targetSlots);
3794
+ else mutate(originSlots, next);
3795
+ },
3796
+ [originSlots, targetSlots, mutate]
3797
+ );
3798
+ const handleDrop = useCallback8(
3799
+ (col, to) => {
3800
+ const from = dragRef.current;
3801
+ dragRef.current = null;
3802
+ setDragging(null);
3803
+ setDragOver(null);
3804
+ if (!from || from.col !== col || from.index === to) return;
3805
+ if (col === "origin") mutate(moveItem(originSlots, from.index, to), targetSlots);
3806
+ else mutate(originSlots, moveItem(targetSlots, from.index, to));
3807
+ },
3808
+ [originSlots, targetSlots, mutate]
3809
+ );
3810
+ const rows = useMemo5(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
3811
+ const creatingDbIds = useMemo5(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
3812
+ const eligibleCount = useMemo5(
3813
+ () => rows.filter((r) => {
3814
+ const k = rowKey(r);
3815
+ return k !== null && !creatingKeys.has(k);
3816
+ }).length,
3817
+ [rows, creatingKeys]
3818
+ );
3819
+ const createRow = useCallback8(
3820
+ async (row) => {
3821
+ const key = rowKey(row);
3822
+ if (!key || !row.type || creatingKeysRef.current.has(key)) return;
3823
+ setCreatingKeys((prev) => new Set(prev).add(key));
3824
+ setRowErrors((prev) => {
3825
+ if (!(key in prev)) return prev;
3826
+ const next = { ...prev };
3827
+ delete next[key];
3828
+ return next;
3829
+ });
3830
+ try {
3831
+ if (row.type === "crossfade") {
3832
+ const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
3833
+ const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
3834
+ if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
3835
+ await onCreateCrossfade(
3836
+ { dbId: o.dbId, name: o.name, role: o.role },
3837
+ { dbId: t.dbId, name: t.name, role: t.role }
3838
+ );
3839
+ } else if (row.type === "fade-out") {
3840
+ const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
3841
+ if (!o) throw new Error("Track is no longer available \u2014 refresh and retry.");
3842
+ const eff = rowEffectsRef.current[o.dbId] ?? "fade";
3843
+ if (eff !== "fade" && onCreateAudioTransition) {
3844
+ await onCreateAudioTransition({ dbId: o.dbId, name: o.name, role: o.role }, "out", eff);
3845
+ } else {
3846
+ await onCreateFade({ dbId: o.dbId, name: o.name, role: o.role }, "out", defaultFadeGesture(o.role));
3847
+ }
3848
+ } else {
3849
+ const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
3850
+ if (!t) throw new Error("Track is no longer available \u2014 refresh and retry.");
3851
+ const eff = rowEffectsRef.current[t.dbId] ?? "fade";
3852
+ if (eff !== "fade" && onCreateAudioTransition) {
3853
+ await onCreateAudioTransition({ dbId: t.dbId, name: t.name, role: t.role }, "in", eff);
3854
+ } else {
3855
+ await onCreateFade({ dbId: t.dbId, name: t.name, role: t.role }, "in", defaultFadeGesture(t.role));
3856
+ }
3857
+ }
3858
+ } catch (err) {
3859
+ setRowErrors((prev) => ({
3860
+ ...prev,
3861
+ [key]: err instanceof Error ? err.message : "Failed to create transition."
3862
+ }));
3863
+ } finally {
3864
+ setCreatingKeys((prev) => {
3865
+ const next = new Set(prev);
3866
+ next.delete(key);
3867
+ return next;
3868
+ });
3869
+ }
3870
+ },
3871
+ [onCreateCrossfade, onCreateFade, onCreateAudioTransition]
3872
+ );
3873
+ const createAll = useCallback8(async () => {
3874
+ const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
3875
+ const k = rowKey(r);
3876
+ return k !== null && !creatingKeysRef.current.has(k);
3877
+ });
3878
+ if (eligible.length === 0) return;
3879
+ let cursor = 0;
3880
+ const worker = async () => {
3881
+ while (cursor < eligible.length) {
3882
+ const row = eligible[cursor];
3883
+ cursor += 1;
3884
+ await createRow(row);
3885
+ }
3886
+ };
3887
+ await Promise.all(
3888
+ Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
3889
+ );
3890
+ }, [createRow]);
3891
+ const fromLabel = fromName ?? "origin";
3892
+ const toLabel = toName ?? "target";
3893
+ const cellDragProps = (col, index, locked) => ({
3894
+ draggable: !locked,
3895
+ onDragStart: (e) => {
3896
+ if (locked) return;
3897
+ dragRef.current = { col, index };
3898
+ setDragging({ col, index });
3899
+ if (e.dataTransfer) {
3900
+ e.dataTransfer.effectAllowed = "move";
3901
+ try {
3902
+ e.dataTransfer.setData("text/plain", String(index));
3903
+ } catch {
3904
+ }
3905
+ }
3906
+ },
3907
+ onDragEnd: () => {
3908
+ dragRef.current = null;
3909
+ setDragging(null);
3910
+ setDragOver(null);
3911
+ },
3912
+ onDragEnter: (e) => {
3913
+ const d = dragRef.current;
3914
+ if (!d || d.col !== col) return;
3915
+ e.preventDefault();
3916
+ setDragOver({ col, index });
3917
+ },
3918
+ onDragOver: (e) => {
3919
+ const d = dragRef.current;
3920
+ if (!d || d.col !== col) return;
3921
+ e.preventDefault();
3922
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
3923
+ },
3924
+ onDragLeave: () => {
3925
+ setDragOver((cur) => cur && cur.col === col && cur.index === index ? null : cur);
3926
+ },
3927
+ onDrop: (e) => {
3928
+ e.preventDefault();
3929
+ handleDrop(col, index);
3930
+ }
3931
+ });
3932
+ const renderCell = (col, index, slotId) => {
3933
+ const byId = col === "origin" ? originById : targetById;
3934
+ const track = slotId ? byId.get(slotId) : void 0;
3935
+ const locked = slotId !== null && creatingDbIds.has(slotId);
3936
+ const isDragging = dragging?.col === col && dragging.index === index;
3937
+ const isDragTarget = dragOver?.col === col && dragOver.index === index && !isDragging;
3938
+ const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
3939
+ const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
3940
+ if (slotId === null) {
3941
+ return /* @__PURE__ */ jsxs13(
3942
+ "div",
3943
+ {
3944
+ ...cellDragProps(col, index, false),
3945
+ "data-testid": `${testIdPrefix}-${col}-gap-${index}`,
3946
+ className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
3947
+ children: [
3948
+ /* @__PURE__ */ jsx17("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
3949
+ /* @__PURE__ */ jsx17(
3950
+ "button",
3951
+ {
3952
+ type: "button",
3953
+ "data-testid": `${testIdPrefix}-${col}-remove-gap-${index}`,
3954
+ onClick: () => removeGap(col, index),
3955
+ title: "Remove gap",
3956
+ className: "text-[10px] text-sas-muted hover:text-sas-danger",
3957
+ children: "\u2715"
3958
+ }
3959
+ )
3960
+ ]
3961
+ }
3962
+ );
3963
+ }
3964
+ const primary = track ? track.prompt?.trim() || track.name : slotId;
3965
+ const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
3966
+ return /* @__PURE__ */ jsx17(
3967
+ "div",
3968
+ {
3969
+ ...cellDragProps(col, index, locked),
3970
+ "data-testid": `${testIdPrefix}-${col}-cell-${slotId}`,
3971
+ "data-value": slotId,
3972
+ className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
3973
+ title: track ? track.dbId : "Track no longer available",
3974
+ children: /* @__PURE__ */ jsxs13("div", { className: "flex items-start gap-1", children: [
3975
+ /* @__PURE__ */ jsx17("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
3976
+ /* @__PURE__ */ jsxs13("div", { className: "min-w-0 flex-1", children: [
3977
+ /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-text truncate", children: primary }),
3978
+ meta && /* @__PURE__ */ jsx17("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
3979
+ ] }),
3980
+ /* @__PURE__ */ jsx17(
3981
+ "button",
3982
+ {
3983
+ type: "button",
3984
+ "data-testid": `${testIdPrefix}-${col}-insert-gap-${index}`,
3985
+ onClick: () => insertGapAbove(col, index),
3986
+ disabled: locked,
3987
+ title: "Insert a gap above (make this a fade)",
3988
+ className: "text-[10px] text-sas-muted opacity-0 group-hover:opacity-100 hover:text-sas-accent disabled:opacity-30",
3989
+ children: "+gap"
3990
+ }
3991
+ )
3992
+ ] })
3993
+ }
3994
+ );
3995
+ };
3996
+ return /* @__PURE__ */ jsxs13("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
3997
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
3998
+ /* @__PURE__ */ jsxs13("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
3999
+ /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: fromLabel }),
4000
+ " \u2192",
4001
+ " ",
4002
+ /* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: toLabel }),
4003
+ familyLabel ? ` \xB7 ${familyLabel}` : "",
4004
+ " \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
4005
+ ] }),
4006
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 shrink-0", children: [
4007
+ creatingKeys.size > 0 && /* @__PURE__ */ jsxs13("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4008
+ creatingKeys.size,
4009
+ " creating\u2026"
4010
+ ] }),
4011
+ /* @__PURE__ */ jsxs13(
4012
+ "button",
4013
+ {
4014
+ type: "button",
4015
+ "data-testid": `${testIdPrefix}-create-all`,
4016
+ onClick: createAll,
4017
+ disabled: eligibleCount === 0,
4018
+ title: "Create every staged transition at once (runs several concurrently)",
4019
+ className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors whitespace-nowrap ${eligibleCount > 0 ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
4020
+ children: [
4021
+ "Create all",
4022
+ eligibleCount > 0 ? ` (${eligibleCount})` : ""
4023
+ ]
4024
+ }
4025
+ )
4026
+ ] })
4027
+ ] }),
4028
+ /* @__PURE__ */ jsxs13("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4029
+ /* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4030
+ "Origin (",
4031
+ fromLabel,
4032
+ ")"
4033
+ ] }),
4034
+ /* @__PURE__ */ jsx17("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4035
+ /* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4036
+ "Target (",
4037
+ toLabel,
4038
+ ")"
4039
+ ] })
4040
+ ] }),
4041
+ load.status === "loading" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4042
+ load.status === "error" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4043
+ load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ jsxs13("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4044
+ "No tracks to arrange in this panel for either scene. Add tracks to ",
4045
+ fromLabel,
4046
+ " or ",
4047
+ toLabel,
4048
+ " ",
4049
+ "first (or free one by deleting an existing crossfade/fade)."
4050
+ ] }) : /* @__PURE__ */ jsx17("div", { className: "space-y-2", children: rows.map((row, i) => {
4051
+ const key = rowKey(row);
4052
+ const isCreatingThis = key !== null && creatingKeys.has(key);
4053
+ const errMsg = key !== null ? rowErrors[key] : void 0;
4054
+ return /* @__PURE__ */ jsxs13(
4055
+ "div",
4056
+ {
4057
+ "data-testid": `${testIdPrefix}-row-${i}`,
4058
+ className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
4059
+ children: [
4060
+ renderCell("origin", i, row.originId),
4061
+ /* @__PURE__ */ jsxs13("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4062
+ !row.type ? /* @__PURE__ */ jsx17("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ jsx17(
4063
+ "span",
4064
+ {
4065
+ "data-testid": `${testIdPrefix}-type-${i}`,
4066
+ className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
4067
+ children: TYPE_LABEL[row.type]
4068
+ }
4069
+ ) : audioEffectsEnabled ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4070
+ /* @__PURE__ */ jsx17(
4071
+ "select",
4072
+ {
4073
+ "data-testid": `${testIdPrefix}-effect-${i}`,
4074
+ value: rowEffects[row.originId ?? row.targetId] ?? "fade",
4075
+ onChange: (e) => {
4076
+ const id = row.originId ?? row.targetId;
4077
+ if (id) setRowEffect(id, e.target.value);
4078
+ },
4079
+ className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
4080
+ children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx17("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4081
+ }
4082
+ ),
4083
+ /* @__PURE__ */ jsx17("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4084
+ ] }) : /* @__PURE__ */ jsx17(
4085
+ "span",
4086
+ {
4087
+ "data-testid": `${testIdPrefix}-type-${i}`,
4088
+ className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
4089
+ children: TYPE_LABEL[row.type]
4090
+ }
4091
+ ),
4092
+ isCreatingThis ? /* @__PURE__ */ jsx17("div", { className: "w-full", children: /* @__PURE__ */ jsx17(
4093
+ SorceryProgressBar,
4094
+ {
4095
+ isLoading: true,
4096
+ heightClass: "h-5",
4097
+ statusText: "CREATING",
4098
+ estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
4099
+ }
4100
+ ) }) : /* @__PURE__ */ jsx17(
4101
+ "button",
4102
+ {
4103
+ type: "button",
4104
+ "data-testid": `${testIdPrefix}-create-${i}`,
4105
+ onClick: () => createRow(row),
4106
+ disabled: !row.type,
4107
+ className: `w-full px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${row.type ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
4108
+ children: "Create"
4109
+ }
4110
+ ),
4111
+ errMsg && /* @__PURE__ */ jsx17(
4112
+ "span",
4113
+ {
4114
+ "data-testid": `${testIdPrefix}-row-error-${i}`,
4115
+ className: "text-[10px] text-sas-danger text-center leading-tight",
4116
+ children: errMsg
4117
+ }
4118
+ )
4119
+ ] }),
4120
+ renderCell("target", i, row.targetId)
4121
+ ]
4122
+ },
4123
+ i
4124
+ );
4125
+ }) }))
4126
+ ] });
4127
+ }
4128
+
4129
+ // src/components/DownloadPackButton.tsx
4130
+ import { useCallback as useCallback9, useEffect as useEffect10, useState as useState12 } from "react";
4131
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
3419
4132
  function formatSize(bytes) {
3420
4133
  if (!bytes || bytes <= 0) return "";
3421
4134
  const gb = bytes / 1024 ** 3;
@@ -3431,10 +4144,10 @@ var DownloadPackButton = ({
3431
4144
  variant = "compact",
3432
4145
  onDownloadComplete
3433
4146
  }) => {
3434
- const [status, setStatus] = useState10("idle");
3435
- const [progress, setProgress] = useState10(0);
3436
- const [errorMessage, setErrorMessage] = useState10(null);
3437
- useEffect9(() => {
4147
+ const [status, setStatus] = useState12("idle");
4148
+ const [progress, setProgress] = useState12(0);
4149
+ const [errorMessage, setErrorMessage] = useState12(null);
4150
+ useEffect10(() => {
3438
4151
  const unsub = host.onSamplePackProgress(packId, (p) => {
3439
4152
  setStatus(p.status);
3440
4153
  setProgress(p.progress);
@@ -3449,7 +4162,7 @@ var DownloadPackButton = ({
3449
4162
  });
3450
4163
  return unsub;
3451
4164
  }, [host, packId, onDownloadComplete]);
3452
- const handleClick = useCallback7(async () => {
4165
+ const handleClick = useCallback9(async () => {
3453
4166
  if (status !== "idle" && status !== "error") return;
3454
4167
  try {
3455
4168
  setStatus("downloading");
@@ -3503,8 +4216,8 @@ var DownloadPackButton = ({
3503
4216
  } else {
3504
4217
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
3505
4218
  }
3506
- return /* @__PURE__ */ jsxs13("div", { children: [
3507
- /* @__PURE__ */ jsx17(
4219
+ return /* @__PURE__ */ jsxs14("div", { children: [
4220
+ /* @__PURE__ */ jsx18(
3508
4221
  "button",
3509
4222
  {
3510
4223
  "data-testid": `download-pack-button-${packId}`,
@@ -3515,12 +4228,12 @@ var DownloadPackButton = ({
3515
4228
  children: buttonLabel
3516
4229
  }
3517
4230
  ),
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 })
4231
+ variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
3519
4232
  ] });
3520
4233
  };
3521
4234
 
3522
4235
  // src/components/SamplePackCTACard.tsx
3523
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
4236
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
3524
4237
  var SamplePackCTACard = ({
3525
4238
  host,
3526
4239
  pack,
@@ -3528,7 +4241,7 @@ var SamplePackCTACard = ({
3528
4241
  onDownloadComplete
3529
4242
  }) => {
3530
4243
  if (status === "checking") {
3531
- return /* @__PURE__ */ jsx18(
4244
+ return /* @__PURE__ */ jsx19(
3532
4245
  "div",
3533
4246
  {
3534
4247
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -3539,16 +4252,16 @@ var SamplePackCTACard = ({
3539
4252
  }
3540
4253
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
3541
4254
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
3542
- return /* @__PURE__ */ jsxs14(
4255
+ return /* @__PURE__ */ jsxs15(
3543
4256
  "div",
3544
4257
  {
3545
4258
  "data-testid": `sample-pack-cta-${pack.packId}`,
3546
4259
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
3547
4260
  children: [
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(
4261
+ /* @__PURE__ */ jsx19("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
4262
+ /* @__PURE__ */ jsx19("div", { className: "text-base text-sas-text mb-1", children: headline }),
4263
+ /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
4264
+ /* @__PURE__ */ jsx19(
3552
4265
  DownloadPackButton,
3553
4266
  {
3554
4267
  host,
@@ -3565,7 +4278,7 @@ var SamplePackCTACard = ({
3565
4278
  };
3566
4279
 
3567
4280
  // src/components/WaveformView.tsx
3568
- import { useEffect as useEffect10, useRef as useRef9, useState as useState11 } from "react";
4281
+ import { useEffect as useEffect11, useRef as useRef11, useState as useState13 } from "react";
3569
4282
 
3570
4283
  // src/components/waveform.ts
3571
4284
  function computePeaks(audioBuffer, bins, targetSamples) {
@@ -3628,7 +4341,7 @@ function drawWaveform(canvas, peaks, options = {}) {
3628
4341
  }
3629
4342
 
3630
4343
  // src/components/WaveformView.tsx
3631
- import { jsx as jsx19 } from "react/jsx-runtime";
4344
+ import { jsx as jsx20 } from "react/jsx-runtime";
3632
4345
  var WaveformView = ({
3633
4346
  host,
3634
4347
  filePath,
@@ -3637,9 +4350,9 @@ var WaveformView = ({
3637
4350
  fillStyle,
3638
4351
  targetSamples
3639
4352
  }) => {
3640
- const canvasRef = useRef9(null);
3641
- const [peaks, setPeaks] = useState11(null);
3642
- useEffect10(() => {
4353
+ const canvasRef = useRef11(null);
4354
+ const [peaks, setPeaks] = useState13(null);
4355
+ useEffect11(() => {
3643
4356
  let cancelled = false;
3644
4357
  let audioContext = null;
3645
4358
  (async () => {
@@ -3665,7 +4378,7 @@ var WaveformView = ({
3665
4378
  cancelled = true;
3666
4379
  };
3667
4380
  }, [host, filePath, bins, targetSamples]);
3668
- useEffect10(() => {
4381
+ useEffect11(() => {
3669
4382
  if (!peaks) return;
3670
4383
  const canvas = canvasRef.current;
3671
4384
  if (!canvas) return;
@@ -3676,7 +4389,7 @@ var WaveformView = ({
3676
4389
  observer.observe(canvas);
3677
4390
  return () => observer.disconnect();
3678
4391
  }, [peaks, fillStyle]);
3679
- return /* @__PURE__ */ jsx19(
4392
+ return /* @__PURE__ */ jsx20(
3680
4393
  "canvas",
3681
4394
  {
3682
4395
  ref: canvasRef,
@@ -3687,8 +4400,8 @@ var WaveformView = ({
3687
4400
  };
3688
4401
 
3689
4402
  // src/components/ScrollingWaveform.tsx
3690
- import { useEffect as useEffect11, useRef as useRef10 } from "react";
3691
- import { jsx as jsx20 } from "react/jsx-runtime";
4403
+ import { useEffect as useEffect12, useRef as useRef12 } from "react";
4404
+ import { jsx as jsx21 } from "react/jsx-runtime";
3692
4405
  var ScrollingWaveform = ({
3693
4406
  getPeakDb,
3694
4407
  active,
@@ -3696,11 +4409,11 @@ var ScrollingWaveform = ({
3696
4409
  className,
3697
4410
  fillStyle
3698
4411
  }) => {
3699
- const canvasRef = useRef10(null);
3700
- const ringRef = useRef10(new Float32Array(columns));
3701
- const writeIdxRef = useRef10(0);
3702
- const rafRef = useRef10(null);
3703
- useEffect11(() => {
4412
+ const canvasRef = useRef12(null);
4413
+ const ringRef = useRef12(new Float32Array(columns));
4414
+ const writeIdxRef = useRef12(0);
4415
+ const rafRef = useRef12(null);
4416
+ useEffect12(() => {
3704
4417
  if (ringRef.current.length !== columns) {
3705
4418
  const next = new Float32Array(columns);
3706
4419
  const prev = ringRef.current;
@@ -3712,7 +4425,7 @@ var ScrollingWaveform = ({
3712
4425
  writeIdxRef.current = writeIdxRef.current % columns;
3713
4426
  }
3714
4427
  }, [columns]);
3715
- useEffect11(() => {
4428
+ useEffect12(() => {
3716
4429
  if (!active) {
3717
4430
  if (rafRef.current !== null) {
3718
4431
  cancelAnimationFrame(rafRef.current);
@@ -3764,7 +4477,7 @@ var ScrollingWaveform = ({
3764
4477
  }
3765
4478
  };
3766
4479
  }, [active, getPeakDb, fillStyle]);
3767
- return /* @__PURE__ */ jsx20(
4480
+ return /* @__PURE__ */ jsx21(
3768
4481
  "canvas",
3769
4482
  {
3770
4483
  ref: canvasRef,
@@ -3775,8 +4488,8 @@ var ScrollingWaveform = ({
3775
4488
  };
3776
4489
 
3777
4490
  // src/components/OffsetScrubber.tsx
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";
4491
+ import { useCallback as useCallback10, useEffect as useEffect13, useMemo as useMemo6, useRef as useRef13, useState as useState14 } from "react";
4492
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
3780
4493
  var SLIDER_HEIGHT_PX = 28;
3781
4494
  var TICK_HEIGHT_PX = 14;
3782
4495
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -3789,40 +4502,40 @@ function OffsetScrubber({
3789
4502
  onChange,
3790
4503
  disabled = false
3791
4504
  }) {
3792
- const trackRef = useRef11(null);
3793
- const [draftOffset, setDraftOffset] = useState12(offsetSamples);
3794
- const [isDragging, setIsDragging] = useState12(false);
3795
- useEffect12(() => {
4505
+ const trackRef = useRef13(null);
4506
+ const [draftOffset, setDraftOffset] = useState14(offsetSamples);
4507
+ const [isDragging, setIsDragging] = useState14(false);
4508
+ useEffect13(() => {
3796
4509
  if (!isDragging) setDraftOffset(offsetSamples);
3797
4510
  }, [offsetSamples, isDragging]);
3798
4511
  const sampleRate = cuePoints?.sample_rate ?? 44100;
3799
4512
  const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
3800
- const beatsForRange = useMemo5(() => {
4513
+ const beatsForRange = useMemo6(() => {
3801
4514
  return Math.round(60 / projectBpm * sampleRate);
3802
4515
  }, [projectBpm, sampleRate]);
3803
4516
  const rangeSamples = beatsForRange * meter;
3804
- const sampleToFraction = useCallback8(
4517
+ const sampleToFraction = useCallback10(
3805
4518
  (sample) => {
3806
4519
  const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
3807
4520
  return (clamped + rangeSamples) / (2 * rangeSamples);
3808
4521
  },
3809
4522
  [rangeSamples]
3810
4523
  );
3811
- const fractionToSample = useCallback8(
4524
+ const fractionToSample = useCallback10(
3812
4525
  (fraction) => {
3813
4526
  const clamped = Math.max(0, Math.min(1, fraction));
3814
4527
  return Math.round(clamped * 2 * rangeSamples - rangeSamples);
3815
4528
  },
3816
4529
  [rangeSamples]
3817
4530
  );
3818
- const snapTargets = useMemo5(() => {
4531
+ const snapTargets = useMemo6(() => {
3819
4532
  if (!cuePoints || cuePoints.beats.length === 0) return [];
3820
4533
  const downbeat = cuePoints.beats[0];
3821
4534
  const positives = cuePoints.beats.map((b) => b - downbeat);
3822
4535
  const negatives = positives.slice(1).map((p) => -p);
3823
4536
  return [...negatives, ...positives].sort((a, b) => a - b);
3824
4537
  }, [cuePoints]);
3825
- const snapToBeat = useCallback8(
4538
+ const snapToBeat = useCallback10(
3826
4539
  (sample) => {
3827
4540
  if (snapTargets.length === 0) return sample;
3828
4541
  let best = snapTargets[0];
@@ -3838,7 +4551,7 @@ function OffsetScrubber({
3838
4551
  },
3839
4552
  [snapTargets]
3840
4553
  );
3841
- const handlePointerDown = useCallback8(
4554
+ const handlePointerDown = useCallback10(
3842
4555
  (e) => {
3843
4556
  if (disabled || !cuePoints) return;
3844
4557
  e.preventDefault();
@@ -3872,7 +4585,7 @@ function OffsetScrubber({
3872
4585
  },
3873
4586
  [disabled, cuePoints, fractionToSample, onChange, snapToBeat]
3874
4587
  );
3875
- const handleResetToZero = useCallback8(() => {
4588
+ const handleResetToZero = useCallback10(() => {
3876
4589
  if (disabled) return;
3877
4590
  setDraftOffset(0);
3878
4591
  onChange(0);
@@ -3880,7 +4593,7 @@ function OffsetScrubber({
3880
4593
  const thumbFraction = sampleToFraction(draftOffset);
3881
4594
  const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
3882
4595
  const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
3883
- const ticks = useMemo5(() => {
4596
+ const ticks = useMemo6(() => {
3884
4597
  if (!cuePoints) return [];
3885
4598
  const downbeat = cuePoints.beats[0] ?? 0;
3886
4599
  return cuePoints.beats.map((b, i) => {
@@ -3891,9 +4604,9 @@ function OffsetScrubber({
3891
4604
  });
3892
4605
  }, [cuePoints, sampleToFraction]);
3893
4606
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
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(
4607
+ return /* @__PURE__ */ jsxs16("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
4608
+ /* @__PURE__ */ jsx22("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
4609
+ /* @__PURE__ */ jsxs16(
3897
4610
  "div",
3898
4611
  {
3899
4612
  ref: trackRef,
@@ -3909,7 +4622,7 @@ function OffsetScrubber({
3909
4622
  "aria-valuenow": draftOffset,
3910
4623
  "aria-disabled": isDisabled,
3911
4624
  children: [
3912
- /* @__PURE__ */ jsx21(
4625
+ /* @__PURE__ */ jsx22(
3913
4626
  "div",
3914
4627
  {
3915
4628
  "aria-hidden": "true",
@@ -3917,7 +4630,7 @@ function OffsetScrubber({
3917
4630
  style: { left: "50%" }
3918
4631
  }
3919
4632
  ),
3920
- ticks.map((t) => /* @__PURE__ */ jsx21(
4633
+ ticks.map((t) => /* @__PURE__ */ jsx22(
3921
4634
  "div",
3922
4635
  {
3923
4636
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -3932,7 +4645,7 @@ function OffsetScrubber({
3932
4645
  },
3933
4646
  t.i
3934
4647
  )),
3935
- /* @__PURE__ */ jsx21(
4648
+ /* @__PURE__ */ jsx22(
3936
4649
  "div",
3937
4650
  {
3938
4651
  "data-testid": "offset-scrubber-thumb",
@@ -3949,7 +4662,7 @@ function OffsetScrubber({
3949
4662
  ]
3950
4663
  }
3951
4664
  ),
3952
- /* @__PURE__ */ jsx21(
4665
+ /* @__PURE__ */ jsx22(
3953
4666
  "span",
3954
4667
  {
3955
4668
  "data-testid": "offset-scrubber-readout",
@@ -3957,7 +4670,7 @@ function OffsetScrubber({
3957
4670
  children: formatOffset(draftOffset, sampleRate)
3958
4671
  }
3959
4672
  ),
3960
- /* @__PURE__ */ jsx21(
4673
+ /* @__PURE__ */ jsx22(
3961
4674
  "button",
3962
4675
  {
3963
4676
  type: "button",
@@ -3969,7 +4682,7 @@ function OffsetScrubber({
3969
4682
  children: "\u2316"
3970
4683
  }
3971
4684
  ),
3972
- bpmMismatch && /* @__PURE__ */ jsx21(
4685
+ bpmMismatch && /* @__PURE__ */ jsx22(
3973
4686
  "span",
3974
4687
  {
3975
4688
  "data-testid": "offset-bpm-mismatch",
@@ -4041,13 +4754,13 @@ function synthesizeCuePoints({
4041
4754
  }
4042
4755
 
4043
4756
  // src/hooks/useSceneState.ts
4044
- import { useState as useState13, useCallback as useCallback9, useRef as useRef12 } from "react";
4757
+ import { useState as useState15, useCallback as useCallback11, useRef as useRef14 } from "react";
4045
4758
  function useSceneState(activeSceneId, initialValue) {
4046
- const [stateMap, setStateMap] = useState13(() => /* @__PURE__ */ new Map());
4047
- const activeSceneIdRef = useRef12(activeSceneId);
4759
+ const [stateMap, setStateMap] = useState15(() => /* @__PURE__ */ new Map());
4760
+ const activeSceneIdRef = useRef14(activeSceneId);
4048
4761
  activeSceneIdRef.current = activeSceneId;
4049
4762
  const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
4050
- const setForCurrentScene = useCallback9((value) => {
4763
+ const setForCurrentScene = useCallback11((value) => {
4051
4764
  const sid = activeSceneIdRef.current;
4052
4765
  if (sid === null) return;
4053
4766
  setStateMap((prev) => {
@@ -4058,7 +4771,7 @@ function useSceneState(activeSceneId, initialValue) {
4058
4771
  return newMap;
4059
4772
  });
4060
4773
  }, [initialValue]);
4061
- const setForScene = useCallback9((sceneId, value) => {
4774
+ const setForScene = useCallback11((sceneId, value) => {
4062
4775
  setStateMap((prev) => {
4063
4776
  const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
4064
4777
  const next = typeof value === "function" ? value(current) : value;
@@ -4071,10 +4784,10 @@ function useSceneState(activeSceneId, initialValue) {
4071
4784
  }
4072
4785
 
4073
4786
  // src/hooks/useAnySolo.ts
4074
- import { useEffect as useEffect13, useState as useState14 } from "react";
4787
+ import { useEffect as useEffect14, useState as useState16 } from "react";
4075
4788
  function useAnySolo(host) {
4076
- const [anySolo, setAnySolo] = useState14(false);
4077
- useEffect13(() => {
4789
+ const [anySolo, setAnySolo] = useState16(false);
4790
+ useEffect14(() => {
4078
4791
  let active = true;
4079
4792
  const refresh = () => {
4080
4793
  host.isAnySoloActive().then((v) => {
@@ -4093,7 +4806,7 @@ function useAnySolo(host) {
4093
4806
  }
4094
4807
 
4095
4808
  // src/hooks/useSoundHistory.ts
4096
- import { useCallback as useCallback10, useMemo as useMemo6, useRef as useRef13, useState as useState15 } from "react";
4809
+ import { useCallback as useCallback12, useMemo as useMemo7, useRef as useRef15, useState as useState17 } from "react";
4097
4810
  var EMPTY = { entries: [], cursor: -1 };
4098
4811
  function sameDescriptor(a, b) {
4099
4812
  if (a === b) return true;
@@ -4105,14 +4818,14 @@ function sameDescriptor(a, b) {
4105
4818
  }
4106
4819
  function useSoundHistory(applySound, opts = {}) {
4107
4820
  const max = Math.max(2, opts.max ?? 24);
4108
- const applyRef = useRef13(applySound);
4821
+ const applyRef = useRef15(applySound);
4109
4822
  applyRef.current = applySound;
4110
- const onChangeRef = useRef13(opts.onChange);
4823
+ const onChangeRef = useRef15(opts.onChange);
4111
4824
  onChangeRef.current = opts.onChange;
4112
- const dataRef = useRef13({});
4113
- const [, setVersion] = useState15(0);
4114
- const bump = useCallback10(() => setVersion((v) => v + 1), []);
4115
- const commit = useCallback10(
4825
+ const dataRef = useRef15({});
4826
+ const [, setVersion] = useState17(0);
4827
+ const bump = useCallback12(() => setVersion((v) => v + 1), []);
4828
+ const commit = useCallback12(
4116
4829
  (trackId, next, notify) => {
4117
4830
  dataRef.current = { ...dataRef.current, [trackId]: next };
4118
4831
  bump();
@@ -4120,7 +4833,7 @@ function useSoundHistory(applySound, opts = {}) {
4120
4833
  },
4121
4834
  [bump]
4122
4835
  );
4123
- const record = useCallback10(
4836
+ const record = useCallback12(
4124
4837
  (trackId, descriptor, label) => {
4125
4838
  const h = dataRef.current[trackId];
4126
4839
  const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
@@ -4135,7 +4848,7 @@ function useSoundHistory(applySound, opts = {}) {
4135
4848
  },
4136
4849
  [max, commit]
4137
4850
  );
4138
- const restoreTo = useCallback10(
4851
+ const restoreTo = useCallback12(
4139
4852
  async (trackId, index) => {
4140
4853
  const h = dataRef.current[trackId];
4141
4854
  if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
@@ -4145,7 +4858,7 @@ function useSoundHistory(applySound, opts = {}) {
4145
4858
  },
4146
4859
  [commit]
4147
4860
  );
4148
- const undo = useCallback10(
4861
+ const undo = useCallback12(
4149
4862
  (trackId) => {
4150
4863
  const h = dataRef.current[trackId];
4151
4864
  if (!h || h.cursor <= 0) return Promise.resolve(false);
@@ -4153,7 +4866,7 @@ function useSoundHistory(applySound, opts = {}) {
4153
4866
  },
4154
4867
  [restoreTo]
4155
4868
  );
4156
- const toggleFavorite = useCallback10(
4869
+ const toggleFavorite = useCallback12(
4157
4870
  (trackId, index) => {
4158
4871
  const h = dataRef.current[trackId];
4159
4872
  if (!h || index < 0 || index >= h.entries.length) return;
@@ -4162,7 +4875,7 @@ function useSoundHistory(applySound, opts = {}) {
4162
4875
  },
4163
4876
  [commit]
4164
4877
  );
4165
- const restore = useCallback10(
4878
+ const restore = useCallback12(
4166
4879
  (trackId, state) => {
4167
4880
  const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
4168
4881
  const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
@@ -4171,15 +4884,15 @@ function useSoundHistory(applySound, opts = {}) {
4171
4884
  },
4172
4885
  [commit]
4173
4886
  );
4174
- const list = useCallback10(
4887
+ const list = useCallback12(
4175
4888
  (trackId) => dataRef.current[trackId] ?? EMPTY,
4176
4889
  []
4177
4890
  );
4178
- const canUndo = useCallback10((trackId) => {
4891
+ const canUndo = useCallback12((trackId) => {
4179
4892
  const h = dataRef.current[trackId];
4180
4893
  return !!h && h.cursor > 0;
4181
4894
  }, []);
4182
- const clear = useCallback10(
4895
+ const clear = useCallback12(
4183
4896
  (trackId) => {
4184
4897
  if (dataRef.current[trackId]) {
4185
4898
  const next = { ...dataRef.current };
@@ -4191,102 +4904,18 @@ function useSoundHistory(applySound, opts = {}) {
4191
4904
  },
4192
4905
  [bump]
4193
4906
  );
4194
- const reset = useCallback10(() => {
4907
+ const reset = useCallback12(() => {
4195
4908
  dataRef.current = {};
4196
4909
  bump();
4197
4910
  }, [bump]);
4198
- return useMemo6(
4911
+ return useMemo7(
4199
4912
  () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
4200
4913
  [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
4201
4914
  );
4202
4915
  }
4203
4916
 
4204
- // src/hooks/useTrackReorder.ts
4205
- import { useCallback as useCallback11, useRef as useRef14, useState as useState16 } from "react";
4206
- function moveItem(arr, from, to) {
4207
- const next = arr.slice();
4208
- if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
4209
- return next;
4210
- }
4211
- const [moved] = next.splice(from, 1);
4212
- next.splice(to, 0, moved);
4213
- return next;
4214
- }
4215
- function useTrackReorder({
4216
- host,
4217
- items,
4218
- setItems,
4219
- getId,
4220
- onError
4221
- }) {
4222
- const [draggingIndex, setDraggingIndex] = useState16(null);
4223
- const [dragOverIndex, setDragOverIndex] = useState16(null);
4224
- const fromRef = useRef14(null);
4225
- const itemsRef = useRef14(items);
4226
- itemsRef.current = items;
4227
- const dragPropsFor = useCallback11(
4228
- (index) => ({
4229
- handleProps: {
4230
- draggable: true,
4231
- onDragStart: (e) => {
4232
- fromRef.current = index;
4233
- setDraggingIndex(index);
4234
- if (e.dataTransfer) {
4235
- e.dataTransfer.effectAllowed = "move";
4236
- try {
4237
- e.dataTransfer.setData("text/plain", String(index));
4238
- } catch {
4239
- }
4240
- }
4241
- },
4242
- onDragEnd: () => {
4243
- fromRef.current = null;
4244
- setDraggingIndex(null);
4245
- setDragOverIndex(null);
4246
- }
4247
- },
4248
- rowProps: {
4249
- onDragEnter: (e) => {
4250
- if (fromRef.current === null) return;
4251
- e.preventDefault();
4252
- setDragOverIndex(index);
4253
- },
4254
- onDragOver: (e) => {
4255
- if (fromRef.current === null) return;
4256
- e.preventDefault();
4257
- if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
4258
- setDragOverIndex((cur) => cur === index ? cur : index);
4259
- },
4260
- onDragLeave: () => {
4261
- setDragOverIndex((cur) => cur === index ? null : cur);
4262
- },
4263
- onDrop: (e) => {
4264
- e.preventDefault();
4265
- const from = fromRef.current;
4266
- fromRef.current = null;
4267
- setDraggingIndex(null);
4268
- setDragOverIndex(null);
4269
- if (from === null || from === index) return;
4270
- const prev = itemsRef.current;
4271
- const next = moveItem(prev, from, index);
4272
- setItems(next);
4273
- const ids = next.map(getId);
4274
- Promise.resolve(host.reorderTracks(ids)).catch((err) => {
4275
- setItems(prev);
4276
- onError?.(err);
4277
- });
4278
- }
4279
- },
4280
- isDragging: draggingIndex === index,
4281
- isDragTarget: dragOverIndex === index && draggingIndex !== index
4282
- }),
4283
- [host, setItems, getId, onError, draggingIndex, dragOverIndex]
4284
- );
4285
- return { dragPropsFor, draggingIndex, dragOverIndex };
4286
- }
4287
-
4288
4917
  // src/constants/sdk-version.ts
4289
- var PLUGIN_SDK_VERSION = "2.28.0";
4918
+ var PLUGIN_SDK_VERSION = "2.34.0";
4290
4919
 
4291
4920
  // src/utils/format-concurrent-tracks.ts
4292
4921
  function formatConcurrentTracks(ctx) {
@@ -4429,6 +5058,8 @@ function pickTopKWeighted(scored, options = {}) {
4429
5058
  return top[top.length - 1].item;
4430
5059
  }
4431
5060
  export {
5061
+ AUDIO_EFFECTS,
5062
+ AUDIO_EFFECT_LABEL,
4432
5063
  ConfirmDialog,
4433
5064
  CrossfadeModal,
4434
5065
  CrossfadeTrackRow,
@@ -4467,34 +5098,49 @@ export {
4467
5098
  ScrollingWaveform,
4468
5099
  SorceryProgressBar,
4469
5100
  TEXTURAL_ROLES,
5101
+ TRANSITION_DESIGNER_DRAFT_KEY,
4470
5102
  TrackDrawer,
4471
5103
  TrackMeterStrip,
4472
5104
  TrackRow,
5105
+ TransitionDesigner,
4473
5106
  VolumeSlider,
4474
5107
  WaveformView,
4475
5108
  analyzeWavPeak,
5109
+ asAudioEffect,
4476
5110
  asCrossfadeMeta,
4477
5111
  asFadeMeta,
5112
+ asTransitionDesignerDraft,
4478
5113
  buildCrossfadeInpaintPrompt,
4479
5114
  buildCrossfadeVolumeCurves,
4480
5115
  buildFadeVolumeCurve,
5116
+ buildRowSlots,
4481
5117
  calculateTimeBasedTarget,
4482
5118
  cellToPx,
4483
5119
  centerScrollTop,
4484
5120
  computePeaks,
5121
+ dbIdsFromKeys,
4485
5122
  dbToSlider,
4486
5123
  defaultFadeGesture,
4487
5124
  drawWaveform,
4488
5125
  formatConcurrentTracks,
5126
+ hashString,
4489
5127
  moveItem,
5128
+ normalizeSlots,
5129
+ padPair,
5130
+ padSlots,
4490
5131
  parseCrossfadePairs,
4491
5132
  parseFades,
4492
5133
  pickTopKWeighted,
4493
5134
  pitchToName,
4494
5135
  pxToCell,
5136
+ reconcileSlots,
4495
5137
  resizeNoteDuration,
5138
+ rowKey,
5139
+ rowType,
4496
5140
  scorePromptMatch,
4497
5141
  sliderToDb,
5142
+ slotsEqual,
5143
+ soundIdentity,
4498
5144
  synthesizeCuePoints,
4499
5145
  tokenizePrompt,
4500
5146
  transposeNotes,