@audiofab-io/easy-spin-ui 0.1.1 → 0.2.0
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/components/PedalFace.d.ts +5 -1
- package/dist/index.js +74 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -24,6 +24,10 @@ export interface PedalFaceProps {
|
|
|
24
24
|
onProgramSlot?: (slotIndex: number) => void;
|
|
25
25
|
/** Index of the slot currently being written to the pedal, or null. */
|
|
26
26
|
programmingSlot?: number | null;
|
|
27
|
+
/** When set, empty slots show a "+" button that calls this callback —
|
|
28
|
+
* used by hosts that have a notion of a "currently tracked / active"
|
|
29
|
+
* program the user can drop into a slot. */
|
|
30
|
+
onAssignTrackedToSlot?: (slotIndex: number) => void;
|
|
27
31
|
/** Audio clip selection — pass an empty array to hide the selector. */
|
|
28
32
|
clips?: ClipInfo[];
|
|
29
33
|
selectedClipId: string | null;
|
|
@@ -44,4 +48,4 @@ export interface PedalFaceProps {
|
|
|
44
48
|
pedalReading?: boolean;
|
|
45
49
|
pedalWriting?: boolean;
|
|
46
50
|
}
|
|
47
|
-
export declare function PedalFace({ pedalImageUrl, pots, onPotChange, potLabels, selectedSlot, onSelectSlot, slotLabels, onAssignSlot, onUnassignSlot, onUnassignAllSlots, onProgramSlot, programmingSlot, clips, selectedClipId, onSelectClip, playing, onPlay, onPause, bypassed, onToggleBypass, channelMode, onChannelModeChange, pedalConnected, onConnectPedal, onReadPedal, onWritePedal, pedalReading, pedalWriting, }: PedalFaceProps): import("react/jsx-runtime").JSX.Element;
|
|
51
|
+
export declare function PedalFace({ pedalImageUrl, pots, onPotChange, potLabels, selectedSlot, onSelectSlot, slotLabels, onAssignSlot, onUnassignSlot, onUnassignAllSlots, onProgramSlot, programmingSlot, onAssignTrackedToSlot, clips, selectedClipId, onSelectClip, playing, onPlay, onPause, bypassed, onToggleBypass, channelMode, onChannelModeChange, pedalConnected, onConnectPedal, onReadPedal, onWritePedal, pedalReading, pedalWriting, }: PedalFaceProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/index.js
CHANGED
|
@@ -240,8 +240,8 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
240
240
|
"Pot 0",
|
|
241
241
|
"Pot 1",
|
|
242
242
|
"Pot 2"
|
|
243
|
-
], selectedSlot: l, onSelectSlot: u, slotLabels: d, onAssignSlot: f, onUnassignSlot: p, onUnassignAllSlots: m, onProgramSlot: h, programmingSlot: g = null,
|
|
244
|
-
let
|
|
243
|
+
], selectedSlot: l, onSelectSlot: u, slotLabels: d, onAssignSlot: f, onUnassignSlot: p, onUnassignAllSlots: m, onProgramSlot: h, programmingSlot: g = null, onAssignTrackedToSlot: _, clips: T, selectedClipId: P, onSelectClip: F, playing: I, onPlay: L, onPause: R, bypassed: z, onToggleBypass: B, channelMode: V, onChannelModeChange: H, pedalConnected: U = !1, onConnectPedal: W, onReadPedal: G, onWritePedal: K, pedalReading: q = !1, pedalWriting: J = !1 }) {
|
|
244
|
+
let Y = q || J, [X, Z] = r(null), Q = !z, $ = d?.some((e) => e) ?? !1, ee = U && !Y;
|
|
245
245
|
return /* @__PURE__ */ o("div", {
|
|
246
246
|
className: "flex flex-col items-center gap-5 w-full max-w-[460px] mx-auto",
|
|
247
247
|
children: [
|
|
@@ -312,8 +312,8 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
312
312
|
width: `${w}%`,
|
|
313
313
|
aspectRatio: "1 / 1",
|
|
314
314
|
transform: "translate(-50%, -50%)",
|
|
315
|
-
background:
|
|
316
|
-
boxShadow:
|
|
315
|
+
background: Q ? "radial-gradient(circle at 35% 30%, #ff8a8a 0%, #e11 55%, #800 100%)" : "radial-gradient(circle at 35% 30%, #f2f2f2 0%, #bbb 60%, #777 100%)",
|
|
316
|
+
boxShadow: Q ? "0 0 10px rgba(255, 60, 60, 0.75)" : "inset 0 1px 2px rgba(0,0,0,0.25)",
|
|
317
317
|
border: "1px solid #333"
|
|
318
318
|
},
|
|
319
319
|
"aria-label": "Status LED"
|
|
@@ -356,35 +356,35 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
356
356
|
strokeLinejoin: "round"
|
|
357
357
|
})]
|
|
358
358
|
}),
|
|
359
|
-
(
|
|
359
|
+
(W || G || K) && /* @__PURE__ */ a("div", {
|
|
360
360
|
className: "absolute flex items-center gap-1",
|
|
361
361
|
style: {
|
|
362
362
|
left: b.pedalIO.left,
|
|
363
363
|
top: b.pedalIO.top,
|
|
364
364
|
transform: "translate(-50%, -50%)"
|
|
365
365
|
},
|
|
366
|
-
children:
|
|
367
|
-
onClick:
|
|
368
|
-
disabled:
|
|
369
|
-
busy:
|
|
366
|
+
children: U ? /* @__PURE__ */ o(i, { children: [G && /* @__PURE__ */ a(O, {
|
|
367
|
+
onClick: G,
|
|
368
|
+
disabled: Y,
|
|
369
|
+
busy: q,
|
|
370
370
|
label: "Read programs from pedal",
|
|
371
371
|
children: /* @__PURE__ */ a(A, {})
|
|
372
|
-
}),
|
|
373
|
-
onClick:
|
|
374
|
-
disabled:
|
|
375
|
-
busy:
|
|
372
|
+
}), K && /* @__PURE__ */ a(O, {
|
|
373
|
+
onClick: K,
|
|
374
|
+
disabled: Y,
|
|
375
|
+
busy: J,
|
|
376
376
|
label: "Write programs to pedal",
|
|
377
377
|
children: /* @__PURE__ */ a(j, {})
|
|
378
|
-
})] }) :
|
|
378
|
+
})] }) : W && /* @__PURE__ */ a("button", {
|
|
379
379
|
type: "button",
|
|
380
|
-
onClick:
|
|
380
|
+
onClick: W,
|
|
381
381
|
className: "px-2 py-1 rounded border border-border bg-surface-card text-[10px] font-semibold text-text-primary hover:border-gold-dim transition-colors whitespace-nowrap",
|
|
382
382
|
children: "Connect"
|
|
383
383
|
})
|
|
384
384
|
}),
|
|
385
385
|
/* @__PURE__ */ a("button", {
|
|
386
|
-
onClick:
|
|
387
|
-
"aria-label":
|
|
386
|
+
onClick: B,
|
|
387
|
+
"aria-label": z ? "Bypassed — click to engage" : "Engaged — click to bypass",
|
|
388
388
|
className: "absolute rounded-full cursor-pointer",
|
|
389
389
|
style: {
|
|
390
390
|
left: b.footswitch.left,
|
|
@@ -416,14 +416,14 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
416
416
|
className: "flex items-center gap-3 flex-wrap justify-center",
|
|
417
417
|
children: [
|
|
418
418
|
/* @__PURE__ */ a("button", {
|
|
419
|
-
onClick:
|
|
419
|
+
onClick: I ? R : L,
|
|
420
420
|
className: "px-4 py-1.5 rounded border border-border bg-surface-card text-sm font-semibold text-text-primary hover:border-gold-dim transition-colors",
|
|
421
|
-
children:
|
|
421
|
+
children: I ? "Pause" : "Play"
|
|
422
422
|
}),
|
|
423
|
-
|
|
424
|
-
clips:
|
|
425
|
-
selectedClipId:
|
|
426
|
-
onSelect:
|
|
423
|
+
T && T.length > 0 && /* @__PURE__ */ a(y, {
|
|
424
|
+
clips: T,
|
|
425
|
+
selectedClipId: P,
|
|
426
|
+
onSelect: F
|
|
427
427
|
}),
|
|
428
428
|
/* @__PURE__ */ a("div", {
|
|
429
429
|
role: "radiogroup",
|
|
@@ -433,9 +433,9 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
433
433
|
children: ["mono", "stereo"].map((e) => /* @__PURE__ */ a("button", {
|
|
434
434
|
type: "button",
|
|
435
435
|
role: "radio",
|
|
436
|
-
"aria-checked":
|
|
437
|
-
onClick: () =>
|
|
438
|
-
className: `px-3 py-1.5 font-semibold transition-colors ${
|
|
436
|
+
"aria-checked": V === e,
|
|
437
|
+
onClick: () => H(e),
|
|
438
|
+
className: `px-3 py-1.5 font-semibold transition-colors ${V === e ? "bg-gold text-surface" : "text-text-secondary hover:text-text-primary"}`,
|
|
439
439
|
children: e === "mono" ? "Mono" : "Stereo"
|
|
440
440
|
}, e))
|
|
441
441
|
})
|
|
@@ -448,7 +448,7 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
448
448
|
children: [/* @__PURE__ */ a("h3", {
|
|
449
449
|
className: "text-xs font-semibold uppercase tracking-wider text-text-muted",
|
|
450
450
|
children: "Programs on Pedal"
|
|
451
|
-
}),
|
|
451
|
+
}), $ && m && /* @__PURE__ */ a("button", {
|
|
452
452
|
type: "button",
|
|
453
453
|
onClick: m,
|
|
454
454
|
className: "text-[11px] text-text-muted hover:text-red-400 transition-colors",
|
|
@@ -458,7 +458,7 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
458
458
|
}), /* @__PURE__ */ a("div", {
|
|
459
459
|
className: "grid grid-cols-2 gap-2",
|
|
460
460
|
children: Array.from({ length: c }, (e, t) => {
|
|
461
|
-
let n = l === t, r =
|
|
461
|
+
let n = l === t, r = X === t, i = d?.[t], s = !i;
|
|
462
462
|
return /* @__PURE__ */ o("div", {
|
|
463
463
|
role: "button",
|
|
464
464
|
tabIndex: 0,
|
|
@@ -467,13 +467,17 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
467
467
|
(e.key === "Enter" || e.key === " ") && (e.preventDefault(), u(t));
|
|
468
468
|
},
|
|
469
469
|
onDragOver: (e) => {
|
|
470
|
-
f && (e.preventDefault(), e.dataTransfer.dropEffect = "copy",
|
|
470
|
+
f && (e.preventDefault(), e.dataTransfer.dropEffect = "copy", X !== t && Z(t));
|
|
471
471
|
},
|
|
472
|
-
onDragLeave: () =>
|
|
472
|
+
onDragLeave: () => Z(null),
|
|
473
473
|
onDrop: (e) => {
|
|
474
474
|
if (!f) return;
|
|
475
|
-
e.preventDefault(),
|
|
475
|
+
e.preventDefault(), Z(null);
|
|
476
476
|
let n = e.dataTransfer.getData("text/plain");
|
|
477
|
+
if (!n) {
|
|
478
|
+
let t = e.dataTransfer.getData("text/uri-list");
|
|
479
|
+
t && (n = t.split("\n").map((e) => e.trim()).find((e) => e && !e.startsWith("#")) ?? "");
|
|
480
|
+
}
|
|
477
481
|
n && f(t, n);
|
|
478
482
|
},
|
|
479
483
|
className: [
|
|
@@ -491,7 +495,17 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
491
495
|
className: ["flex-1 min-w-0 truncate", s ? "italic text-text-muted" : "font-medium"].join(" "),
|
|
492
496
|
children: s ? "Empty" : i
|
|
493
497
|
}),
|
|
494
|
-
|
|
498
|
+
s && _ && /* @__PURE__ */ a("button", {
|
|
499
|
+
type: "button",
|
|
500
|
+
onClick: (e) => {
|
|
501
|
+
e.stopPropagation(), _(t);
|
|
502
|
+
},
|
|
503
|
+
"aria-label": `Assign currently tracked program to slot ${t + 1}`,
|
|
504
|
+
title: "Assign currently tracked program here",
|
|
505
|
+
className: "flex-none p-1 rounded text-text-muted hover:text-gold-dim hover:bg-black/20 transition-colors",
|
|
506
|
+
children: /* @__PURE__ */ a(M, {})
|
|
507
|
+
}),
|
|
508
|
+
!s && h && ee && /* @__PURE__ */ a("button", {
|
|
495
509
|
type: "button",
|
|
496
510
|
onClick: (e) => {
|
|
497
511
|
e.stopPropagation(), h(t);
|
|
@@ -510,7 +524,7 @@ function T({ pedalImageUrl: e, pots: t, onPotChange: n, potLabels: s = [
|
|
|
510
524
|
"aria-label": `Unassign slot ${t + 1}`,
|
|
511
525
|
title: "Unassign",
|
|
512
526
|
className: "flex-none p-1 rounded text-text-muted hover:text-red-400 hover:bg-black/20 transition-colors",
|
|
513
|
-
children: /* @__PURE__ */ a(
|
|
527
|
+
children: /* @__PURE__ */ a(N, {})
|
|
514
528
|
})
|
|
515
529
|
]
|
|
516
530
|
}, t);
|
|
@@ -615,6 +629,28 @@ function j({ small: e = !1 }) {
|
|
|
615
629
|
});
|
|
616
630
|
}
|
|
617
631
|
function M() {
|
|
632
|
+
return /* @__PURE__ */ o("svg", {
|
|
633
|
+
viewBox: "0 0 24 24",
|
|
634
|
+
fill: "none",
|
|
635
|
+
stroke: "currentColor",
|
|
636
|
+
strokeWidth: "2",
|
|
637
|
+
strokeLinecap: "round",
|
|
638
|
+
strokeLinejoin: "round",
|
|
639
|
+
className: "w-3.5 h-3.5",
|
|
640
|
+
children: [/* @__PURE__ */ a("line", {
|
|
641
|
+
x1: "12",
|
|
642
|
+
y1: "5",
|
|
643
|
+
x2: "12",
|
|
644
|
+
y2: "19"
|
|
645
|
+
}), /* @__PURE__ */ a("line", {
|
|
646
|
+
x1: "5",
|
|
647
|
+
y1: "12",
|
|
648
|
+
x2: "19",
|
|
649
|
+
y2: "12"
|
|
650
|
+
})]
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function N() {
|
|
618
654
|
return /* @__PURE__ */ o("svg", {
|
|
619
655
|
viewBox: "0 0 24 24",
|
|
620
656
|
fill: "none",
|
|
@@ -633,14 +669,14 @@ function M() {
|
|
|
633
669
|
}
|
|
634
670
|
//#endregion
|
|
635
671
|
//#region src/audio/binary.ts
|
|
636
|
-
function
|
|
672
|
+
function P(e) {
|
|
637
673
|
let t = [], n = new DataView(e.buffer, e.byteOffset, e.byteLength);
|
|
638
674
|
for (let r = 0; r < e.length; r += 4) t.push(n.getUint32(r, !1));
|
|
639
675
|
return t;
|
|
640
676
|
}
|
|
641
677
|
//#endregion
|
|
642
678
|
//#region src/simulator/useSimulator.ts
|
|
643
|
-
function
|
|
679
|
+
function F({ workletUrl: i, sampleRate: a = 32768 }) {
|
|
644
680
|
let o = n(null), s = n(null), c = n(null), u = n(null), d = n(null), [f, p] = r({
|
|
645
681
|
playing: !1,
|
|
646
682
|
bypassed: !1,
|
|
@@ -658,7 +694,7 @@ function P({ workletUrl: i, sampleRate: a = 32768 }) {
|
|
|
658
694
|
})(), d.current;
|
|
659
695
|
}, [i, a]), h = e(async (e) => {
|
|
660
696
|
await m();
|
|
661
|
-
let t =
|
|
697
|
+
let t = P(e);
|
|
662
698
|
s.current.port.postMessage({
|
|
663
699
|
type: "loadProgram",
|
|
664
700
|
machineCode: t
|
|
@@ -670,7 +706,7 @@ function P({ workletUrl: i, sampleRate: a = 32768 }) {
|
|
|
670
706
|
success: !1,
|
|
671
707
|
errors: n
|
|
672
708
|
};
|
|
673
|
-
let r =
|
|
709
|
+
let r = P(l.toUint8Array(t.machineCode));
|
|
674
710
|
return s.current.port.postMessage({
|
|
675
711
|
type: "loadProgram",
|
|
676
712
|
machineCode: r
|
|
@@ -759,6 +795,6 @@ function P({ workletUrl: i, sampleRate: a = 32768 }) {
|
|
|
759
795
|
};
|
|
760
796
|
}
|
|
761
797
|
//#endregion
|
|
762
|
-
export { y as ClipSelector, f as Knob, s as PROGRAM_SLOT_COUNT, T as PedalFace, v as ProgramSelector,
|
|
798
|
+
export { y as ClipSelector, f as Knob, s as PROGRAM_SLOT_COUNT, T as PedalFace, v as ProgramSelector, P as binaryToMachineCode, F as useSimulator };
|
|
763
799
|
|
|
764
800
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/components/Knob.tsx","../src/components/ProgramSelector.tsx","../src/components/ClipSelector.tsx","../src/components/PedalFace.tsx","../src/audio/binary.ts","../src/simulator/useSimulator.ts"],"sourcesContent":["import { useRef, useCallback } from 'react'\n\ninterface KnobProps {\n value: number // 0.0 – 1.0\n label: string\n onChange: (value: number) => void\n size?: number\n /** When true, render without a visible body — just a pointer indicator\n * (for overlaying on top of a pre-drawn dial graphic). */\n overlay?: boolean\n}\n\nconst SWEEP_DEG = 300\nconst MIN_DEG = -SWEEP_DEG / 2\n\nexport function Knob({ value, label, onChange, size = 56, overlay = false }: KnobProps) {\n const knobRef = useRef<SVGSVGElement>(null)\n const dragging = useRef(false)\n const startAngle = useRef(0)\n const startValue = useRef(0)\n\n const getAngleFromEvent = useCallback((e: PointerEvent) => {\n const el = knobRef.current\n if (!el) return 0\n const rect = el.getBoundingClientRect()\n const cx = rect.left + rect.width / 2\n const cy = rect.top + rect.height / 2\n return Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI)\n }, [])\n\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n dragging.current = true\n startAngle.current = getAngleFromEvent(e.nativeEvent)\n startValue.current = value\n ;(e.target as Element).setPointerCapture(e.pointerId)\n }, [getAngleFromEvent, value])\n\n const onPointerMove = useCallback((e: React.PointerEvent) => {\n if (!dragging.current) return\n const angle = getAngleFromEvent(e.nativeEvent)\n const delta = angle - startAngle.current\n const norm = ((delta + 540) % 360) - 180\n const valueDelta = norm / SWEEP_DEG\n const newValue = Math.max(0, Math.min(1, startValue.current + valueDelta))\n onChange(newValue)\n }, [getAngleFromEvent, onChange])\n\n const onPointerUp = useCallback(() => {\n dragging.current = false\n }, [])\n\n const rotation = MIN_DEG + value * SWEEP_DEG\n\n const svg = (\n <svg\n ref={knobRef}\n width={overlay ? '100%' : size}\n height={overlay ? '100%' : size}\n viewBox=\"0 0 56 56\"\n className=\"cursor-grab active:cursor-grabbing select-none touch-none\"\n role=\"slider\"\n aria-valuenow={Math.round(value * 100)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={label}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n >\n <circle cx=\"28\" cy=\"28\" r=\"28\" fill=\"transparent\" />\n {overlay ? (\n <circle cx=\"28\" cy=\"28\" r=\"24\" fill=\"none\" stroke=\"#111\" strokeWidth=\"1.5\" />\n ) : (\n <>\n <circle cx=\"28\" cy=\"28\" r=\"26\" fill=\"none\" stroke=\"#888\" strokeWidth=\"1.5\" />\n <circle cx=\"28\" cy=\"28\" r=\"22\" fill=\"#f0f0f0\" stroke=\"#aaa\" strokeWidth=\"1\" />\n </>\n )}\n <g transform={`rotate(${rotation} 28 28)`}>\n <line\n x1=\"28\"\n y1=\"28\"\n x2=\"28\"\n y2={overlay ? 10 : 8}\n stroke={overlay ? '#111' : '#B8942C'}\n strokeWidth={overlay ? 2.5 : 2}\n strokeLinecap=\"round\"\n />\n </g>\n </svg>\n )\n\n if (overlay) {\n return svg\n }\n\n return (\n <div className=\"flex flex-col items-center gap-1\">\n {svg}\n <span className=\"text-[10px] text-text-secondary text-center leading-tight max-w-[80px] truncate\">\n {label}\n </span>\n </div>\n )\n}\n","import { useRef, useCallback } from 'react'\nimport { PROGRAM_SLOT_COUNT } from '@audiofab-io/fv1-core/pedal'\n\ninterface ProgramSelectorProps {\n selectedSlot: number\n onSelectSlot: (slot: number) => void\n slotLabels?: string[]\n /** When true, render as a rotary dial with a pointer indicator (no visible body). */\n overlay?: boolean\n}\n\nconst POS_COUNT = PROGRAM_SLOT_COUNT\nconst START_ANGLE_DEG = 180 // position 1 (lower-left)\nconst END_ANGLE_DEG = 30 + 360 // position 8 (lower-right, via the top)\nconst STEP_DEG = (END_ANGLE_DEG - START_ANGLE_DEG) / (POS_COUNT - 1)\n\nfunction angleForPosition(index0: number): number {\n return START_ANGLE_DEG + index0 * STEP_DEG\n}\n\nfunction nearestSlotFromPointerAngle(pointerDeg: number): number {\n const a = ((pointerDeg % 360) + 360) % 360\n let bestSlot = 0\n let bestDist = Infinity\n for (let i = 0; i < POS_COUNT; i++) {\n const slotAngle = ((angleForPosition(i) % 360) + 360) % 360\n let diff = Math.abs(a - slotAngle)\n if (diff > 180) diff = 360 - diff\n if (diff < bestDist) {\n bestDist = diff\n bestSlot = i\n }\n }\n return bestSlot\n}\n\n/**\n * Rotary program selector. In overlay mode it renders as a pot-style knob\n * (black circular outline + pointer) that points at the currently selected\n * slot. The knob can be dragged to change program — it snaps to the nearest\n * of the 8 discrete positions.\n */\nexport function ProgramSelector({\n selectedSlot,\n onSelectSlot,\n slotLabels,\n overlay = false,\n}: ProgramSelectorProps) {\n const knobRef = useRef<SVGSVGElement>(null)\n const dragging = useRef(false)\n\n const getAngleFromEvent = useCallback((e: PointerEvent) => {\n const el = knobRef.current\n if (!el) return 0\n const rect = el.getBoundingClientRect()\n const cx = rect.left + rect.width / 2\n const cy = rect.top + rect.height / 2\n const raw = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI)\n return ((raw % 360) + 360) % 360\n }, [])\n\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n dragging.current = true\n ;(e.target as Element).setPointerCapture(e.pointerId)\n const angle = getAngleFromEvent(e.nativeEvent)\n const nearest = nearestSlotFromPointerAngle(angle)\n if (nearest !== selectedSlot) onSelectSlot(nearest)\n }, [getAngleFromEvent, selectedSlot, onSelectSlot])\n\n const onPointerMove = useCallback((e: React.PointerEvent) => {\n if (!dragging.current) return\n const angle = getAngleFromEvent(e.nativeEvent)\n const nearest = nearestSlotFromPointerAngle(angle)\n if (nearest !== selectedSlot) onSelectSlot(nearest)\n }, [getAngleFromEvent, selectedSlot, onSelectSlot])\n\n const onPointerUp = useCallback(() => {\n dragging.current = false\n }, [])\n\n if (!overlay) {\n return (\n <div className=\"grid grid-cols-4 gap-1\">\n {Array.from({ length: POS_COUNT }, (_, i) => (\n <button\n key={i}\n onClick={() => onSelectSlot(i)}\n title={slotLabels?.[i] ?? `Slot ${i + 1}`}\n className={`w-7 h-7 rounded text-xs font-bold ${selectedSlot === i\n ? 'bg-gold text-surface'\n : 'bg-surface-hover text-text-secondary hover:bg-surface-card border border-border'\n }`}\n >\n {i + 1}\n </button>\n ))}\n </div>\n )\n }\n\n const rotation = angleForPosition(selectedSlot) - 270\n\n return (\n <svg\n ref={knobRef}\n width=\"100%\"\n height=\"100%\"\n viewBox=\"0 0 56 56\"\n className=\"cursor-grab active:cursor-grabbing select-none touch-none\"\n role=\"listbox\"\n aria-label=\"Program selector\"\n aria-activedescendant={`prog-slot-${selectedSlot}`}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n >\n <circle cx=\"28\" cy=\"28\" r=\"28\" fill=\"transparent\" />\n <circle cx=\"28\" cy=\"28\" r=\"24\" fill=\"none\" stroke=\"#111\" strokeWidth=\"1.5\" />\n <g transform={`rotate(${rotation} 28 28)`}>\n <line\n x1=\"28\"\n y1=\"28\"\n x2=\"28\"\n y2=\"10\"\n stroke=\"#111\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n </g>\n </svg>\n )\n}\n","export interface ClipInfo {\n id: string\n name: string\n description?: string\n}\n\ninterface ClipSelectorProps {\n /** List of clips the consumer wants to expose. */\n clips: ClipInfo[]\n selectedClipId: string | null\n onSelect: (clipId: string) => void\n /** When true, the selector is visually disabled (e.g. while a list is loading). */\n disabled?: boolean\n}\n\n/**\n * Pure presentational dropdown for picking an audio clip. The shared package\n * has no opinion about where clips come from — consumers fetch / bundle their\n * own list and pass it in.\n */\nexport function ClipSelector({ clips, selectedClipId, onSelect, disabled = false }: ClipSelectorProps) {\n return (\n <select\n value={selectedClipId ?? ''}\n onChange={e => onSelect(e.target.value)}\n disabled={disabled}\n className=\"px-2 py-1 rounded bg-surface-hover border border-border text-xs text-text-primary focus:outline-none focus:border-gold-dim disabled:opacity-50\"\n >\n <option value=\"\" disabled>\n {disabled ? 'Loading…' : 'Select clip'}\n </option>\n {clips.map(clip => (\n <option key={clip.id} value={clip.id}>\n {clip.name}\n </option>\n ))}\n </select>\n )\n}\n","import { useState } from 'react'\nimport { Knob } from './Knob'\nimport { ProgramSelector } from './ProgramSelector'\nimport { ClipSelector, type ClipInfo } from './ClipSelector'\nimport { PROGRAM_SLOT_COUNT } from '@audiofab-io/fv1-core/pedal'\n\nexport type ChannelMode = 'mono' | 'stereo'\n\nexport interface PedalFaceProps {\n /** URL of the pedal outline graphic. Required — see assets/pedal.png in\n * this package, or bring your own. */\n pedalImageUrl: string\n\n /** Current values of the three pots, normalised 0..1. */\n pots: [number, number, number]\n onPotChange: (index: number, value: number) => void\n /** Per-pot display labels — typically the effect's `controls[].name`,\n * or `Pot 0/1/2` when the active program is unknown. */\n potLabels?: [string, string, string]\n\n /** Currently selected slot (0-based). */\n selectedSlot: number\n onSelectSlot: (slot: number) => void\n /** Display labels for each of the 8 slots. Empty / undefined means empty. */\n slotLabels?: string[]\n /** Drop handler for slot N. The data string is the platform-specific\n * payload (e.g. an effect id, a file URI). The consumer interprets it. */\n onAssignSlot?: (slotIndex: number, payload: string) => void\n onUnassignSlot?: (slotIndex: number) => void\n onUnassignAllSlots?: () => void\n /** Called when the user clicks the per-slot \"send to pedal\" icon. */\n onProgramSlot?: (slotIndex: number) => void\n /** Index of the slot currently being written to the pedal, or null. */\n programmingSlot?: number | null\n\n /** Audio clip selection — pass an empty array to hide the selector. */\n clips?: ClipInfo[]\n selectedClipId: string | null\n onSelectClip: (clipId: string) => void\n\n playing: boolean\n onPlay: () => void\n onPause: () => void\n bypassed: boolean\n onToggleBypass: () => void\n channelMode: ChannelMode\n onChannelModeChange: (mode: ChannelMode) => void\n\n /** Pedal connection state — the I/O button cluster only renders when\n * these are wired up. */\n pedalConnected?: boolean\n onConnectPedal?: () => void\n onReadPedal?: () => void\n onWritePedal?: () => void\n pedalReading?: boolean\n pedalWriting?: boolean\n}\n\n/**\n * Overlay positions as percentages of the pedal image (1573 × 2627).\n *\n * pot0 / pot1 / pot2 — potentiometer centres\n * programSelector — rotary program selector knob centre\n * led — status LED\n * footswitch — bypass footswitch centre\n */\nconst POS = {\n pot0: { left: '22.85%', top: '14%' },\n pot1: { left: '77.15%', top: '14%' },\n pot2: { left: '22.85%', top: '33.75%' },\n programSelector: { left: '76%', top: '33.75%' },\n led: { left: '50%', top: '42%' },\n footswitch: { left: '50%', top: '68.5%' },\n pot0Label: { left: '22.85%', top: '2.5%' },\n pot1Label: { left: '77.15%', top: '2.5%' },\n pot2Label: { left: '22.85%', top: '44%' },\n pedalIO: { left: '105%', top: '70%' },\n}\n\nconst KNOB_SIZE_PCT = 20\nconst FOOTSWITCH_SIZE_PCT = 14\nconst HEX_NUT_SIZE_PCT = 20\nconst LED_SIZE_PCT = 5\n\nexport function PedalFace({\n pedalImageUrl,\n pots,\n onPotChange,\n potLabels = ['Pot 0', 'Pot 1', 'Pot 2'],\n selectedSlot,\n onSelectSlot,\n slotLabels,\n onAssignSlot,\n onUnassignSlot,\n onUnassignAllSlots,\n onProgramSlot,\n programmingSlot = null,\n clips,\n selectedClipId,\n onSelectClip,\n playing,\n onPlay,\n onPause,\n bypassed,\n onToggleBypass,\n channelMode,\n onChannelModeChange,\n pedalConnected = false,\n onConnectPedal,\n onReadPedal,\n onWritePedal,\n pedalReading = false,\n pedalWriting = false,\n}: PedalFaceProps) {\n const pedalBusy = pedalReading || pedalWriting\n const [dragOverSlot, setDragOverSlot] = useState<number | null>(null)\n\n const ledOn = !bypassed\n const anyAssigned = slotLabels?.some(l => l) ?? false\n const slotProgrammable = pedalConnected && !pedalBusy\n\n return (\n <div className=\"flex flex-col items-center gap-5 w-full max-w-[460px] mx-auto\">\n <div\n className=\"relative w-full max-w-[360px] pedal-outline\"\n style={{\n aspectRatio: '1573 / 2627',\n backgroundImage: `url(${pedalImageUrl})`,\n backgroundSize: 'contain',\n backgroundRepeat: 'no-repeat',\n backgroundPosition: 'center top',\n }}\n >\n <KnobOverlay\n posStyle={POS.pot0}\n sizePct={KNOB_SIZE_PCT}\n value={pots[0]}\n label={potLabels[0]}\n onChange={v => onPotChange(0, v)}\n />\n <KnobOverlay\n posStyle={POS.pot1}\n sizePct={KNOB_SIZE_PCT}\n value={pots[1]}\n label={potLabels[1]}\n onChange={v => onPotChange(1, v)}\n />\n <KnobOverlay\n posStyle={POS.pot2}\n sizePct={KNOB_SIZE_PCT}\n value={pots[2]}\n label={potLabels[2]}\n onChange={v => onPotChange(2, v)}\n />\n\n <PotLabel posStyle={POS.pot0Label} text={potLabels[0]} />\n <PotLabel posStyle={POS.pot1Label} text={potLabels[1]} />\n <PotLabel posStyle={POS.pot2Label} text={potLabels[2]} />\n\n {/* Program Selector knob */}\n <div\n className=\"absolute\"\n style={{\n left: POS.programSelector.left,\n top: POS.programSelector.top,\n width: `${KNOB_SIZE_PCT}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n }}\n >\n <ProgramSelector\n overlay\n selectedSlot={selectedSlot}\n onSelectSlot={onSelectSlot}\n slotLabels={slotLabels}\n />\n </div>\n\n {/* LED */}\n <div\n className=\"absolute rounded-full transition-all\"\n style={{\n left: POS.led.left,\n top: POS.led.top,\n width: `${LED_SIZE_PCT}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n background: ledOn\n ? 'radial-gradient(circle at 35% 30%, #ff8a8a 0%, #e11 55%, #800 100%)'\n : 'radial-gradient(circle at 35% 30%, #f2f2f2 0%, #bbb 60%, #777 100%)',\n boxShadow: ledOn\n ? '0 0 10px rgba(255, 60, 60, 0.75)'\n : 'inset 0 1px 2px rgba(0,0,0,0.25)',\n border: '1px solid #333',\n }}\n aria-label=\"Status LED\"\n />\n\n {/* Hex-shaped silver retaining nut frames the footswitch */}\n <svg\n viewBox=\"0 0 100 100\"\n className=\"absolute pointer-events-none\"\n style={{\n left: POS.footswitch.left,\n top: POS.footswitch.top,\n width: `${HEX_NUT_SIZE_PCT}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n filter: 'drop-shadow(0 2px 2px rgba(0,0,0,0.45))',\n }}\n >\n <defs>\n <radialGradient id=\"silverHexNut\" cx=\"40%\" cy=\"35%\" r=\"65%\">\n <stop offset=\"0%\" stopColor=\"#f8f8f8\" />\n <stop offset=\"45%\" stopColor=\"#c8c8c8\" />\n <stop offset=\"100%\" stopColor=\"#707070\" />\n </radialGradient>\n </defs>\n <polygon\n points=\"5,50 27.5,11 72.5,11 95,50 72.5,89 27.5,89\"\n fill=\"url(#silverHexNut)\"\n stroke=\"#333\"\n strokeWidth=\"2\"\n strokeLinejoin=\"round\"\n />\n </svg>\n\n {/* Pedal I/O cluster — only rendered when consumer wires it up */}\n {(onConnectPedal || onReadPedal || onWritePedal) && (\n <div\n className=\"absolute flex items-center gap-1\"\n style={{\n left: POS.pedalIO.left,\n top: POS.pedalIO.top,\n transform: 'translate(-50%, -50%)',\n }}\n >\n {!pedalConnected ? (\n onConnectPedal && (\n <button\n type=\"button\"\n onClick={onConnectPedal}\n className=\"px-2 py-1 rounded border border-border bg-surface-card text-[10px] font-semibold text-text-primary hover:border-gold-dim transition-colors whitespace-nowrap\"\n >\n Connect\n </button>\n )\n ) : (\n <>\n {onReadPedal && (\n <IconButton\n onClick={onReadPedal}\n disabled={pedalBusy}\n busy={pedalReading}\n label=\"Read programs from pedal\"\n >\n <ArrowUpFromBox />\n </IconButton>\n )}\n {onWritePedal && (\n <IconButton\n onClick={onWritePedal}\n disabled={pedalBusy}\n busy={pedalWriting}\n label=\"Write programs to pedal\"\n >\n <ArrowDownToBox />\n </IconButton>\n )}\n </>\n )}\n </div>\n )}\n\n {/* Footswitch — silver stompbox-style */}\n <button\n onClick={onToggleBypass}\n aria-label={bypassed ? 'Bypassed — click to engage' : 'Engaged — click to bypass'}\n className=\"absolute rounded-full cursor-pointer\"\n style={{\n left: POS.footswitch.left,\n top: POS.footswitch.top,\n width: `${FOOTSWITCH_SIZE_PCT}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n background: 'radial-gradient(circle at 35% 30%, #fafafa 0%, #cfcfcf 45%, #888 90%, #555 100%)',\n border: '1.5px solid #222',\n boxShadow: '0 2px 4px rgba(0,0,0,0.4), inset 0 1px 1px rgba(255,255,255,0.6), inset 0 -2px 3px rgba(0,0,0,0.25)',\n }}\n >\n <span\n className=\"absolute rounded-full pointer-events-none\"\n style={{\n left: '50%',\n top: '50%',\n width: '80%',\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n background: 'radial-gradient(circle at 40% 35%, #f0f0f0 0%, #b0b0b0 60%, #707070 100%)',\n border: '1px solid #444',\n boxShadow: 'inset 0 1px 1px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.35)',\n }}\n />\n </button>\n </div>\n\n {/* Web/extension playback controls (not part of the physical pedal) */}\n <div className=\"flex items-center gap-3 flex-wrap justify-center\">\n <button\n onClick={playing ? onPause : onPlay}\n className=\"px-4 py-1.5 rounded border border-border bg-surface-card text-sm font-semibold text-text-primary hover:border-gold-dim transition-colors\"\n >\n {playing ? 'Pause' : 'Play'}\n </button>\n {clips && clips.length > 0 && (\n <ClipSelector\n clips={clips}\n selectedClipId={selectedClipId}\n onSelect={onSelectClip}\n />\n )}\n <div\n role=\"radiogroup\"\n aria-label=\"Output channel mode\"\n title=\"Mono matches the current Easy Spin hardware (DACL to both jacks). Stereo plays the FV-1's native left/right outputs.\"\n className=\"flex items-center rounded border border-border bg-surface-card text-sm overflow-hidden\"\n >\n {(['mono', 'stereo'] as const).map(mode => (\n <button\n key={mode}\n type=\"button\"\n role=\"radio\"\n aria-checked={channelMode === mode}\n onClick={() => onChannelModeChange(mode)}\n className={`px-3 py-1.5 font-semibold transition-colors ${\n channelMode === mode\n ? 'bg-gold text-surface'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {mode === 'mono' ? 'Mono' : 'Stereo'}\n </button>\n ))}\n </div>\n </div>\n\n {/* Program slots — drop a payload onto a slot to assign it; click a\n slot to make it the active program. */}\n <div className=\"w-full space-y-2\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-xs font-semibold uppercase tracking-wider text-text-muted\">\n Programs on Pedal\n </h3>\n {anyAssigned && onUnassignAllSlots && (\n <button\n type=\"button\"\n onClick={onUnassignAllSlots}\n className=\"text-[11px] text-text-muted hover:text-red-400 transition-colors\"\n title=\"Clear all local slot assignments\"\n >\n Unassign all\n </button>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-2\">\n {Array.from({ length: PROGRAM_SLOT_COUNT }, (_, i) => {\n const isActive = selectedSlot === i\n const isDragOver = dragOverSlot === i\n const label = slotLabels?.[i]\n const isEmpty = !label\n return (\n <div\n key={i}\n role=\"button\"\n tabIndex={0}\n onClick={() => onSelectSlot(i)}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onSelectSlot(i)\n }\n }}\n onDragOver={e => {\n if (!onAssignSlot) return\n e.preventDefault()\n e.dataTransfer.dropEffect = 'copy'\n if (dragOverSlot !== i) setDragOverSlot(i)\n }}\n onDragLeave={() => setDragOverSlot(null)}\n onDrop={e => {\n if (!onAssignSlot) return\n e.preventDefault()\n setDragOverSlot(null)\n const payload = e.dataTransfer.getData('text/plain')\n if (payload) onAssignSlot(i, payload)\n }}\n className={[\n 'flex items-center gap-2 px-2 py-2 rounded border text-left transition-colors cursor-pointer',\n 'bg-surface-card text-sm outline-none focus-visible:ring-1 focus-visible:ring-gold-dim',\n isActive\n ? 'border-gold-dim ring-1 ring-gold-dim/60 text-text-primary'\n : 'border-border text-text-primary hover:border-gold-dim',\n isDragOver ? 'border-dashed border-gold-dim bg-gold-dim/10' : '',\n ].join(' ')}\n >\n <span\n className={[\n 'flex-none w-6 h-6 inline-flex items-center justify-center rounded-full text-xs font-bold',\n isActive ? 'bg-gold-dim text-black' : 'bg-black/30 text-text-primary',\n ].join(' ')}\n >\n {i + 1}\n </span>\n <span\n className={[\n 'flex-1 min-w-0 truncate',\n isEmpty ? 'italic text-text-muted' : 'font-medium',\n ].join(' ')}\n >\n {isEmpty ? 'Empty' : label}\n </span>\n {!isEmpty && onProgramSlot && slotProgrammable && (\n <button\n type=\"button\"\n onClick={e => {\n e.stopPropagation()\n onProgramSlot(i)\n }}\n disabled={programmingSlot !== null}\n aria-label={`Write slot ${i + 1} to pedal`}\n title=\"Write this slot to pedal\"\n className=\"flex-none p-1 rounded text-text-muted hover:text-gold-dim hover:bg-black/20 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n >\n {programmingSlot === i ? <Spinner small /> : <ArrowDownToBox small />}\n </button>\n )}\n {!isEmpty && onUnassignSlot && (\n <button\n type=\"button\"\n onClick={e => {\n e.stopPropagation()\n onUnassignSlot(i)\n }}\n aria-label={`Unassign slot ${i + 1}`}\n title=\"Unassign\"\n className=\"flex-none p-1 rounded text-text-muted hover:text-red-400 hover:bg-black/20 transition-colors\"\n >\n <TrashIcon />\n </button>\n )}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\ninterface KnobOverlayProps {\n posStyle: { left: string; top: string }\n sizePct: number\n value: number\n label: string\n onChange: (value: number) => void\n}\n\nfunction KnobOverlay({ posStyle, sizePct, value, label, onChange }: KnobOverlayProps) {\n return (\n <div\n className=\"absolute\"\n style={{\n left: posStyle.left,\n top: posStyle.top,\n width: `${sizePct}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n }}\n >\n <Knob overlay value={value} label={label} onChange={onChange} />\n </div>\n )\n}\n\ninterface PotLabelProps {\n posStyle: { left: string; top: string }\n text: string\n}\n\nfunction PotLabel({ posStyle, text }: PotLabelProps) {\n return (\n <div\n className=\"absolute text-center text-[11px] sm:text-xs font-semibold text-black whitespace-nowrap pointer-events-none\"\n style={{\n left: posStyle.left,\n top: posStyle.top,\n transform: 'translate(-50%, -50%)',\n }}\n >\n {text}\n </div>\n )\n}\n\ninterface IconButtonProps {\n onClick?: () => void\n disabled?: boolean\n busy?: boolean\n label: string\n children: React.ReactNode\n}\n\nfunction IconButton({ onClick, disabled, busy, label, children }: IconButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-label={label}\n title={label}\n className=\"p-1 rounded border border-border bg-surface-card text-text-primary hover:border-gold-dim hover:text-gold-dim disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n >\n {busy ? <Spinner /> : children}\n </button>\n )\n}\n\nfunction Spinner({ small = false }: { small?: boolean }) {\n const cls = small ? 'w-3.5 h-3.5 animate-spin' : 'w-4 h-4 animate-spin'\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={cls}>\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n )\n}\n\nfunction ArrowUpFromBox() {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"w-4 h-4\">\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n <polyline points=\"17 8 12 3 7 8\" />\n <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\" />\n </svg>\n )\n}\n\nfunction ArrowDownToBox({ small = false }: { small?: boolean }) {\n const cls = small ? 'w-3.5 h-3.5' : 'w-4 h-4'\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={cls}>\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n <polyline points=\"7 10 12 15 17 10\" />\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\" />\n </svg>\n )\n}\n\nfunction TrashIcon() {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"w-3.5 h-3.5\">\n <path d=\"M3 6h18\" />\n <path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\" />\n <path d=\"M10 11v6M14 11v6\" />\n </svg>\n )\n}\n","/**\n * Converts a compiled FV-1 binary (Uint8Array, big-endian) to an array of\n * 32-bit machine code words suitable for FV1Simulator.loadProgram().\n *\n * An FV-1 program is 128 instructions × 4 bytes = 512 bytes.\n */\nexport function binaryToMachineCode(binary: Uint8Array): number[] {\n const words: number[] = []\n const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength)\n for (let i = 0; i < binary.length; i += 4) {\n words.push(view.getUint32(i, false)) // big-endian\n }\n return words\n}\n","import { useRef, useState, useCallback, useEffect } from 'react'\nimport { FV1Assembler } from '@audiofab-io/fv1-core'\nimport type { FV1AssemblerProblem } from '@audiofab-io/fv1-core'\nimport { binaryToMachineCode } from '../audio/binary'\n\nexport type ChannelMode = 'mono' | 'stereo'\n\nexport interface SimulatorState {\n /** True when audio is currently playing. */\n playing: boolean\n /** True when the bypass footswitch is engaged (signal passes through dry). */\n bypassed: boolean\n /** Output channel mode — see ChannelMode. */\n channelMode: ChannelMode\n /** Sample rate of the AudioContext (set after init). */\n sampleRate: number | null\n}\n\nexport interface UseSimulatorOptions {\n /**\n * URL of the bundled FV-1 audio worklet processor. Required.\n *\n * In Vite-based consumers, prefer:\n * import workletUrl from '@audiofab-io/easy-spin-ui/worklet?url'\n *\n * In VS Code webview consumers, derive the URL via\n * webview.asWebviewUri(...)\n * after copying `node_modules/@audiofab-io/easy-spin-ui/dist/worklet/fv1-processor.js`\n * into your webview's resource directory.\n */\n workletUrl: string\n /** AudioContext sample rate. Defaults to 32 768 Hz to match the FV-1. */\n sampleRate?: number\n}\n\nexport interface AssembleResult {\n success: boolean\n errors?: FV1AssemblerProblem[]\n}\n\n/**\n * Headless hook that drives an off-DOM AudioContext + FV-1 audio worklet.\n *\n * The hook is deliberately decoupled from any \"where do programs come from\"\n * concern — consumers fetch / compile / load binaries themselves and pass\n * them in via `loadProgram`. The same applies to audio clips.\n */\nexport function useSimulator({ workletUrl, sampleRate = 32768 }: UseSimulatorOptions) {\n const ctxRef = useRef<AudioContext | null>(null)\n const workletRef = useRef<AudioWorkletNode | null>(null)\n const sourceRef = useRef<AudioBufferSourceNode | null>(null)\n const clipBufferRef = useRef<AudioBuffer | null>(null)\n const initPromiseRef = useRef<Promise<void> | null>(null)\n\n const [state, setState] = useState<SimulatorState>({\n playing: false,\n bypassed: false,\n channelMode: 'mono',\n sampleRate: null,\n })\n\n const init = useCallback(async () => {\n if (ctxRef.current) return\n if (initPromiseRef.current) return initPromiseRef.current\n\n initPromiseRef.current = (async () => {\n const ctx = new AudioContext({ sampleRate })\n await ctx.audioWorklet.addModule(workletUrl)\n const worklet = new AudioWorkletNode(ctx, 'fv1-processor', {\n outputChannelCount: [2],\n })\n worklet.connect(ctx.destination)\n ctxRef.current = ctx\n workletRef.current = worklet\n setState(s => ({ ...s, sampleRate: ctx.sampleRate }))\n })()\n\n return initPromiseRef.current\n }, [workletUrl, sampleRate])\n\n /** Load a pre-compiled FV-1 binary (512 bytes, big-endian) into the simulator. */\n const loadProgram = useCallback(async (binary: Uint8Array) => {\n await init()\n const code = binaryToMachineCode(binary)\n workletRef.current!.port.postMessage({\n type: 'loadProgram',\n machineCode: code,\n })\n }, [init])\n\n /**\n * Convenience: assemble FV-1 source and load the result.\n *\n * Returns `{ success: false, errors }` on fatal assembler errors so callers\n * can surface them. Non-fatal warnings are ignored here.\n */\n const loadProgramFromSource = useCallback(async (spnSource: string): Promise<AssembleResult> => {\n await init()\n const assembler = new FV1Assembler()\n const result = assembler.assemble(spnSource)\n const fatal = result.problems.filter(p => p.isfatal)\n if (fatal.length > 0) {\n return { success: false, errors: fatal }\n }\n const binary = FV1Assembler.toUint8Array(result.machineCode)\n const code = binaryToMachineCode(binary)\n workletRef.current!.port.postMessage({\n type: 'loadProgram',\n machineCode: code,\n })\n return { success: true }\n }, [init])\n\n /**\n * Decode an audio clip from raw bytes and stage it as the simulator's input.\n * If audio is currently playing, the new clip starts immediately; otherwise\n * it's just held until `play()` is called.\n */\n const loadClipBuffer = useCallback(async (clipBytes: ArrayBuffer | Uint8Array) => {\n await init()\n const ctx = ctxRef.current!\n const wasPlaying = sourceRef.current !== null\n\n if (sourceRef.current) {\n try { sourceRef.current.stop() } catch { /* already stopped */ }\n sourceRef.current.disconnect()\n sourceRef.current = null\n }\n\n const ab = clipBytes instanceof ArrayBuffer\n ? clipBytes\n : (clipBytes.buffer.slice(clipBytes.byteOffset, clipBytes.byteOffset + clipBytes.byteLength) as ArrayBuffer)\n const buffer = await ctx.decodeAudioData(ab)\n clipBufferRef.current = buffer\n\n if (wasPlaying) {\n const source = ctx.createBufferSource()\n source.buffer = buffer\n source.loop = true\n source.connect(workletRef.current!)\n source.start()\n sourceRef.current = source\n await ctx.resume()\n }\n }, [init])\n\n const play = useCallback(async () => {\n await init()\n const ctx = ctxRef.current!\n const buffer = clipBufferRef.current\n if (!buffer) return\n\n // AudioBufferSourceNode is single-use — create a fresh one each time.\n if (sourceRef.current) {\n try { sourceRef.current.stop() } catch { /* already stopped */ }\n sourceRef.current.disconnect()\n }\n\n const source = ctx.createBufferSource()\n source.buffer = buffer\n source.loop = true\n source.connect(workletRef.current!)\n source.start()\n sourceRef.current = source\n await ctx.resume()\n\n setState(s => ({ ...s, playing: true }))\n }, [init])\n\n const pause = useCallback(() => {\n if (sourceRef.current) {\n try { sourceRef.current.stop() } catch { /* already stopped */ }\n sourceRef.current.disconnect()\n sourceRef.current = null\n }\n ctxRef.current?.suspend()\n setState(s => ({ ...s, playing: false }))\n }, [])\n\n const setPot = useCallback((index: number, value: number) => {\n workletRef.current?.port.postMessage({ type: 'setPot', index, value })\n }, [])\n\n const setBypass = useCallback((active: boolean) => {\n workletRef.current?.port.postMessage({ type: 'bypass', active })\n setState(s => ({ ...s, bypassed: active }))\n }, [])\n\n const setChannelMode = useCallback((mode: ChannelMode) => {\n workletRef.current?.port.postMessage({ type: 'setChannelMode', mode })\n setState(s => ({ ...s, channelMode: mode }))\n }, [])\n\n // Tear down the AudioContext on unmount to avoid leaking audio threads.\n useEffect(() => {\n return () => {\n if (sourceRef.current) {\n try { sourceRef.current.stop() } catch { /* already stopped */ }\n sourceRef.current.disconnect()\n }\n ctxRef.current?.close().catch(() => { /* already closed */ })\n }\n }, [])\n\n return {\n ...state,\n loadProgram,\n loadProgramFromSource,\n loadClipBuffer,\n play,\n pause,\n setPot,\n setBypass,\n setChannelMode,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/components/Knob.tsx","../src/components/ProgramSelector.tsx","../src/components/ClipSelector.tsx","../src/components/PedalFace.tsx","../src/audio/binary.ts","../src/simulator/useSimulator.ts"],"sourcesContent":["import { useRef, useCallback } from 'react'\n\ninterface KnobProps {\n value: number // 0.0 – 1.0\n label: string\n onChange: (value: number) => void\n size?: number\n /** When true, render without a visible body — just a pointer indicator\n * (for overlaying on top of a pre-drawn dial graphic). */\n overlay?: boolean\n}\n\nconst SWEEP_DEG = 300\nconst MIN_DEG = -SWEEP_DEG / 2\n\nexport function Knob({ value, label, onChange, size = 56, overlay = false }: KnobProps) {\n const knobRef = useRef<SVGSVGElement>(null)\n const dragging = useRef(false)\n const startAngle = useRef(0)\n const startValue = useRef(0)\n\n const getAngleFromEvent = useCallback((e: PointerEvent) => {\n const el = knobRef.current\n if (!el) return 0\n const rect = el.getBoundingClientRect()\n const cx = rect.left + rect.width / 2\n const cy = rect.top + rect.height / 2\n return Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI)\n }, [])\n\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n dragging.current = true\n startAngle.current = getAngleFromEvent(e.nativeEvent)\n startValue.current = value\n ;(e.target as Element).setPointerCapture(e.pointerId)\n }, [getAngleFromEvent, value])\n\n const onPointerMove = useCallback((e: React.PointerEvent) => {\n if (!dragging.current) return\n const angle = getAngleFromEvent(e.nativeEvent)\n const delta = angle - startAngle.current\n const norm = ((delta + 540) % 360) - 180\n const valueDelta = norm / SWEEP_DEG\n const newValue = Math.max(0, Math.min(1, startValue.current + valueDelta))\n onChange(newValue)\n }, [getAngleFromEvent, onChange])\n\n const onPointerUp = useCallback(() => {\n dragging.current = false\n }, [])\n\n const rotation = MIN_DEG + value * SWEEP_DEG\n\n const svg = (\n <svg\n ref={knobRef}\n width={overlay ? '100%' : size}\n height={overlay ? '100%' : size}\n viewBox=\"0 0 56 56\"\n className=\"cursor-grab active:cursor-grabbing select-none touch-none\"\n role=\"slider\"\n aria-valuenow={Math.round(value * 100)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={label}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n >\n <circle cx=\"28\" cy=\"28\" r=\"28\" fill=\"transparent\" />\n {overlay ? (\n <circle cx=\"28\" cy=\"28\" r=\"24\" fill=\"none\" stroke=\"#111\" strokeWidth=\"1.5\" />\n ) : (\n <>\n <circle cx=\"28\" cy=\"28\" r=\"26\" fill=\"none\" stroke=\"#888\" strokeWidth=\"1.5\" />\n <circle cx=\"28\" cy=\"28\" r=\"22\" fill=\"#f0f0f0\" stroke=\"#aaa\" strokeWidth=\"1\" />\n </>\n )}\n <g transform={`rotate(${rotation} 28 28)`}>\n <line\n x1=\"28\"\n y1=\"28\"\n x2=\"28\"\n y2={overlay ? 10 : 8}\n stroke={overlay ? '#111' : '#B8942C'}\n strokeWidth={overlay ? 2.5 : 2}\n strokeLinecap=\"round\"\n />\n </g>\n </svg>\n )\n\n if (overlay) {\n return svg\n }\n\n return (\n <div className=\"flex flex-col items-center gap-1\">\n {svg}\n <span className=\"text-[10px] text-text-secondary text-center leading-tight max-w-[80px] truncate\">\n {label}\n </span>\n </div>\n )\n}\n","import { useRef, useCallback } from 'react'\nimport { PROGRAM_SLOT_COUNT } from '@audiofab-io/fv1-core/pedal'\n\ninterface ProgramSelectorProps {\n selectedSlot: number\n onSelectSlot: (slot: number) => void\n slotLabels?: string[]\n /** When true, render as a rotary dial with a pointer indicator (no visible body). */\n overlay?: boolean\n}\n\nconst POS_COUNT = PROGRAM_SLOT_COUNT\nconst START_ANGLE_DEG = 180 // position 1 (lower-left)\nconst END_ANGLE_DEG = 30 + 360 // position 8 (lower-right, via the top)\nconst STEP_DEG = (END_ANGLE_DEG - START_ANGLE_DEG) / (POS_COUNT - 1)\n\nfunction angleForPosition(index0: number): number {\n return START_ANGLE_DEG + index0 * STEP_DEG\n}\n\nfunction nearestSlotFromPointerAngle(pointerDeg: number): number {\n const a = ((pointerDeg % 360) + 360) % 360\n let bestSlot = 0\n let bestDist = Infinity\n for (let i = 0; i < POS_COUNT; i++) {\n const slotAngle = ((angleForPosition(i) % 360) + 360) % 360\n let diff = Math.abs(a - slotAngle)\n if (diff > 180) diff = 360 - diff\n if (diff < bestDist) {\n bestDist = diff\n bestSlot = i\n }\n }\n return bestSlot\n}\n\n/**\n * Rotary program selector. In overlay mode it renders as a pot-style knob\n * (black circular outline + pointer) that points at the currently selected\n * slot. The knob can be dragged to change program — it snaps to the nearest\n * of the 8 discrete positions.\n */\nexport function ProgramSelector({\n selectedSlot,\n onSelectSlot,\n slotLabels,\n overlay = false,\n}: ProgramSelectorProps) {\n const knobRef = useRef<SVGSVGElement>(null)\n const dragging = useRef(false)\n\n const getAngleFromEvent = useCallback((e: PointerEvent) => {\n const el = knobRef.current\n if (!el) return 0\n const rect = el.getBoundingClientRect()\n const cx = rect.left + rect.width / 2\n const cy = rect.top + rect.height / 2\n const raw = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI)\n return ((raw % 360) + 360) % 360\n }, [])\n\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n dragging.current = true\n ;(e.target as Element).setPointerCapture(e.pointerId)\n const angle = getAngleFromEvent(e.nativeEvent)\n const nearest = nearestSlotFromPointerAngle(angle)\n if (nearest !== selectedSlot) onSelectSlot(nearest)\n }, [getAngleFromEvent, selectedSlot, onSelectSlot])\n\n const onPointerMove = useCallback((e: React.PointerEvent) => {\n if (!dragging.current) return\n const angle = getAngleFromEvent(e.nativeEvent)\n const nearest = nearestSlotFromPointerAngle(angle)\n if (nearest !== selectedSlot) onSelectSlot(nearest)\n }, [getAngleFromEvent, selectedSlot, onSelectSlot])\n\n const onPointerUp = useCallback(() => {\n dragging.current = false\n }, [])\n\n if (!overlay) {\n return (\n <div className=\"grid grid-cols-4 gap-1\">\n {Array.from({ length: POS_COUNT }, (_, i) => (\n <button\n key={i}\n onClick={() => onSelectSlot(i)}\n title={slotLabels?.[i] ?? `Slot ${i + 1}`}\n className={`w-7 h-7 rounded text-xs font-bold ${selectedSlot === i\n ? 'bg-gold text-surface'\n : 'bg-surface-hover text-text-secondary hover:bg-surface-card border border-border'\n }`}\n >\n {i + 1}\n </button>\n ))}\n </div>\n )\n }\n\n const rotation = angleForPosition(selectedSlot) - 270\n\n return (\n <svg\n ref={knobRef}\n width=\"100%\"\n height=\"100%\"\n viewBox=\"0 0 56 56\"\n className=\"cursor-grab active:cursor-grabbing select-none touch-none\"\n role=\"listbox\"\n aria-label=\"Program selector\"\n aria-activedescendant={`prog-slot-${selectedSlot}`}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n >\n <circle cx=\"28\" cy=\"28\" r=\"28\" fill=\"transparent\" />\n <circle cx=\"28\" cy=\"28\" r=\"24\" fill=\"none\" stroke=\"#111\" strokeWidth=\"1.5\" />\n <g transform={`rotate(${rotation} 28 28)`}>\n <line\n x1=\"28\"\n y1=\"28\"\n x2=\"28\"\n y2=\"10\"\n stroke=\"#111\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n </g>\n </svg>\n )\n}\n","export interface ClipInfo {\n id: string\n name: string\n description?: string\n}\n\ninterface ClipSelectorProps {\n /** List of clips the consumer wants to expose. */\n clips: ClipInfo[]\n selectedClipId: string | null\n onSelect: (clipId: string) => void\n /** When true, the selector is visually disabled (e.g. while a list is loading). */\n disabled?: boolean\n}\n\n/**\n * Pure presentational dropdown for picking an audio clip. The shared package\n * has no opinion about where clips come from — consumers fetch / bundle their\n * own list and pass it in.\n */\nexport function ClipSelector({ clips, selectedClipId, onSelect, disabled = false }: ClipSelectorProps) {\n return (\n <select\n value={selectedClipId ?? ''}\n onChange={e => onSelect(e.target.value)}\n disabled={disabled}\n className=\"px-2 py-1 rounded bg-surface-hover border border-border text-xs text-text-primary focus:outline-none focus:border-gold-dim disabled:opacity-50\"\n >\n <option value=\"\" disabled>\n {disabled ? 'Loading…' : 'Select clip'}\n </option>\n {clips.map(clip => (\n <option key={clip.id} value={clip.id}>\n {clip.name}\n </option>\n ))}\n </select>\n )\n}\n","import { useState } from 'react'\nimport { Knob } from './Knob'\nimport { ProgramSelector } from './ProgramSelector'\nimport { ClipSelector, type ClipInfo } from './ClipSelector'\nimport { PROGRAM_SLOT_COUNT } from '@audiofab-io/fv1-core/pedal'\n\nexport type ChannelMode = 'mono' | 'stereo'\n\nexport interface PedalFaceProps {\n /** URL of the pedal outline graphic. Required — see assets/pedal.png in\n * this package, or bring your own. */\n pedalImageUrl: string\n\n /** Current values of the three pots, normalised 0..1. */\n pots: [number, number, number]\n onPotChange: (index: number, value: number) => void\n /** Per-pot display labels — typically the effect's `controls[].name`,\n * or `Pot 0/1/2` when the active program is unknown. */\n potLabels?: [string, string, string]\n\n /** Currently selected slot (0-based). */\n selectedSlot: number\n onSelectSlot: (slot: number) => void\n /** Display labels for each of the 8 slots. Empty / undefined means empty. */\n slotLabels?: string[]\n /** Drop handler for slot N. The data string is the platform-specific\n * payload (e.g. an effect id, a file URI). The consumer interprets it. */\n onAssignSlot?: (slotIndex: number, payload: string) => void\n onUnassignSlot?: (slotIndex: number) => void\n onUnassignAllSlots?: () => void\n /** Called when the user clicks the per-slot \"send to pedal\" icon. */\n onProgramSlot?: (slotIndex: number) => void\n /** Index of the slot currently being written to the pedal, or null. */\n programmingSlot?: number | null\n /** When set, empty slots show a \"+\" button that calls this callback —\n * used by hosts that have a notion of a \"currently tracked / active\"\n * program the user can drop into a slot. */\n onAssignTrackedToSlot?: (slotIndex: number) => void\n\n /** Audio clip selection — pass an empty array to hide the selector. */\n clips?: ClipInfo[]\n selectedClipId: string | null\n onSelectClip: (clipId: string) => void\n\n playing: boolean\n onPlay: () => void\n onPause: () => void\n bypassed: boolean\n onToggleBypass: () => void\n channelMode: ChannelMode\n onChannelModeChange: (mode: ChannelMode) => void\n\n /** Pedal connection state — the I/O button cluster only renders when\n * these are wired up. */\n pedalConnected?: boolean\n onConnectPedal?: () => void\n onReadPedal?: () => void\n onWritePedal?: () => void\n pedalReading?: boolean\n pedalWriting?: boolean\n}\n\n/**\n * Overlay positions as percentages of the pedal image (1573 × 2627).\n *\n * pot0 / pot1 / pot2 — potentiometer centres\n * programSelector — rotary program selector knob centre\n * led — status LED\n * footswitch — bypass footswitch centre\n */\nconst POS = {\n pot0: { left: '22.85%', top: '14%' },\n pot1: { left: '77.15%', top: '14%' },\n pot2: { left: '22.85%', top: '33.75%' },\n programSelector: { left: '76%', top: '33.75%' },\n led: { left: '50%', top: '42%' },\n footswitch: { left: '50%', top: '68.5%' },\n pot0Label: { left: '22.85%', top: '2.5%' },\n pot1Label: { left: '77.15%', top: '2.5%' },\n pot2Label: { left: '22.85%', top: '44%' },\n pedalIO: { left: '105%', top: '70%' },\n}\n\nconst KNOB_SIZE_PCT = 20\nconst FOOTSWITCH_SIZE_PCT = 14\nconst HEX_NUT_SIZE_PCT = 20\nconst LED_SIZE_PCT = 5\n\nexport function PedalFace({\n pedalImageUrl,\n pots,\n onPotChange,\n potLabels = ['Pot 0', 'Pot 1', 'Pot 2'],\n selectedSlot,\n onSelectSlot,\n slotLabels,\n onAssignSlot,\n onUnassignSlot,\n onUnassignAllSlots,\n onProgramSlot,\n programmingSlot = null,\n onAssignTrackedToSlot,\n clips,\n selectedClipId,\n onSelectClip,\n playing,\n onPlay,\n onPause,\n bypassed,\n onToggleBypass,\n channelMode,\n onChannelModeChange,\n pedalConnected = false,\n onConnectPedal,\n onReadPedal,\n onWritePedal,\n pedalReading = false,\n pedalWriting = false,\n}: PedalFaceProps) {\n const pedalBusy = pedalReading || pedalWriting\n const [dragOverSlot, setDragOverSlot] = useState<number | null>(null)\n\n const ledOn = !bypassed\n const anyAssigned = slotLabels?.some(l => l) ?? false\n const slotProgrammable = pedalConnected && !pedalBusy\n\n return (\n <div className=\"flex flex-col items-center gap-5 w-full max-w-[460px] mx-auto\">\n <div\n className=\"relative w-full max-w-[360px] pedal-outline\"\n style={{\n aspectRatio: '1573 / 2627',\n backgroundImage: `url(${pedalImageUrl})`,\n backgroundSize: 'contain',\n backgroundRepeat: 'no-repeat',\n backgroundPosition: 'center top',\n }}\n >\n <KnobOverlay\n posStyle={POS.pot0}\n sizePct={KNOB_SIZE_PCT}\n value={pots[0]}\n label={potLabels[0]}\n onChange={v => onPotChange(0, v)}\n />\n <KnobOverlay\n posStyle={POS.pot1}\n sizePct={KNOB_SIZE_PCT}\n value={pots[1]}\n label={potLabels[1]}\n onChange={v => onPotChange(1, v)}\n />\n <KnobOverlay\n posStyle={POS.pot2}\n sizePct={KNOB_SIZE_PCT}\n value={pots[2]}\n label={potLabels[2]}\n onChange={v => onPotChange(2, v)}\n />\n\n <PotLabel posStyle={POS.pot0Label} text={potLabels[0]} />\n <PotLabel posStyle={POS.pot1Label} text={potLabels[1]} />\n <PotLabel posStyle={POS.pot2Label} text={potLabels[2]} />\n\n {/* Program Selector knob */}\n <div\n className=\"absolute\"\n style={{\n left: POS.programSelector.left,\n top: POS.programSelector.top,\n width: `${KNOB_SIZE_PCT}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n }}\n >\n <ProgramSelector\n overlay\n selectedSlot={selectedSlot}\n onSelectSlot={onSelectSlot}\n slotLabels={slotLabels}\n />\n </div>\n\n {/* LED */}\n <div\n className=\"absolute rounded-full transition-all\"\n style={{\n left: POS.led.left,\n top: POS.led.top,\n width: `${LED_SIZE_PCT}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n background: ledOn\n ? 'radial-gradient(circle at 35% 30%, #ff8a8a 0%, #e11 55%, #800 100%)'\n : 'radial-gradient(circle at 35% 30%, #f2f2f2 0%, #bbb 60%, #777 100%)',\n boxShadow: ledOn\n ? '0 0 10px rgba(255, 60, 60, 0.75)'\n : 'inset 0 1px 2px rgba(0,0,0,0.25)',\n border: '1px solid #333',\n }}\n aria-label=\"Status LED\"\n />\n\n {/* Hex-shaped silver retaining nut frames the footswitch */}\n <svg\n viewBox=\"0 0 100 100\"\n className=\"absolute pointer-events-none\"\n style={{\n left: POS.footswitch.left,\n top: POS.footswitch.top,\n width: `${HEX_NUT_SIZE_PCT}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n filter: 'drop-shadow(0 2px 2px rgba(0,0,0,0.45))',\n }}\n >\n <defs>\n <radialGradient id=\"silverHexNut\" cx=\"40%\" cy=\"35%\" r=\"65%\">\n <stop offset=\"0%\" stopColor=\"#f8f8f8\" />\n <stop offset=\"45%\" stopColor=\"#c8c8c8\" />\n <stop offset=\"100%\" stopColor=\"#707070\" />\n </radialGradient>\n </defs>\n <polygon\n points=\"5,50 27.5,11 72.5,11 95,50 72.5,89 27.5,89\"\n fill=\"url(#silverHexNut)\"\n stroke=\"#333\"\n strokeWidth=\"2\"\n strokeLinejoin=\"round\"\n />\n </svg>\n\n {/* Pedal I/O cluster — only rendered when consumer wires it up */}\n {(onConnectPedal || onReadPedal || onWritePedal) && (\n <div\n className=\"absolute flex items-center gap-1\"\n style={{\n left: POS.pedalIO.left,\n top: POS.pedalIO.top,\n transform: 'translate(-50%, -50%)',\n }}\n >\n {!pedalConnected ? (\n onConnectPedal && (\n <button\n type=\"button\"\n onClick={onConnectPedal}\n className=\"px-2 py-1 rounded border border-border bg-surface-card text-[10px] font-semibold text-text-primary hover:border-gold-dim transition-colors whitespace-nowrap\"\n >\n Connect\n </button>\n )\n ) : (\n <>\n {onReadPedal && (\n <IconButton\n onClick={onReadPedal}\n disabled={pedalBusy}\n busy={pedalReading}\n label=\"Read programs from pedal\"\n >\n <ArrowUpFromBox />\n </IconButton>\n )}\n {onWritePedal && (\n <IconButton\n onClick={onWritePedal}\n disabled={pedalBusy}\n busy={pedalWriting}\n label=\"Write programs to pedal\"\n >\n <ArrowDownToBox />\n </IconButton>\n )}\n </>\n )}\n </div>\n )}\n\n {/* Footswitch — silver stompbox-style */}\n <button\n onClick={onToggleBypass}\n aria-label={bypassed ? 'Bypassed — click to engage' : 'Engaged — click to bypass'}\n className=\"absolute rounded-full cursor-pointer\"\n style={{\n left: POS.footswitch.left,\n top: POS.footswitch.top,\n width: `${FOOTSWITCH_SIZE_PCT}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n background: 'radial-gradient(circle at 35% 30%, #fafafa 0%, #cfcfcf 45%, #888 90%, #555 100%)',\n border: '1.5px solid #222',\n boxShadow: '0 2px 4px rgba(0,0,0,0.4), inset 0 1px 1px rgba(255,255,255,0.6), inset 0 -2px 3px rgba(0,0,0,0.25)',\n }}\n >\n <span\n className=\"absolute rounded-full pointer-events-none\"\n style={{\n left: '50%',\n top: '50%',\n width: '80%',\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n background: 'radial-gradient(circle at 40% 35%, #f0f0f0 0%, #b0b0b0 60%, #707070 100%)',\n border: '1px solid #444',\n boxShadow: 'inset 0 1px 1px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.35)',\n }}\n />\n </button>\n </div>\n\n {/* Web/extension playback controls (not part of the physical pedal) */}\n <div className=\"flex items-center gap-3 flex-wrap justify-center\">\n <button\n onClick={playing ? onPause : onPlay}\n className=\"px-4 py-1.5 rounded border border-border bg-surface-card text-sm font-semibold text-text-primary hover:border-gold-dim transition-colors\"\n >\n {playing ? 'Pause' : 'Play'}\n </button>\n {clips && clips.length > 0 && (\n <ClipSelector\n clips={clips}\n selectedClipId={selectedClipId}\n onSelect={onSelectClip}\n />\n )}\n <div\n role=\"radiogroup\"\n aria-label=\"Output channel mode\"\n title=\"Mono matches the current Easy Spin hardware (DACL to both jacks). Stereo plays the FV-1's native left/right outputs.\"\n className=\"flex items-center rounded border border-border bg-surface-card text-sm overflow-hidden\"\n >\n {(['mono', 'stereo'] as const).map(mode => (\n <button\n key={mode}\n type=\"button\"\n role=\"radio\"\n aria-checked={channelMode === mode}\n onClick={() => onChannelModeChange(mode)}\n className={`px-3 py-1.5 font-semibold transition-colors ${\n channelMode === mode\n ? 'bg-gold text-surface'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {mode === 'mono' ? 'Mono' : 'Stereo'}\n </button>\n ))}\n </div>\n </div>\n\n {/* Program slots — drop a payload onto a slot to assign it; click a\n slot to make it the active program. */}\n <div className=\"w-full space-y-2\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-xs font-semibold uppercase tracking-wider text-text-muted\">\n Programs on Pedal\n </h3>\n {anyAssigned && onUnassignAllSlots && (\n <button\n type=\"button\"\n onClick={onUnassignAllSlots}\n className=\"text-[11px] text-text-muted hover:text-red-400 transition-colors\"\n title=\"Clear all local slot assignments\"\n >\n Unassign all\n </button>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-2\">\n {Array.from({ length: PROGRAM_SLOT_COUNT }, (_, i) => {\n const isActive = selectedSlot === i\n const isDragOver = dragOverSlot === i\n const label = slotLabels?.[i]\n const isEmpty = !label\n return (\n <div\n key={i}\n role=\"button\"\n tabIndex={0}\n onClick={() => onSelectSlot(i)}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onSelectSlot(i)\n }\n }}\n onDragOver={e => {\n if (!onAssignSlot) return\n e.preventDefault()\n e.dataTransfer.dropEffect = 'copy'\n if (dragOverSlot !== i) setDragOverSlot(i)\n }}\n onDragLeave={() => setDragOverSlot(null)}\n onDrop={e => {\n if (!onAssignSlot) return\n e.preventDefault()\n setDragOverSlot(null)\n // Try text/plain first (web-app effect-id pattern), then\n // text/uri-list (VS Code / OS file drags). uri-list payloads\n // are newline-separated; we take the first URI.\n let payload = e.dataTransfer.getData('text/plain')\n if (!payload) {\n const uriList = e.dataTransfer.getData('text/uri-list')\n if (uriList) {\n payload = uriList.split('\\n').map(s => s.trim()).find(s => s && !s.startsWith('#')) ?? ''\n }\n }\n if (payload) onAssignSlot(i, payload)\n }}\n className={[\n 'flex items-center gap-2 px-2 py-2 rounded border text-left transition-colors cursor-pointer',\n 'bg-surface-card text-sm outline-none focus-visible:ring-1 focus-visible:ring-gold-dim',\n isActive\n ? 'border-gold-dim ring-1 ring-gold-dim/60 text-text-primary'\n : 'border-border text-text-primary hover:border-gold-dim',\n isDragOver ? 'border-dashed border-gold-dim bg-gold-dim/10' : '',\n ].join(' ')}\n >\n <span\n className={[\n 'flex-none w-6 h-6 inline-flex items-center justify-center rounded-full text-xs font-bold',\n isActive ? 'bg-gold-dim text-black' : 'bg-black/30 text-text-primary',\n ].join(' ')}\n >\n {i + 1}\n </span>\n <span\n className={[\n 'flex-1 min-w-0 truncate',\n isEmpty ? 'italic text-text-muted' : 'font-medium',\n ].join(' ')}\n >\n {isEmpty ? 'Empty' : label}\n </span>\n {isEmpty && onAssignTrackedToSlot && (\n <button\n type=\"button\"\n onClick={e => {\n e.stopPropagation()\n onAssignTrackedToSlot(i)\n }}\n aria-label={`Assign currently tracked program to slot ${i + 1}`}\n title=\"Assign currently tracked program here\"\n className=\"flex-none p-1 rounded text-text-muted hover:text-gold-dim hover:bg-black/20 transition-colors\"\n >\n <PlusIcon />\n </button>\n )}\n {!isEmpty && onProgramSlot && slotProgrammable && (\n <button\n type=\"button\"\n onClick={e => {\n e.stopPropagation()\n onProgramSlot(i)\n }}\n disabled={programmingSlot !== null}\n aria-label={`Write slot ${i + 1} to pedal`}\n title=\"Write this slot to pedal\"\n className=\"flex-none p-1 rounded text-text-muted hover:text-gold-dim hover:bg-black/20 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n >\n {programmingSlot === i ? <Spinner small /> : <ArrowDownToBox small />}\n </button>\n )}\n {!isEmpty && onUnassignSlot && (\n <button\n type=\"button\"\n onClick={e => {\n e.stopPropagation()\n onUnassignSlot(i)\n }}\n aria-label={`Unassign slot ${i + 1}`}\n title=\"Unassign\"\n className=\"flex-none p-1 rounded text-text-muted hover:text-red-400 hover:bg-black/20 transition-colors\"\n >\n <TrashIcon />\n </button>\n )}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\ninterface KnobOverlayProps {\n posStyle: { left: string; top: string }\n sizePct: number\n value: number\n label: string\n onChange: (value: number) => void\n}\n\nfunction KnobOverlay({ posStyle, sizePct, value, label, onChange }: KnobOverlayProps) {\n return (\n <div\n className=\"absolute\"\n style={{\n left: posStyle.left,\n top: posStyle.top,\n width: `${sizePct}%`,\n aspectRatio: '1 / 1',\n transform: 'translate(-50%, -50%)',\n }}\n >\n <Knob overlay value={value} label={label} onChange={onChange} />\n </div>\n )\n}\n\ninterface PotLabelProps {\n posStyle: { left: string; top: string }\n text: string\n}\n\nfunction PotLabel({ posStyle, text }: PotLabelProps) {\n return (\n <div\n className=\"absolute text-center text-[11px] sm:text-xs font-semibold text-black whitespace-nowrap pointer-events-none\"\n style={{\n left: posStyle.left,\n top: posStyle.top,\n transform: 'translate(-50%, -50%)',\n }}\n >\n {text}\n </div>\n )\n}\n\ninterface IconButtonProps {\n onClick?: () => void\n disabled?: boolean\n busy?: boolean\n label: string\n children: React.ReactNode\n}\n\nfunction IconButton({ onClick, disabled, busy, label, children }: IconButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-label={label}\n title={label}\n className=\"p-1 rounded border border-border bg-surface-card text-text-primary hover:border-gold-dim hover:text-gold-dim disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n >\n {busy ? <Spinner /> : children}\n </button>\n )\n}\n\nfunction Spinner({ small = false }: { small?: boolean }) {\n const cls = small ? 'w-3.5 h-3.5 animate-spin' : 'w-4 h-4 animate-spin'\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={cls}>\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n )\n}\n\nfunction ArrowUpFromBox() {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"w-4 h-4\">\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n <polyline points=\"17 8 12 3 7 8\" />\n <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\" />\n </svg>\n )\n}\n\nfunction ArrowDownToBox({ small = false }: { small?: boolean }) {\n const cls = small ? 'w-3.5 h-3.5' : 'w-4 h-4'\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={cls}>\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n <polyline points=\"7 10 12 15 17 10\" />\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\" />\n </svg>\n )\n}\n\nfunction PlusIcon() {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"w-3.5 h-3.5\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n )\n}\n\nfunction TrashIcon() {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"w-3.5 h-3.5\">\n <path d=\"M3 6h18\" />\n <path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\" />\n <path d=\"M10 11v6M14 11v6\" />\n </svg>\n )\n}\n","/**\n * Converts a compiled FV-1 binary (Uint8Array, big-endian) to an array of\n * 32-bit machine code words suitable for FV1Simulator.loadProgram().\n *\n * An FV-1 program is 128 instructions × 4 bytes = 512 bytes.\n */\nexport function binaryToMachineCode(binary: Uint8Array): number[] {\n const words: number[] = []\n const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength)\n for (let i = 0; i < binary.length; i += 4) {\n words.push(view.getUint32(i, false)) // big-endian\n }\n return words\n}\n","import { useRef, useState, useCallback, useEffect } from 'react'\nimport { FV1Assembler } from '@audiofab-io/fv1-core'\nimport type { FV1AssemblerProblem } from '@audiofab-io/fv1-core'\nimport { binaryToMachineCode } from '../audio/binary'\n\nexport type ChannelMode = 'mono' | 'stereo'\n\nexport interface SimulatorState {\n /** True when audio is currently playing. */\n playing: boolean\n /** True when the bypass footswitch is engaged (signal passes through dry). */\n bypassed: boolean\n /** Output channel mode — see ChannelMode. */\n channelMode: ChannelMode\n /** Sample rate of the AudioContext (set after init). */\n sampleRate: number | null\n}\n\nexport interface UseSimulatorOptions {\n /**\n * URL of the bundled FV-1 audio worklet processor. Required.\n *\n * In Vite-based consumers, prefer:\n * import workletUrl from '@audiofab-io/easy-spin-ui/worklet?url'\n *\n * In VS Code webview consumers, derive the URL via\n * webview.asWebviewUri(...)\n * after copying `node_modules/@audiofab-io/easy-spin-ui/dist/worklet/fv1-processor.js`\n * into your webview's resource directory.\n */\n workletUrl: string\n /** AudioContext sample rate. Defaults to 32 768 Hz to match the FV-1. */\n sampleRate?: number\n}\n\nexport interface AssembleResult {\n success: boolean\n errors?: FV1AssemblerProblem[]\n}\n\n/**\n * Headless hook that drives an off-DOM AudioContext + FV-1 audio worklet.\n *\n * The hook is deliberately decoupled from any \"where do programs come from\"\n * concern — consumers fetch / compile / load binaries themselves and pass\n * them in via `loadProgram`. The same applies to audio clips.\n */\nexport function useSimulator({ workletUrl, sampleRate = 32768 }: UseSimulatorOptions) {\n const ctxRef = useRef<AudioContext | null>(null)\n const workletRef = useRef<AudioWorkletNode | null>(null)\n const sourceRef = useRef<AudioBufferSourceNode | null>(null)\n const clipBufferRef = useRef<AudioBuffer | null>(null)\n const initPromiseRef = useRef<Promise<void> | null>(null)\n\n const [state, setState] = useState<SimulatorState>({\n playing: false,\n bypassed: false,\n channelMode: 'mono',\n sampleRate: null,\n })\n\n const init = useCallback(async () => {\n if (ctxRef.current) return\n if (initPromiseRef.current) return initPromiseRef.current\n\n initPromiseRef.current = (async () => {\n const ctx = new AudioContext({ sampleRate })\n await ctx.audioWorklet.addModule(workletUrl)\n const worklet = new AudioWorkletNode(ctx, 'fv1-processor', {\n outputChannelCount: [2],\n })\n worklet.connect(ctx.destination)\n ctxRef.current = ctx\n workletRef.current = worklet\n setState(s => ({ ...s, sampleRate: ctx.sampleRate }))\n })()\n\n return initPromiseRef.current\n }, [workletUrl, sampleRate])\n\n /** Load a pre-compiled FV-1 binary (512 bytes, big-endian) into the simulator. */\n const loadProgram = useCallback(async (binary: Uint8Array) => {\n await init()\n const code = binaryToMachineCode(binary)\n workletRef.current!.port.postMessage({\n type: 'loadProgram',\n machineCode: code,\n })\n }, [init])\n\n /**\n * Convenience: assemble FV-1 source and load the result.\n *\n * Returns `{ success: false, errors }` on fatal assembler errors so callers\n * can surface them. Non-fatal warnings are ignored here.\n */\n const loadProgramFromSource = useCallback(async (spnSource: string): Promise<AssembleResult> => {\n await init()\n const assembler = new FV1Assembler()\n const result = assembler.assemble(spnSource)\n const fatal = result.problems.filter(p => p.isfatal)\n if (fatal.length > 0) {\n return { success: false, errors: fatal }\n }\n const binary = FV1Assembler.toUint8Array(result.machineCode)\n const code = binaryToMachineCode(binary)\n workletRef.current!.port.postMessage({\n type: 'loadProgram',\n machineCode: code,\n })\n return { success: true }\n }, [init])\n\n /**\n * Decode an audio clip from raw bytes and stage it as the simulator's input.\n * If audio is currently playing, the new clip starts immediately; otherwise\n * it's just held until `play()` is called.\n */\n const loadClipBuffer = useCallback(async (clipBytes: ArrayBuffer | Uint8Array) => {\n await init()\n const ctx = ctxRef.current!\n const wasPlaying = sourceRef.current !== null\n\n if (sourceRef.current) {\n try { sourceRef.current.stop() } catch { /* already stopped */ }\n sourceRef.current.disconnect()\n sourceRef.current = null\n }\n\n const ab = clipBytes instanceof ArrayBuffer\n ? clipBytes\n : (clipBytes.buffer.slice(clipBytes.byteOffset, clipBytes.byteOffset + clipBytes.byteLength) as ArrayBuffer)\n const buffer = await ctx.decodeAudioData(ab)\n clipBufferRef.current = buffer\n\n if (wasPlaying) {\n const source = ctx.createBufferSource()\n source.buffer = buffer\n source.loop = true\n source.connect(workletRef.current!)\n source.start()\n sourceRef.current = source\n await ctx.resume()\n }\n }, [init])\n\n const play = useCallback(async () => {\n await init()\n const ctx = ctxRef.current!\n const buffer = clipBufferRef.current\n if (!buffer) return\n\n // AudioBufferSourceNode is single-use — create a fresh one each time.\n if (sourceRef.current) {\n try { sourceRef.current.stop() } catch { /* already stopped */ }\n sourceRef.current.disconnect()\n }\n\n const source = ctx.createBufferSource()\n source.buffer = buffer\n source.loop = true\n source.connect(workletRef.current!)\n source.start()\n sourceRef.current = source\n await ctx.resume()\n\n setState(s => ({ ...s, playing: true }))\n }, [init])\n\n const pause = useCallback(() => {\n if (sourceRef.current) {\n try { sourceRef.current.stop() } catch { /* already stopped */ }\n sourceRef.current.disconnect()\n sourceRef.current = null\n }\n ctxRef.current?.suspend()\n setState(s => ({ ...s, playing: false }))\n }, [])\n\n const setPot = useCallback((index: number, value: number) => {\n workletRef.current?.port.postMessage({ type: 'setPot', index, value })\n }, [])\n\n const setBypass = useCallback((active: boolean) => {\n workletRef.current?.port.postMessage({ type: 'bypass', active })\n setState(s => ({ ...s, bypassed: active }))\n }, [])\n\n const setChannelMode = useCallback((mode: ChannelMode) => {\n workletRef.current?.port.postMessage({ type: 'setChannelMode', mode })\n setState(s => ({ ...s, channelMode: mode }))\n }, [])\n\n // Tear down the AudioContext on unmount to avoid leaking audio threads.\n useEffect(() => {\n return () => {\n if (sourceRef.current) {\n try { sourceRef.current.stop() } catch { /* already stopped */ }\n sourceRef.current.disconnect()\n }\n ctxRef.current?.close().catch(() => { /* already closed */ })\n }\n }, [])\n\n return {\n ...state,\n loadProgram,\n loadProgramFromSource,\n loadClipBuffer,\n play,\n pause,\n setPot,\n setBypass,\n setChannelMode,\n }\n}\n"],"mappings":";;;;;AAYA,IAAM,IAAY,KACZ,IAAU,CAAC,IAAY;AAE7B,SAAgB,EAAK,EAAE,UAAO,UAAO,aAAU,UAAO,IAAI,aAAU,MAAoB;CACtF,IAAM,IAAU,EAAsB,KAAK,EACrC,IAAW,EAAO,GAAM,EACxB,IAAa,EAAO,EAAE,EACtB,IAAa,EAAO,EAAE,EAEtB,IAAoB,GAAa,MAAoB;EACzD,IAAM,IAAK,EAAQ;AACnB,MAAI,CAAC,EAAI,QAAO;EAChB,IAAM,IAAO,EAAG,uBAAuB,EACjC,IAAK,EAAK,OAAO,EAAK,QAAQ,GAC9B,IAAK,EAAK,MAAM,EAAK,SAAS;AACpC,SAAO,KAAK,MAAM,EAAE,UAAU,GAAI,EAAE,UAAU,EAAG,IAAI,MAAM,KAAK;IAC/D,EAAE,CAAC,EAEA,IAAgB,GAAa,MAA0B;AAIzD,EAHF,EAAS,UAAU,IACnB,EAAW,UAAU,EAAkB,EAAE,YAAY,EACrD,EAAW,UAAU,GACnB,EAAE,OAAmB,kBAAkB,EAAE,UAAU;IACpD,CAAC,GAAmB,EAAM,CAAC,EAExB,IAAgB,GAAa,MAA0B;AAC3D,MAAI,CAAC,EAAS,QAAS;EAIvB,IAAM,MAHQ,EAAkB,EAAE,YACpB,GAAQ,EAAW,UACV,OAAO,MAAO,OACX;AAE1B,IADiB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,EAAW,UAAU,EAAW,CAChE,CAAS;IACjB,CAAC,GAAmB,EAAS,CAAC,EAE3B,IAAc,QAAkB;AACpC,IAAS,UAAU;IAClB,EAAE,CAAC,EAEA,IAAW,IAAU,IAAQ,GAE7B,IACJ,kBAAC,OAAD;EACE,KAAK;EACL,OAAO,IAAU,SAAS;EAC1B,QAAQ,IAAU,SAAS;EAC3B,SAAQ;EACR,WAAU;EACV,MAAK;EACL,iBAAe,KAAK,MAAM,IAAQ,IAAI;EACtC,iBAAe;EACf,iBAAe;EACf,cAAY;EACG;EACA;EACF;YAbf;GAeE,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAgB,CAAA;GACnD,IACC,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAO,QAAO;IAAO,aAAY;IAAQ,CAAA,GAE7E,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAO,QAAO;IAAO,aAAY;IAAQ,CAAA,EAC7E,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAU,QAAO;IAAO,aAAY;IAAM,CAAA,CAC7E,EAAA,CAAA;GAEL,kBAAC,KAAD;IAAG,WAAW,UAAU,EAAS;cAC/B,kBAAC,QAAD;KACE,IAAG;KACH,IAAG;KACH,IAAG;KACH,IAAI,IAAU,KAAK;KACnB,QAAQ,IAAU,SAAS;KAC3B,aAAa,IAAU,MAAM;KAC7B,eAAc;KACd,CAAA;IACA,CAAA;GACA;;AAOR,QAJI,IACK,IAIP,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,GACD,kBAAC,QAAD;GAAM,WAAU;aACb;GACI,CAAA,CACH;;;;;AC3FV,IAAM,IAAY,GACZ,IAAkB,KAElB,KAAY,MAAgB,MAAoB,IAAY;AAElE,SAAS,EAAiB,GAAwB;AAChD,QAAO,IAAkB,IAAS;;AAGpC,SAAS,EAA4B,GAA4B;CAC/D,IAAM,KAAM,IAAa,MAAO,OAAO,KACnC,IAAW,GACX,IAAW;AACf,MAAK,IAAI,IAAI,GAAG,IAAI,GAAW,KAAK;EAClC,IAAM,KAAc,EAAiB,EAAE,GAAG,MAAO,OAAO,KACpD,IAAO,KAAK,IAAI,IAAI,EAAU;AAElC,EADI,IAAO,QAAK,IAAO,MAAM,IACzB,IAAO,MACT,IAAW,GACX,IAAW;;AAGf,QAAO;;AAST,SAAgB,EAAgB,EAC9B,iBACA,iBACA,eACA,aAAU,MACa;CACvB,IAAM,IAAU,EAAsB,KAAK,EACrC,IAAW,EAAO,GAAM,EAExB,IAAoB,GAAa,MAAoB;EACzD,IAAM,IAAK,EAAQ;AACnB,MAAI,CAAC,EAAI,QAAO;EAChB,IAAM,IAAO,EAAG,uBAAuB,EACjC,IAAK,EAAK,OAAO,EAAK,QAAQ,GAC9B,IAAK,EAAK,MAAM,EAAK,SAAS;AAEpC,UADY,KAAK,MAAM,EAAE,UAAU,GAAI,EAAE,UAAU,EAAG,IAAI,MAAM,KAAK,MACtD,MAAO,OAAO;IAC5B,EAAE,CAAC,EAEA,IAAgB,GAAa,MAA0B;AAEzD,EADF,EAAS,UAAU,IACjB,EAAE,OAAmB,kBAAkB,EAAE,UAAU;EAErD,IAAM,IAAU,EADF,EAAkB,EAAE,YACU,CAAM;AAClD,EAAI,MAAY,KAAc,EAAa,EAAQ;IAClD;EAAC;EAAmB;EAAc;EAAa,CAAC,EAE7C,IAAgB,GAAa,MAA0B;AAC3D,MAAI,CAAC,EAAS,QAAS;EAEvB,IAAM,IAAU,EADF,EAAkB,EAAE,YACU,CAAM;AAClD,EAAI,MAAY,KAAc,EAAa,EAAQ;IAClD;EAAC;EAAmB;EAAc;EAAa,CAAC,EAE7C,IAAc,QAAkB;AACpC,IAAS,UAAU;IAClB,EAAE,CAAC;AAEN,KAAI,CAAC,EACH,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,MAAM,KAAK,EAAE,QAAQ,GAAW,GAAG,GAAG,MACrC,kBAAC,UAAD;GAEE,eAAe,EAAa,EAAE;GAC9B,OAAO,IAAa,MAAM,QAAQ,IAAI;GACtC,WAAW,qCAAqC,MAAiB,IAC3D,yBACA;aAGL,IAAI;GACE,EATF,EASE,CACT;EACE,CAAA;CAIV,IAAM,IAAW,EAAiB,EAAa,GAAG;AAElD,QACE,kBAAC,OAAD;EACE,KAAK;EACL,OAAM;EACN,QAAO;EACP,SAAQ;EACR,WAAU;EACV,MAAK;EACL,cAAW;EACX,yBAAuB,aAAa;EACrB;EACA;EACF;YAXf;GAaE,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAgB,CAAA;GACpD,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAO,QAAO;IAAO,aAAY;IAAQ,CAAA;GAC7E,kBAAC,KAAD;IAAG,WAAW,UAAU,EAAS;cAC/B,kBAAC,QAAD;KACE,IAAG;KACH,IAAG;KACH,IAAG;KACH,IAAG;KACH,QAAO;KACP,aAAY;KACZ,eAAc;KACd,CAAA;IACA,CAAA;GACA;;;;;AC7GV,SAAgB,EAAa,EAAE,UAAO,mBAAgB,aAAU,cAAW,MAA4B;AACrG,QACE,kBAAC,UAAD;EACE,OAAO,KAAkB;EACzB,WAAU,MAAK,EAAS,EAAE,OAAO,MAAM;EAC7B;EACV,WAAU;YAJZ,CAME,kBAAC,UAAD;GAAQ,OAAM;GAAG,UAAA;aACd,IAAW,aAAa;GAClB,CAAA,EACR,EAAM,KAAI,MACT,kBAAC,UAAD;GAAsB,OAAO,EAAK;aAC/B,EAAK;GACC,EAFI,EAAK,GAET,CACT,CACK;;;;;ACkCb,IAAM,IAAM;CACV,MAAiB;EAAE,MAAM;EAAU,KAAK;EAAO;CAC/C,MAAiB;EAAE,MAAM;EAAU,KAAK;EAAO;CAC/C,MAAiB;EAAE,MAAM;EAAU,KAAK;EAAU;CAClD,iBAAiB;EAAE,MAAM;EAAO,KAAK;EAAU;CAC/C,KAAiB;EAAE,MAAM;EAAO,KAAK;EAAO;CAC5C,YAAiB;EAAE,MAAM;EAAO,KAAK;EAAS;CAC9C,WAAiB;EAAE,MAAM;EAAU,KAAK;EAAQ;CAChD,WAAiB;EAAE,MAAM;EAAU,KAAK;EAAQ;CAChD,WAAiB;EAAE,MAAM;EAAU,KAAK;EAAO;CAC/C,SAAiB;EAAE,MAAM;EAAQ,KAAK;EAAO;CAC9C,EAEK,IAAgB,IAChB,IAAsB,IACtB,IAAmB,IACnB,IAAe;AAErB,SAAgB,EAAU,EACxB,kBACA,SACA,gBACA,eAAY;CAAC;CAAS;CAAS;CAAQ,EACvC,iBACA,iBACA,eACA,iBACA,mBACA,uBACA,kBACA,qBAAkB,MAClB,0BACA,UACA,mBACA,iBACA,YACA,WACA,YACA,aACA,mBACA,gBACA,wBACA,oBAAiB,IACjB,mBACA,gBACA,iBACA,kBAAe,IACf,kBAAe,MACE;CACjB,IAAM,IAAY,KAAgB,GAC5B,CAAC,GAAc,KAAmB,EAAwB,KAAK,EAE/D,IAAQ,CAAC,GACT,IAAc,GAAY,MAAK,MAAK,EAAE,IAAI,IAC1C,KAAmB,KAAkB,CAAC;AAE5C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IACE,WAAU;IACV,OAAO;KACL,aAAa;KACb,iBAAiB,OAAO,EAAc;KACtC,gBAAgB;KAChB,kBAAkB;KAClB,oBAAoB;KACrB;cARH;KAUE,kBAAC,GAAD;MACE,UAAU,EAAI;MACd,SAAS;MACT,OAAO,EAAK;MACZ,OAAO,EAAU;MACjB,WAAU,MAAK,EAAY,GAAG,EAAE;MAChC,CAAA;KACF,kBAAC,GAAD;MACE,UAAU,EAAI;MACd,SAAS;MACT,OAAO,EAAK;MACZ,OAAO,EAAU;MACjB,WAAU,MAAK,EAAY,GAAG,EAAE;MAChC,CAAA;KACF,kBAAC,GAAD;MACE,UAAU,EAAI;MACd,SAAS;MACT,OAAO,EAAK;MACZ,OAAO,EAAU;MACjB,WAAU,MAAK,EAAY,GAAG,EAAE;MAChC,CAAA;KAEF,kBAAC,GAAD;MAAU,UAAU,EAAI;MAAW,MAAM,EAAU;MAAM,CAAA;KACzD,kBAAC,GAAD;MAAU,UAAU,EAAI;MAAW,MAAM,EAAU;MAAM,CAAA;KACzD,kBAAC,GAAD;MAAU,UAAU,EAAI;MAAW,MAAM,EAAU;MAAM,CAAA;KAGzD,kBAAC,OAAD;MACE,WAAU;MACV,OAAO;OACL,MAAM,EAAI,gBAAgB;OAC1B,KAAK,EAAI,gBAAgB;OACzB,OAAO,GAAG,EAAc;OACxB,aAAa;OACb,WAAW;OACZ;gBAED,kBAAC,GAAD;OACE,SAAA;OACc;OACA;OACF;OACZ,CAAA;MACE,CAAA;KAGN,kBAAC,OAAD;MACE,WAAU;MACV,OAAO;OACL,MAAM,EAAI,IAAI;OACd,KAAK,EAAI,IAAI;OACb,OAAO,GAAG,EAAa;OACvB,aAAa;OACb,WAAW;OACX,YAAY,IACR,wEACA;OACJ,WAAW,IACP,qCACA;OACJ,QAAQ;OACT;MACD,cAAW;MACX,CAAA;KAGF,kBAAC,OAAD;MACE,SAAQ;MACR,WAAU;MACV,OAAO;OACL,MAAM,EAAI,WAAW;OACrB,KAAK,EAAI,WAAW;OACpB,OAAO,GAAG,EAAiB;OAC3B,aAAa;OACb,WAAW;OACX,QAAQ;OACT;gBAVH,CAYE,kBAAC,QAAD,EAAA,UACE,kBAAC,kBAAD;OAAgB,IAAG;OAAe,IAAG;OAAM,IAAG;OAAM,GAAE;iBAAtD;QACE,kBAAC,QAAD;SAAM,QAAO;SAAK,WAAU;SAAY,CAAA;QACxC,kBAAC,QAAD;SAAM,QAAO;SAAM,WAAU;SAAY,CAAA;QACzC,kBAAC,QAAD;SAAM,QAAO;SAAO,WAAU;SAAY,CAAA;QAC3B;UACZ,CAAA,EACP,kBAAC,WAAD;OACE,QAAO;OACP,MAAK;OACL,QAAO;OACP,aAAY;OACZ,gBAAe;OACf,CAAA,CACE;;MAGJ,KAAkB,KAAe,MACjC,kBAAC,OAAD;MACE,WAAU;MACV,OAAO;OACL,MAAM,EAAI,QAAQ;OAClB,KAAK,EAAI,QAAQ;OACjB,WAAW;OACZ;gBAEC,IAWA,kBAAA,GAAA,EAAA,UAAA,CACG,KACC,kBAAC,GAAD;OACE,SAAS;OACT,UAAU;OACV,MAAM;OACN,OAAM;iBAEN,kBAAC,GAAD,EAAkB,CAAA;OACP,CAAA,EAEd,KACC,kBAAC,GAAD;OACE,SAAS;OACT,UAAU;OACV,MAAM;OACN,OAAM;iBAEN,kBAAC,GAAD,EAAkB,CAAA;OACP,CAAA,CAEd,EAAA,CAAA,GA/BH,KACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBACX;OAEQ,CAAA;MA0BT,CAAA;KAIR,kBAAC,UAAD;MACE,SAAS;MACT,cAAY,IAAW,+BAA+B;MACtD,WAAU;MACV,OAAO;OACL,MAAM,EAAI,WAAW;OACrB,KAAK,EAAI,WAAW;OACpB,OAAO,GAAG,EAAoB;OAC9B,aAAa;OACb,WAAW;OACX,YAAY;OACZ,QAAQ;OACR,WAAW;OACZ;gBAED,kBAAC,QAAD;OACE,WAAU;OACV,OAAO;QACL,MAAM;QACN,KAAK;QACL,OAAO;QACP,aAAa;QACb,WAAW;QACX,YAAY;QACZ,QAAQ;QACR,WAAW;QACZ;OACD,CAAA;MACK,CAAA;KACL;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,UAAD;MACE,SAAS,IAAU,IAAU;MAC7B,WAAU;gBAET,IAAU,UAAU;MACd,CAAA;KACR,KAAS,EAAM,SAAS,KACvB,kBAAC,GAAD;MACS;MACS;MAChB,UAAU;MACV,CAAA;KAEJ,kBAAC,OAAD;MACE,MAAK;MACL,cAAW;MACX,OAAM;MACN,WAAU;gBAER,CAAC,QAAQ,SAAS,CAAW,KAAI,MACjC,kBAAC,UAAD;OAEE,MAAK;OACL,MAAK;OACL,gBAAc,MAAgB;OAC9B,eAAe,EAAoB,EAAK;OACxC,WAAW,+CACT,MAAgB,IACZ,yBACA;iBAGL,MAAS,SAAS,SAAS;OACrB,EAZF,EAYE,CACT;MACE,CAAA;KACF;;GAIN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAiE;MAE1E,CAAA,EACJ,KAAe,KACd,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,WAAU;MACV,OAAM;gBACP;MAEQ,CAAA,CAEP;QACN,kBAAC,OAAD;KAAK,WAAU;eACd,MAAM,KAAK,EAAE,QAAQ,GAAoB,GAAG,GAAG,MAAM;MACpD,IAAM,IAAW,MAAiB,GAC5B,IAAa,MAAiB,GAC9B,IAAQ,IAAa,IACrB,IAAU,CAAC;AACjB,aACE,kBAAC,OAAD;OAEE,MAAK;OACL,UAAU;OACV,eAAe,EAAa,EAAE;OAC9B,YAAW,MAAK;AACd,SAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,EAAa,EAAE;;OAGnB,aAAY,MAAK;AACV,cACL,EAAE,gBAAgB,EAClB,EAAE,aAAa,aAAa,QACxB,MAAiB,KAAG,EAAgB,EAAE;;OAE5C,mBAAmB,EAAgB,KAAK;OACxC,SAAQ,MAAK;AACX,YAAI,CAAC,EAAc;AAEnB,QADA,EAAE,gBAAgB,EAClB,EAAgB,KAAK;QAIrB,IAAI,IAAU,EAAE,aAAa,QAAQ,aAAa;AAClD,YAAI,CAAC,GAAS;SACZ,IAAM,IAAU,EAAE,aAAa,QAAQ,gBAAgB;AACvD,SAAI,MACF,IAAU,EAAQ,MAAM,KAAK,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC,MAAK,MAAK,KAAK,CAAC,EAAE,WAAW,IAAI,CAAC,IAAI;;AAG3F,QAAI,KAAS,EAAa,GAAG,EAAQ;;OAEvC,WAAW;QACT;QACA;QACA,IACI,8DACA;QACJ,IAAa,iDAAiD;QAC/D,CAAC,KAAK,IAAI;iBAzCb;QA2CE,kBAAC,QAAD;SACE,WAAW,CACT,4FACA,IAAW,2BAA2B,gCACvC,CAAC,KAAK,IAAI;mBAEV,IAAI;SACA,CAAA;QACP,kBAAC,QAAD;SACE,WAAW,CACT,2BACA,IAAU,2BAA2B,cACtC,CAAC,KAAK,IAAI;mBAEV,IAAU,UAAU;SAChB,CAAA;QACN,KAAW,KACV,kBAAC,UAAD;SACE,MAAK;SACL,UAAS,MAAK;AAEZ,UADA,EAAE,iBAAiB,EACnB,EAAsB,EAAE;;SAE1B,cAAY,4CAA4C,IAAI;SAC5D,OAAM;SACN,WAAU;mBAEV,kBAAC,GAAD,EAAY,CAAA;SACL,CAAA;QAEV,CAAC,KAAW,KAAiB,MAC5B,kBAAC,UAAD;SACE,MAAK;SACL,UAAS,MAAK;AAEZ,UADA,EAAE,iBAAiB,EACnB,EAAc,EAAE;;SAElB,UAAU,MAAoB;SAC9B,cAAY,cAAc,IAAI,EAAE;SAChC,OAAM;SACN,WAAU;mBAEe,EAAxB,MAAoB,IAAK,IAAoB,GAArB,EAAS,OAAA,IAAQ,CAA2B;SAC9D,CAAA;QAEV,CAAC,KAAW,KACX,kBAAC,UAAD;SACE,MAAK;SACL,UAAS,MAAK;AAEZ,UADA,EAAE,iBAAiB,EACnB,EAAe,EAAE;;SAEnB,cAAY,iBAAiB,IAAI;SACjC,OAAM;SACN,WAAU;mBAEV,kBAAC,GAAD,EAAa,CAAA;SACN,CAAA;QAEP;SArGC,EAqGD;OAER;KACI,CAAA,CACF;;GACF;;;AAYV,SAAS,EAAY,EAAE,aAAU,YAAS,UAAO,UAAO,eAA8B;AACpF,QACE,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GACL,MAAM,EAAS;GACf,KAAK,EAAS;GACd,OAAO,GAAG,EAAQ;GAClB,aAAa;GACb,WAAW;GACZ;YAED,kBAAC,GAAD;GAAM,SAAA;GAAe;GAAc;GAAiB;GAAY,CAAA;EAC5D,CAAA;;AASV,SAAS,EAAS,EAAE,aAAU,WAAuB;AACnD,QACE,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GACL,MAAM,EAAS;GACf,KAAK,EAAS;GACd,WAAW;GACZ;YAEA;EACG,CAAA;;AAYV,SAAS,EAAW,EAAE,YAAS,aAAU,SAAM,UAAO,eAA6B;AACjF,QACE,kBAAC,UAAD;EACE,MAAK;EACI;EACC;EACV,cAAY;EACZ,OAAO;EACP,WAAU;YAET,IAAO,kBAAC,GAAD,EAAW,CAAA,GAAG;EACf,CAAA;;AAIb,SAAS,EAAQ,EAAE,WAAQ,MAA8B;AAEvD,QACE,kBAAC,OAAD;EAAK,SAAQ;EAAY,MAAK;EAAO,QAAO;EAAe,aAAY;EAAI,eAAc;EAAQ,gBAAe;EAAQ,WAF9G,IAAQ,6BAA6B;YAG7C,kBAAC,QAAD,EAAM,GAAE,+BAAgC,CAAA;EACpC,CAAA;;AAIV,SAAS,IAAiB;AACxB,QACE,kBAAC,OAAD;EAAK,SAAQ;EAAY,MAAK;EAAO,QAAO;EAAe,aAAY;EAAI,eAAc;EAAQ,gBAAe;EAAQ,WAAU;YAAlI;GACE,kBAAC,QAAD,EAAM,GAAE,6CAA8C,CAAA;GACtD,kBAAC,YAAD,EAAU,QAAO,iBAAkB,CAAA;GACnC,kBAAC,QAAD;IAAM,IAAG;IAAK,IAAG;IAAI,IAAG;IAAK,IAAG;IAAO,CAAA;GACnC;;;AAIV,SAAS,EAAe,EAAE,WAAQ,MAA8B;AAE9D,QACE,kBAAC,OAAD;EAAK,SAAQ;EAAY,MAAK;EAAO,QAAO;EAAe,aAAY;EAAI,eAAc;EAAQ,gBAAe;EAAQ,WAF9G,IAAQ,gBAAgB;YAElC;GACE,kBAAC,QAAD,EAAM,GAAE,6CAA8C,CAAA;GACtD,kBAAC,YAAD,EAAU,QAAO,oBAAqB,CAAA;GACtC,kBAAC,QAAD;IAAM,IAAG;IAAK,IAAG;IAAK,IAAG;IAAK,IAAG;IAAM,CAAA;GACnC;;;AAIV,SAAS,IAAW;AAClB,QACE,kBAAC,OAAD;EAAK,SAAQ;EAAY,MAAK;EAAO,QAAO;EAAe,aAAY;EAAI,eAAc;EAAQ,gBAAe;EAAQ,WAAU;YAAlI,CACE,kBAAC,QAAD;GAAM,IAAG;GAAK,IAAG;GAAI,IAAG;GAAK,IAAG;GAAO,CAAA,EACvC,kBAAC,QAAD;GAAM,IAAG;GAAI,IAAG;GAAK,IAAG;GAAK,IAAG;GAAO,CAAA,CACnC;;;AAIV,SAAS,IAAY;AACnB,QACE,kBAAC,OAAD;EAAK,SAAQ;EAAY,MAAK;EAAO,QAAO;EAAe,aAAY;EAAI,eAAc;EAAQ,gBAAe;EAAQ,WAAU;YAAlI;GACE,kBAAC,QAAD,EAAM,GAAE,WAAY,CAAA;GACpB,kBAAC,QAAD,EAAM,GAAE,0CAA2C,CAAA;GACnD,kBAAC,QAAD,EAAM,GAAE,4CAA6C,CAAA;GACrD,kBAAC,QAAD,EAAM,GAAE,oBAAqB,CAAA;GACzB;;;;;ACnlBV,SAAgB,EAAoB,GAA8B;CAChE,IAAM,IAAkB,EAAE,EACpB,IAAO,IAAI,SAAS,EAAO,QAAQ,EAAO,YAAY,EAAO,WAAW;AAC9E,MAAK,IAAI,IAAI,GAAG,IAAI,EAAO,QAAQ,KAAK,EACtC,GAAM,KAAK,EAAK,UAAU,GAAG,GAAM,CAAC;AAEtC,QAAO;;;;ACmCT,SAAgB,EAAa,EAAE,eAAY,gBAAa,SAA8B;CACpF,IAAM,IAAS,EAA4B,KAAK,EAC1C,IAAa,EAAgC,KAAK,EAClD,IAAY,EAAqC,KAAK,EACtD,IAAgB,EAA2B,KAAK,EAChD,IAAiB,EAA6B,KAAK,EAEnD,CAAC,GAAO,KAAY,EAAyB;EACjD,SAAS;EACT,UAAU;EACV,aAAa;EACb,YAAY;EACb,CAAC,EAEI,IAAO,EAAY,YAAY;AAC/B,SAAO,QAeX,QAdI,AAEJ,EAAe,aAAW,YAAY;GACpC,IAAM,IAAM,IAAI,aAAa,EAAE,eAAY,CAAC;AAC5C,SAAM,EAAI,aAAa,UAAU,EAAW;GAC5C,IAAM,IAAU,IAAI,iBAAiB,GAAK,iBAAiB,EACzD,oBAAoB,CAAC,EAAE,EACxB,CAAC;AAIF,GAHA,EAAQ,QAAQ,EAAI,YAAY,EAChC,EAAO,UAAU,GACjB,EAAW,UAAU,GACrB,GAAS,OAAM;IAAE,GAAG;IAAG,YAAY,EAAI;IAAY,EAAE;MACnD,EAZ+B,EAAe;IAejD,CAAC,GAAY,EAAW,CAAC,EAGtB,IAAc,EAAY,OAAO,MAAuB;AAC5D,QAAM,GAAM;EACZ,IAAM,IAAO,EAAoB,EAAO;AACxC,IAAW,QAAS,KAAK,YAAY;GACnC,MAAM;GACN,aAAa;GACd,CAAC;IACD,CAAC,EAAK,CAAC,EAQJ,IAAwB,EAAY,OAAO,MAA+C;AAC9F,QAAM,GAAM;EAEZ,IAAM,IAAS,IADO,GACP,CAAU,SAAS,EAAU,EACtC,IAAQ,EAAO,SAAS,QAAO,MAAK,EAAE,QAAQ;AACpD,MAAI,EAAM,SAAS,EACjB,QAAO;GAAE,SAAS;GAAO,QAAQ;GAAO;EAG1C,IAAM,IAAO,EADE,EAAa,aAAa,EAAO,YACf,CAAO;AAKxC,SAJA,EAAW,QAAS,KAAK,YAAY;GACnC,MAAM;GACN,aAAa;GACd,CAAC,EACK,EAAE,SAAS,IAAM;IACvB,CAAC,EAAK,CAAC,EAOJ,IAAiB,EAAY,OAAO,MAAwC;AAChF,QAAM,GAAM;EACZ,IAAM,IAAM,EAAO,SACb,IAAa,EAAU,YAAY;AAEzC,MAAI,EAAU,SAAS;AACrB,OAAI;AAAE,MAAU,QAAQ,MAAM;WAAS;AAEvC,GADA,EAAU,QAAQ,YAAY,EAC9B,EAAU,UAAU;;EAGtB,IAAM,IAAK,aAAqB,cAC5B,IACC,EAAU,OAAO,MAAM,EAAU,YAAY,EAAU,aAAa,EAAU,WAAW,EACxF,IAAS,MAAM,EAAI,gBAAgB,EAAG;AAG5C,MAFA,EAAc,UAAU,GAEpB,GAAY;GACd,IAAM,IAAS,EAAI,oBAAoB;AAMvC,GALA,EAAO,SAAS,GAChB,EAAO,OAAO,IACd,EAAO,QAAQ,EAAW,QAAS,EACnC,EAAO,OAAO,EACd,EAAU,UAAU,GACpB,MAAM,EAAI,QAAQ;;IAEnB,CAAC,EAAK,CAAC,EAEJ,IAAO,EAAY,YAAY;AACnC,QAAM,GAAM;EACZ,IAAM,IAAM,EAAO,SACb,IAAS,EAAc;AAC7B,MAAI,CAAC,EAAQ;AAGb,MAAI,EAAU,SAAS;AACrB,OAAI;AAAE,MAAU,QAAQ,MAAM;WAAS;AACvC,KAAU,QAAQ,YAAY;;EAGhC,IAAM,IAAS,EAAI,oBAAoB;AAQvC,EAPA,EAAO,SAAS,GAChB,EAAO,OAAO,IACd,EAAO,QAAQ,EAAW,QAAS,EACnC,EAAO,OAAO,EACd,EAAU,UAAU,GACpB,MAAM,EAAI,QAAQ,EAElB,GAAS,OAAM;GAAE,GAAG;GAAG,SAAS;GAAM,EAAE;IACvC,CAAC,EAAK,CAAC,EAEJ,IAAQ,QAAkB;AAC9B,MAAI,EAAU,SAAS;AACrB,OAAI;AAAE,MAAU,QAAQ,MAAM;WAAS;AAEvC,GADA,EAAU,QAAQ,YAAY,EAC9B,EAAU,UAAU;;AAGtB,EADA,EAAO,SAAS,SAAS,EACzB,GAAS,OAAM;GAAE,GAAG;GAAG,SAAS;GAAO,EAAE;IACxC,EAAE,CAAC,EAEA,IAAS,GAAa,GAAe,MAAkB;AAC3D,IAAW,SAAS,KAAK,YAAY;GAAE,MAAM;GAAU;GAAO;GAAO,CAAC;IACrE,EAAE,CAAC,EAEA,IAAY,GAAa,MAAoB;AAEjD,EADA,EAAW,SAAS,KAAK,YAAY;GAAE,MAAM;GAAU;GAAQ,CAAC,EAChE,GAAS,OAAM;GAAE,GAAG;GAAG,UAAU;GAAQ,EAAE;IAC1C,EAAE,CAAC,EAEA,IAAiB,GAAa,MAAsB;AAExD,EADA,EAAW,SAAS,KAAK,YAAY;GAAE,MAAM;GAAkB;GAAM,CAAC,EACtE,GAAS,OAAM;GAAE,GAAG;GAAG,aAAa;GAAM,EAAE;IAC3C,EAAE,CAAC;AAaN,QAVA,cACe;AACX,MAAI,EAAU,SAAS;AACrB,OAAI;AAAE,MAAU,QAAQ,MAAM;WAAS;AACvC,KAAU,QAAQ,YAAY;;AAEhC,IAAO,SAAS,OAAO,CAAC,YAAY,GAAyB;IAE9D,EAAE,CAAC,EAEC;EACL,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@audiofab-io/easy-spin-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared React components and FV-1 simulation runtime for Audiofab's Easy Spin pedal — used by the Easy Spin web tool and the FV-1 VS Code extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|