@signalsandsorcery/plugin-sdk 2.35.8 → 2.36.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
@@ -443,25 +443,31 @@ function clamp(v, lo, hi) {
443
443
  function snapLabel(s) {
444
444
  return SNAP_LABELS[String(s)] ?? `${s}`;
445
445
  }
446
+ function effectivePxPerBeat(containerWidth, totalBeats) {
447
+ if (totalBeats <= 0) return PX_PER_BEAT;
448
+ const available = containerWidth - GUTTER_W;
449
+ if (available <= 0) return PX_PER_BEAT;
450
+ return Math.max(PX_PER_BEAT, available / totalBeats);
451
+ }
446
452
  function pitchToName(pitch) {
447
453
  const name = NOTE_NAMES[(pitch % 12 + 12) % 12];
448
454
  const octave = Math.floor(pitch / 12) - 1;
449
455
  return `${name}${octave}`;
450
456
  }
451
- function cellToPx(pitch, startBeat, hi) {
452
- return { left: startBeat * PX_PER_BEAT, top: (hi - pitch) * ROW_HEIGHT };
457
+ function cellToPx(pitch, startBeat, hi, pxPerBeat = PX_PER_BEAT) {
458
+ return { left: startBeat * pxPerBeat, top: (hi - pitch) * ROW_HEIGHT };
453
459
  }
454
- function pxToCell(localX, localY, hi, snap, bars, beatsPerBar) {
460
+ function pxToCell(localX, localY, hi, snap, bars, beatsPerBar, pxPerBeat = PX_PER_BEAT) {
455
461
  const totalBeats = bars * beatsPerBar;
456
462
  const pitch = clamp(hi - Math.floor(localY / ROW_HEIGHT), 0, 127);
457
- const rawBeat = localX / PX_PER_BEAT;
463
+ const rawBeat = localX / pxPerBeat;
458
464
  const snapped = Math.round(rawBeat / snap) * snap;
459
465
  const startBeat = clamp(snapped, 0, Math.max(0, totalBeats - snap));
460
466
  return { pitch, startBeat };
461
467
  }
462
- function resizeNoteDuration(startBeat, localX, snap, bars, beatsPerBar) {
468
+ function resizeNoteDuration(startBeat, localX, snap, bars, beatsPerBar, pxPerBeat = PX_PER_BEAT) {
463
469
  const totalBeats = bars * beatsPerBar;
464
- const snappedEnd = Math.round(localX / PX_PER_BEAT / snap) * snap;
470
+ const snappedEnd = Math.round(localX / pxPerBeat / snap) * snap;
465
471
  const end = clamp(snappedEnd, startBeat + snap, totalBeats);
466
472
  return end - startBeat;
467
473
  }
@@ -512,7 +518,19 @@ function PianoRollEditor({
512
518
  }, [autoFit, notes, minPitch, maxPitch]);
513
519
  const rowCount = hi - lo + 1;
514
520
  const totalBeats = bars * beatsPerBar;
515
- const gridWidth = totalBeats * PX_PER_BEAT;
521
+ const [containerW, setContainerW] = useState(0);
522
+ useLayoutEffect(() => {
523
+ const el = scrollRef.current;
524
+ if (!el) return;
525
+ const measure = () => setContainerW(el.clientWidth);
526
+ measure();
527
+ if (typeof ResizeObserver === "undefined") return void 0;
528
+ const ro = new ResizeObserver(measure);
529
+ ro.observe(el);
530
+ return () => ro.disconnect();
531
+ }, []);
532
+ const pxPerBeat = effectivePxPerBeat(containerW, totalBeats);
533
+ const gridWidth = totalBeats * pxPerBeat;
516
534
  const gridHeight = rowCount * ROW_HEIGHT;
517
535
  const stateRef = useRef({
518
536
  notes,
@@ -524,7 +542,8 @@ function PianoRollEditor({
524
542
  defaultVelocity,
525
543
  bpm,
526
544
  onAuditionNote,
527
- disabled
545
+ disabled,
546
+ pxPerBeat
528
547
  });
529
548
  stateRef.current = {
530
549
  notes,
@@ -536,7 +555,8 @@ function PianoRollEditor({
536
555
  defaultVelocity,
537
556
  bpm,
538
557
  onAuditionNote,
539
- disabled
558
+ disabled,
559
+ pxPerBeat
540
560
  };
541
561
  const localCoords = useCallback((clientX, clientY) => {
542
562
  const rect = gridRef.current?.getBoundingClientRect();
@@ -572,14 +592,14 @@ function PianoRollEditor({
572
592
  if (drag.mode === "resize") {
573
593
  const note = s.notes[drag.index];
574
594
  if (!note) return;
575
- const durationBeats = resizeNoteDuration(note.startBeat, x, s.snapState, s.bars, s.beatsPerBar);
595
+ const durationBeats = resizeNoteDuration(note.startBeat, x, s.snapState, s.bars, s.beatsPerBar, s.pxPerBeat);
576
596
  if (durationBeats === note.durationBeats) return;
577
597
  const next2 = s.notes.map((n, i) => i === drag.index ? { ...n, durationBeats } : n);
578
598
  s.onChange(next2);
579
599
  return;
580
600
  }
581
601
  if (drag.mode !== "drag") return;
582
- const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);
602
+ const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar, s.pxPerBeat);
583
603
  const next = s.notes.map((n, i) => i === drag.index ? { ...n, pitch, startBeat } : n);
584
604
  s.onChange(next);
585
605
  }, [localCoords]);
@@ -595,7 +615,7 @@ function PianoRollEditor({
595
615
  }
596
616
  if (drag.mode === "pending-add") {
597
617
  const { x, y } = localCoords(e.clientX, e.clientY);
598
- const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);
618
+ const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar, s.pxPerBeat);
599
619
  const note = {
600
620
  pitch,
601
621
  startBeat,
@@ -644,14 +664,14 @@ function PianoRollEditor({
644
664
  return out;
645
665
  }, [hi, lo]);
646
666
  const gridBg = useMemo(() => {
647
- const beatPx = PX_PER_BEAT;
648
- const barPx = PX_PER_BEAT * beatsPerBar;
667
+ const beatPx = pxPerBeat;
668
+ const barPx = pxPerBeat * beatsPerBar;
649
669
  return [
650
670
  `repeating-linear-gradient(to right, transparent 0 ${beatPx - 1}px, rgba(255,255,255,0.06) ${beatPx - 1}px ${beatPx}px)`,
651
671
  `repeating-linear-gradient(to right, transparent 0 ${barPx - 1}px, rgba(255,255,255,0.16) ${barPx - 1}px ${barPx}px)`,
652
672
  `repeating-linear-gradient(to bottom, transparent 0 ${ROW_HEIGHT - 1}px, rgba(255,255,255,0.04) ${ROW_HEIGHT - 1}px ${ROW_HEIGHT}px)`
653
673
  ].join(", ");
654
- }, [beatsPerBar]);
674
+ }, [beatsPerBar, pxPerBeat]);
655
675
  const octaveDisabled = disabled || notes.length === 0;
656
676
  return /* @__PURE__ */ jsxs2("div", { className: `flex flex-col gap-1 ${className ?? ""}`, "data-testid": testId, children: [
657
677
  /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", "data-testid": "sdk-pr-toolbar", children: [
@@ -745,8 +765,8 @@ function PianoRollEditor({
745
765
  onPointerCancel: handlePointerCancel,
746
766
  children: [
747
767
  notes.map((n, i) => {
748
- const { left, top } = cellToPx(n.pitch, n.startBeat, hi);
749
- const width = Math.max(3, n.durationBeats * PX_PER_BEAT);
768
+ const { left, top } = cellToPx(n.pitch, n.startBeat, hi, pxPerBeat);
769
+ const width = Math.max(3, n.durationBeats * pxPerBeat);
750
770
  const handleW = Math.min(RESIZE_HANDLE_PX, width / 2);
751
771
  return /* @__PURE__ */ jsx2(
752
772
  "div",
@@ -2224,6 +2244,71 @@ function SorceryProgressBar({
2224
2244
  );
2225
2245
  }
2226
2246
 
2247
+ // src/panel-core/panel-helpers.ts
2248
+ function promptEnterToGenerate(generate, disabled = false) {
2249
+ return (e) => {
2250
+ if (e.key === "Enter" && !e.shiftKey && !disabled) {
2251
+ e.preventDefault();
2252
+ generate();
2253
+ }
2254
+ };
2255
+ }
2256
+ function trackDataKey(dbId, suffix) {
2257
+ return `track:${dbId}:${suffix}`;
2258
+ }
2259
+ function pluginFxToToggleFx(sdkState) {
2260
+ const result = { ...EMPTY_FX_DETAIL_STATE };
2261
+ for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
2262
+ const sdkCat = sdkState[category];
2263
+ if (sdkCat) {
2264
+ result[category] = {
2265
+ enabled: sdkCat.enabled,
2266
+ presetIndex: sdkCat.presetIndex,
2267
+ dryWet: sdkCat.dryWet
2268
+ };
2269
+ }
2270
+ }
2271
+ return result;
2272
+ }
2273
+ function parseLLMNoteResponse(content) {
2274
+ try {
2275
+ let jsonStr = content.trim();
2276
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
2277
+ if (fenceMatch) {
2278
+ jsonStr = fenceMatch[1].trim();
2279
+ }
2280
+ const parsed = JSON.parse(jsonStr);
2281
+ if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
2282
+ return null;
2283
+ }
2284
+ const obj = parsed;
2285
+ if (!Array.isArray(obj.notes)) {
2286
+ return null;
2287
+ }
2288
+ const validNotes = [];
2289
+ for (const raw of obj.notes) {
2290
+ if (typeof raw !== "object" || raw === null) continue;
2291
+ const note = raw;
2292
+ const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
2293
+ const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
2294
+ const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
2295
+ const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
2296
+ if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
2297
+ validNotes.push({
2298
+ pitch: Math.round(pitch),
2299
+ startBeat,
2300
+ durationBeats,
2301
+ velocity: Math.round(velocity)
2302
+ });
2303
+ }
2304
+ }
2305
+ const role = typeof obj.role === "string" ? obj.role : void 0;
2306
+ return { notes: validNotes, role };
2307
+ } catch {
2308
+ return null;
2309
+ }
2310
+ }
2311
+
2227
2312
  // src/components/TrackRow.tsx
2228
2313
  import { Fragment, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2229
2314
  function TrackRow({
@@ -2292,12 +2377,7 @@ function TrackRow({
2292
2377
  );
2293
2378
  const fxTabOpen = drawerOpen && drawerTab === "fx";
2294
2379
  const soundTabOpen = drawerOpen && drawerTab !== "fx";
2295
- const handleKeyDown = (e) => {
2296
- if (e.key === "Enter" && !e.shiftKey && onGenerate) {
2297
- e.preventDefault();
2298
- onGenerate();
2299
- }
2300
- };
2380
+ const handleKeyDown = promptEnterToGenerate(() => onGenerate?.(), !onGenerate);
2301
2381
  const borderColorStyle = needsGeneration ? void 0 : accentColor;
2302
2382
  const borderClass = needsGeneration ? "border-amber-400 animate-pulse" : "border-sas-border";
2303
2383
  return /* @__PURE__ */ jsxs8("div", { "data-testid": "sdk-track-row-wrapper", className: "w-full", ...drag?.rowProps ?? {}, children: [
@@ -5885,63 +5965,6 @@ function newTrackState(handle, overrides = {}) {
5885
5965
  };
5886
5966
  }
5887
5967
 
5888
- // src/panel-core/panel-helpers.ts
5889
- function trackDataKey(dbId, suffix) {
5890
- return `track:${dbId}:${suffix}`;
5891
- }
5892
- function pluginFxToToggleFx(sdkState) {
5893
- const result = { ...EMPTY_FX_DETAIL_STATE };
5894
- for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
5895
- const sdkCat = sdkState[category];
5896
- if (sdkCat) {
5897
- result[category] = {
5898
- enabled: sdkCat.enabled,
5899
- presetIndex: sdkCat.presetIndex,
5900
- dryWet: sdkCat.dryWet
5901
- };
5902
- }
5903
- }
5904
- return result;
5905
- }
5906
- function parseLLMNoteResponse(content) {
5907
- try {
5908
- let jsonStr = content.trim();
5909
- const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
5910
- if (fenceMatch) {
5911
- jsonStr = fenceMatch[1].trim();
5912
- }
5913
- const parsed = JSON.parse(jsonStr);
5914
- if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
5915
- return null;
5916
- }
5917
- const obj = parsed;
5918
- if (!Array.isArray(obj.notes)) {
5919
- return null;
5920
- }
5921
- const validNotes = [];
5922
- for (const raw of obj.notes) {
5923
- if (typeof raw !== "object" || raw === null) continue;
5924
- const note = raw;
5925
- const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
5926
- const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
5927
- const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
5928
- const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
5929
- if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
5930
- validNotes.push({
5931
- pitch: Math.round(pitch),
5932
- startBeat,
5933
- durationBeats,
5934
- velocity: Math.round(velocity)
5935
- });
5936
- }
5937
- }
5938
- const role = typeof obj.role === "string" ? obj.role : void 0;
5939
- return { notes: validNotes, role };
5940
- } catch {
5941
- return null;
5942
- }
5943
- }
5944
-
5945
5968
  // src/panel-core/group-meta.ts
5946
5969
  function parseTrackGroups(sceneData, spec) {
5947
5970
  const pattern = new RegExp(`^track:(.+):${spec.metaKey}$`);
@@ -9124,6 +9147,7 @@ export {
9124
9147
  defaultVoiceSpecs,
9125
9148
  describeViolations,
9126
9149
  drawWaveform,
9150
+ effectivePxPerBeat,
9127
9151
  enforceVoice,
9128
9152
  foldPitchToRegister,
9129
9153
  formatConcurrentTracks,
@@ -9142,6 +9166,7 @@ export {
9142
9166
  pickTopKWeighted,
9143
9167
  pitchToName,
9144
9168
  pluginFxToToggleFx,
9169
+ promptEnterToGenerate,
9145
9170
  pxToCell,
9146
9171
  reconcileSlots,
9147
9172
  resizeNoteDuration,