@vsreact/core 0.0.5 → 0.0.7
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/button.d.ts +15 -0
- package/dist/button.js +20 -0
- package/dist/controls.d.ts +22 -6
- package/dist/controls.js +35 -13
- package/dist/hooks.d.ts +18 -0
- package/dist/hooks.js +56 -0
- package/dist/hostConfig.js +2 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/parameters.d.ts +3 -0
- package/dist/parameters.js +3 -1
- package/dist/popover.d.ts +27 -0
- package/dist/popover.js +55 -0
- package/dist/primitives.d.ts +10 -0
- package/dist/visualizers.d.ts +37 -0
- package/dist/visualizers.js +49 -0
- package/package.json +1 -1
- package/src/button.tsx +68 -0
- package/src/controls.test.tsx +240 -0
- package/src/controls.tsx +65 -12
- package/src/hooks.ts +71 -0
- package/src/hostConfig.ts +2 -0
- package/src/index.ts +17 -2
- package/src/parameters.ts +6 -1
- package/src/popover.tsx +137 -0
- package/src/primitives.tsx +9 -0
- package/src/visualizers.tsx +148 -0
package/src/controls.test.tsx
CHANGED
|
@@ -302,3 +302,243 @@ describe("onLayout + Select", () => {
|
|
|
302
302
|
expect(nativeCalls.some((c) => c.name === "param:end")).toBe(true);
|
|
303
303
|
});
|
|
304
304
|
});
|
|
305
|
+
|
|
306
|
+
describe("DAW feel (0.0.6)", () => {
|
|
307
|
+
test("Knob: double-click resets to defaultValue as a full gesture", () => {
|
|
308
|
+
const { Knob } = require("./index");
|
|
309
|
+
const calls: string[] = [];
|
|
310
|
+
render(
|
|
311
|
+
<Knob
|
|
312
|
+
value={0.9}
|
|
313
|
+
defaultValue={0.5}
|
|
314
|
+
onChange={(v: number) => calls.push(`set:${v}`)}
|
|
315
|
+
onBegin={() => calls.push("begin")}
|
|
316
|
+
onEnd={() => calls.push("end")}
|
|
317
|
+
/>,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const id = nodeWithListener("dblclick");
|
|
321
|
+
dispatch({ kind: "event", nodeId: id, type: "dblclick" });
|
|
322
|
+
expect(calls).toEqual(["begin", "set:0.5", "end"]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("Knob: wheel nudges by dy * sensitivity, clamped", () => {
|
|
326
|
+
const { Knob } = require("./index");
|
|
327
|
+
const seen: number[] = [];
|
|
328
|
+
render(<Knob value={0.5} wheelSensitivity={0.4} onChange={(v: number) => seen.push(v)} />);
|
|
329
|
+
|
|
330
|
+
const id = nodeWithListener("wheel");
|
|
331
|
+
dispatch({ kind: "event", nodeId: id, type: "wheel", payload: { dy: 0.1 } });
|
|
332
|
+
expect(seen.at(-1)).toBeCloseTo(0.54);
|
|
333
|
+
|
|
334
|
+
dispatch({ kind: "event", nodeId: id, type: "wheel", payload: { dy: 100 } });
|
|
335
|
+
expect(seen.at(-1)).toBe(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("Knob: bipolar arc sweeps from centre in both directions", () => {
|
|
339
|
+
const { Knob } = require("./index");
|
|
340
|
+
render(<Knob bipolar value={0.75} onChange={() => {}} />);
|
|
341
|
+
let arc: any = opsNamed("setProps").find((op: any) => op[2]?.style?.arcValueEnd !== undefined);
|
|
342
|
+
expect(arc[2].style.arcValueStart).toBe(0);
|
|
343
|
+
expect(arc[2].style.arcValueEnd).toBeCloseTo(67.5);
|
|
344
|
+
|
|
345
|
+
unmount();
|
|
346
|
+
batches.length = 0;
|
|
347
|
+
render(<Knob bipolar value={0.25} onChange={() => {}} />);
|
|
348
|
+
arc = opsNamed("setProps").find((op: any) => op[2]?.style?.arcValueEnd !== undefined);
|
|
349
|
+
expect(arc[2].style.arcValueStart).toBeCloseTo(-67.5);
|
|
350
|
+
expect(arc[2].style.arcValueEnd).toBe(0);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("ParamKnob: host default drives the double-click reset", () => {
|
|
354
|
+
paramGetResult = { value: 0.9, text: "+5 dB", name: "Gain", label: "dB", defaultValue: 0.25 };
|
|
355
|
+
const { ParamKnob } = require("./index");
|
|
356
|
+
render(<ParamKnob paramId="gain" />);
|
|
357
|
+
|
|
358
|
+
const id = nodeWithListener("dblclick");
|
|
359
|
+
dispatch({ kind: "event", nodeId: id, type: "dblclick" });
|
|
360
|
+
|
|
361
|
+
const set = nativeCalls.find((c) => c.name === "param:set");
|
|
362
|
+
expect(set?.args).toEqual({ id: "gain", value: 0.25 });
|
|
363
|
+
expect(nativeCalls.some((c) => c.name === "param:begin")).toBe(true);
|
|
364
|
+
expect(nativeCalls.some((c) => c.name === "param:end")).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("Slider: double-click reset + wheel work in both orientations", () => {
|
|
368
|
+
const { Slider } = require("./index");
|
|
369
|
+
const seen: number[] = [];
|
|
370
|
+
render(<Slider vertical value={0.8} defaultValue={0.5} onChange={(v: number) => seen.push(v)} />);
|
|
371
|
+
|
|
372
|
+
dispatch({ kind: "event", nodeId: nodeWithListener("dblclick"), type: "dblclick" });
|
|
373
|
+
expect(seen.at(-1)).toBe(0.5);
|
|
374
|
+
|
|
375
|
+
dispatch({ kind: "event", nodeId: nodeWithListener("wheel"), type: "wheel", payload: { dy: -0.1 } });
|
|
376
|
+
expect(seen.at(-1)).toBeCloseTo(0.76);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("Tooltip + Modal", () => {
|
|
381
|
+
test("Tooltip shows below its anchor after the hover delay, hides on leave", async () => {
|
|
382
|
+
const { Tooltip } = require("./index");
|
|
383
|
+
render(
|
|
384
|
+
<Tooltip label="Resets to 0 dB" delayMs={15} offset={6}>
|
|
385
|
+
<Slider value={0.5} onChange={() => {}} />
|
|
386
|
+
</Tooltip>,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const anchor = nodeWithListener("mouseenter");
|
|
390
|
+
dispatch({
|
|
391
|
+
kind: "event",
|
|
392
|
+
nodeId: anchor,
|
|
393
|
+
type: "layout",
|
|
394
|
+
payload: { x: 10, y: 20, width: 100, height: 30 },
|
|
395
|
+
});
|
|
396
|
+
dispatch({ kind: "event", nodeId: anchor, type: "mouseenter" });
|
|
397
|
+
await new Promise((r) => setTimeout(r, 40));
|
|
398
|
+
|
|
399
|
+
const tip: any = opsNamed("setProps").find(
|
|
400
|
+
(op: any) => op[2]?.style?.top === 20 + 30 + 6 && op[2]?.style?.left === 10,
|
|
401
|
+
);
|
|
402
|
+
expect(tip).toBeDefined();
|
|
403
|
+
expect(allOps().some((op: any) => op[0] === "setText" && op[2] === "Resets to 0 dB")).toBe(true);
|
|
404
|
+
|
|
405
|
+
dispatch({ kind: "event", nodeId: anchor, type: "mouseleave" });
|
|
406
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
407
|
+
expect(allOps().some((op: any) => op[0] === "removeChild")).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("Modal renders a centered panel; backdrop closes, panel doesn't", async () => {
|
|
411
|
+
const { Modal, Text: T } = require("./index");
|
|
412
|
+
const closed: number[] = [];
|
|
413
|
+
render(
|
|
414
|
+
<Modal open onClose={() => closed.push(1)} title="ABOUT" width={280}>
|
|
415
|
+
<T>VSReacT 0.0.6</T>
|
|
416
|
+
</Modal>,
|
|
417
|
+
);
|
|
418
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
419
|
+
|
|
420
|
+
expect(allOps().some((op: any) => op[0] === "setText" && op[2] === "ABOUT")).toBe(true);
|
|
421
|
+
const panel: any = opsNamed("setProps").find((op: any) => op[2]?.style?.width === 280);
|
|
422
|
+
expect(panel).toBeDefined();
|
|
423
|
+
|
|
424
|
+
// panel click swallows; backdrop click closes
|
|
425
|
+
dispatch({ kind: "event", nodeId: panel[1], type: "click" });
|
|
426
|
+
expect(closed.length).toBe(0);
|
|
427
|
+
|
|
428
|
+
const backdrop: any = opsNamed("setProps").find(
|
|
429
|
+
(op: any) => op[2]?.listeners?.includes("click") && op[2]?.style?.left === 0 && op[2]?.style?.right === 0,
|
|
430
|
+
);
|
|
431
|
+
expect(backdrop).toBeDefined();
|
|
432
|
+
dispatch({ kind: "event", nodeId: backdrop[1], type: "click" });
|
|
433
|
+
expect(closed).toEqual([1]);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe("0.0.7 — Button, visualizers, hooks", () => {
|
|
438
|
+
test("Button: click fires; disabled renders no listener; variants style", () => {
|
|
439
|
+
const { Button } = require("./index");
|
|
440
|
+
const seen: number[] = [];
|
|
441
|
+
render(<Button label="APPLY" onClick={() => seen.push(1)} accentColor="#FF2E2E" />);
|
|
442
|
+
|
|
443
|
+
dispatch({ kind: "event", nodeId: nodeWithListener("click"), type: "click" });
|
|
444
|
+
expect(seen).toEqual([1]);
|
|
445
|
+
|
|
446
|
+
// solid: filled with the accent
|
|
447
|
+
const solid: any = opsNamed("setProps").find(
|
|
448
|
+
(op: any) => op[2]?.style?.backgroundColor === "#FF2E2E",
|
|
449
|
+
);
|
|
450
|
+
expect(solid).toBeDefined();
|
|
451
|
+
expect(solid[2].hoverStyle).toBeDefined();
|
|
452
|
+
|
|
453
|
+
unmount();
|
|
454
|
+
batches.length = 0;
|
|
455
|
+
render(<Button label="X" disabled onClick={() => seen.push(2)} />);
|
|
456
|
+
expect(opsNamed("setProps").some((op: any) => op[2]?.listeners?.includes("click"))).toBe(false);
|
|
457
|
+
|
|
458
|
+
unmount();
|
|
459
|
+
batches.length = 0;
|
|
460
|
+
render(<Button label="Y" variant="outline" onClick={() => {}} accentColor="#FF2E2E" />);
|
|
461
|
+
const outline: any = opsNamed("setProps").find(
|
|
462
|
+
(op: any) => op[2]?.style?.borderColor === "#FF2E2E",
|
|
463
|
+
);
|
|
464
|
+
expect(outline).toBeDefined();
|
|
465
|
+
expect(outline[2].style.borderWidth).toBe(1);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("Bars: one bar per value, hot bars switch color, heights scale", () => {
|
|
469
|
+
const { Bars } = require("./index");
|
|
470
|
+
render(<Bars values={[0.5, 0.9]} height={60} hotFrom={0.85} color="#AAA111" hotColor="#FF0000" />);
|
|
471
|
+
|
|
472
|
+
const bars = opsNamed("setProps").filter(
|
|
473
|
+
(op: any) => op[2]?.style?.backgroundColor === "#AAA111" || op[2]?.style?.backgroundColor === "#FF0000",
|
|
474
|
+
);
|
|
475
|
+
expect(bars.length).toBe(2);
|
|
476
|
+
const cold: any = bars.find((op: any) => op[2].style.backgroundColor === "#AAA111");
|
|
477
|
+
const hot: any = bars.find((op: any) => op[2].style.backgroundColor === "#FF0000");
|
|
478
|
+
expect(cold[2].style.height).toBeCloseTo(0.5 * 56);
|
|
479
|
+
expect(hot[2].style.height).toBeCloseTo(0.9 * 56);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("Waveform: bars mirror around centre with |value| heights", () => {
|
|
483
|
+
const { Waveform } = require("./index");
|
|
484
|
+
render(<Waveform values={[-1, 0.5]} height={60} color="#AAA111" centerLine={false} />);
|
|
485
|
+
|
|
486
|
+
const bars = opsNamed("setProps")
|
|
487
|
+
.filter((op: any) => op[2]?.style?.backgroundColor === "#AAA111")
|
|
488
|
+
.map((op: any) => op[2].style.height);
|
|
489
|
+
expect(bars).toHaveLength(2);
|
|
490
|
+
expect(Math.max(...bars)).toBeCloseTo(56);
|
|
491
|
+
expect(Math.min(...bars)).toBeCloseTo(28);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("pushRolling keeps a fixed window, newest last", () => {
|
|
495
|
+
const { pushRolling } = require("./index");
|
|
496
|
+
expect(pushRolling([1, 2, 3], 4, 3)).toEqual([2, 3, 4]);
|
|
497
|
+
expect(pushRolling([], 7, 3)).toEqual([0, 0, 7]);
|
|
498
|
+
expect(pushRolling([1, 2, 3, 4, 5], 6, 3)).toEqual([4, 5, 6]);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("useToggle + useHover drive a probe component", () => {
|
|
502
|
+
const { useToggle, useHover, View: V, Text: T } = require("./index");
|
|
503
|
+
|
|
504
|
+
function Probe() {
|
|
505
|
+
const [on, toggle] = useToggle(false);
|
|
506
|
+
const [hovered, hoverProps] = useHover();
|
|
507
|
+
return (
|
|
508
|
+
<V onClick={toggle} {...hoverProps}>
|
|
509
|
+
<T>{`${on ? "ON" : "OFF"}:${hovered ? "HOV" : "OUT"}`}</T>
|
|
510
|
+
</V>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
render(<Probe />);
|
|
515
|
+
const id = nodeWithListener("click");
|
|
516
|
+
|
|
517
|
+
dispatch({ kind: "event", nodeId: id, type: "click" });
|
|
518
|
+
dispatch({ kind: "event", nodeId: id, type: "mouseenter" });
|
|
519
|
+
expect(allOps().some((op: any) => op[0] === "setText" && op[2] === "ON:HOV")).toBe(true);
|
|
520
|
+
|
|
521
|
+
dispatch({ kind: "event", nodeId: id, type: "mouseleave" });
|
|
522
|
+
expect(allOps().some((op: any) => op[0] === "setText" && op[2] === "ON:OUT")).toBe(true);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("usePrevious reports the last-render value", () => {
|
|
526
|
+
const { usePrevious, View: V, Text: T } = require("./index");
|
|
527
|
+
const values: Array<string> = [];
|
|
528
|
+
|
|
529
|
+
function Probe({ n }: { n: number }) {
|
|
530
|
+
const prev = usePrevious(n);
|
|
531
|
+
values.push(`${prev}->${n}`);
|
|
532
|
+
return (
|
|
533
|
+
<V>
|
|
534
|
+
<T>{n}</T>
|
|
535
|
+
</V>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
render(<Probe n={1} />);
|
|
540
|
+
render(<Probe n={2} />);
|
|
541
|
+
expect(values[0]).toBe("undefined->1");
|
|
542
|
+
expect(values.at(-1)).toBe("1->2");
|
|
543
|
+
});
|
|
544
|
+
});
|
package/src/controls.tsx
CHANGED
|
@@ -25,6 +25,13 @@ export interface KnobProps {
|
|
|
25
25
|
label?: string;
|
|
26
26
|
size?: number;
|
|
27
27
|
disabled?: boolean;
|
|
28
|
+
/** Double-click resets to this (DAW convention). */
|
|
29
|
+
defaultValue?: number;
|
|
30
|
+
/** Value arc sweeps from 12 o'clock instead of the left stop — for
|
|
31
|
+
centre-based parameters like pan. */
|
|
32
|
+
bipolar?: boolean;
|
|
33
|
+
/** Value change per wheel notch fraction. 0 disables. Default 0.4. */
|
|
34
|
+
wheelSensitivity?: number;
|
|
28
35
|
trackColor?: string;
|
|
29
36
|
valueColor?: string;
|
|
30
37
|
onChange: (value: number) => void;
|
|
@@ -38,6 +45,9 @@ export function Knob({
|
|
|
38
45
|
label,
|
|
39
46
|
size = 64,
|
|
40
47
|
disabled,
|
|
48
|
+
defaultValue,
|
|
49
|
+
bipolar,
|
|
50
|
+
wheelSensitivity = 0.4,
|
|
41
51
|
trackColor = "#2A2F27",
|
|
42
52
|
valueColor = "#C6F135",
|
|
43
53
|
onChange,
|
|
@@ -45,6 +55,15 @@ export function Knob({
|
|
|
45
55
|
onEnd,
|
|
46
56
|
}: KnobProps) {
|
|
47
57
|
const startValue = useRef(0);
|
|
58
|
+
const clamped = clamp01(value);
|
|
59
|
+
const angle = ARC_START + (ARC_END - ARC_START) * clamped;
|
|
60
|
+
const center = (ARC_START + ARC_END) / 2;
|
|
61
|
+
|
|
62
|
+
const nudge = (target: number) => {
|
|
63
|
+
onBegin?.();
|
|
64
|
+
onChange(clamp01(target));
|
|
65
|
+
onEnd?.();
|
|
66
|
+
};
|
|
48
67
|
|
|
49
68
|
return (
|
|
50
69
|
<View className="items-center gap-2">
|
|
@@ -57,19 +76,28 @@ export function Knob({
|
|
|
57
76
|
arcColor: valueColor,
|
|
58
77
|
arcStart: ARC_START,
|
|
59
78
|
arcEnd: ARC_END,
|
|
60
|
-
|
|
79
|
+
arcValueStart: bipolar ? Math.min(center, angle) : ARC_START,
|
|
80
|
+
arcValueEnd: bipolar ? Math.max(center, angle) : angle,
|
|
61
81
|
arcThickness: Math.max(3, size * 0.08),
|
|
62
82
|
}}
|
|
63
83
|
onDragStart={
|
|
64
84
|
disabled
|
|
65
85
|
? undefined
|
|
66
86
|
: () => {
|
|
67
|
-
startValue.current =
|
|
87
|
+
startValue.current = clamped;
|
|
68
88
|
onBegin?.();
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
onDrag={disabled ? undefined : (e) => onChange(dragToValue(startValue.current, e.dy))}
|
|
72
92
|
onDragEnd={disabled ? undefined : () => onEnd?.()}
|
|
93
|
+
onDoubleClick={
|
|
94
|
+
disabled || defaultValue === undefined ? undefined : () => nudge(defaultValue)
|
|
95
|
+
}
|
|
96
|
+
onWheel={
|
|
97
|
+
disabled || wheelSensitivity === 0
|
|
98
|
+
? undefined
|
|
99
|
+
: (e) => nudge(clamped + e.dy * wheelSensitivity)
|
|
100
|
+
}
|
|
73
101
|
>
|
|
74
102
|
{text !== undefined ? (
|
|
75
103
|
<Text
|
|
@@ -91,12 +119,15 @@ export interface ParamKnobProps {
|
|
|
91
119
|
paramId: string;
|
|
92
120
|
label?: string;
|
|
93
121
|
size?: number;
|
|
122
|
+
bipolar?: boolean;
|
|
123
|
+
wheelSensitivity?: number;
|
|
94
124
|
trackColor?: string;
|
|
95
125
|
valueColor?: string;
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
/** A Knob bound to an APVTS parameter
|
|
99
|
-
|
|
128
|
+
/** A Knob bound to an APVTS parameter — double-click resets to the
|
|
129
|
+
host's default, the wheel nudges, gestures stay automation-safe. */
|
|
130
|
+
export function ParamKnob({ paramId, label, ...rest }: ParamKnobProps) {
|
|
100
131
|
const param = useParameter(paramId);
|
|
101
132
|
|
|
102
133
|
return (
|
|
@@ -104,12 +135,11 @@ export function ParamKnob({ paramId, label, size, trackColor, valueColor }: Para
|
|
|
104
135
|
value={param.value}
|
|
105
136
|
text={param.text}
|
|
106
137
|
label={label ?? param.name.toUpperCase()}
|
|
107
|
-
|
|
108
|
-
trackColor={trackColor}
|
|
109
|
-
valueColor={valueColor}
|
|
138
|
+
defaultValue={param.defaultValue}
|
|
110
139
|
onChange={param.set}
|
|
111
140
|
onBegin={param.begin}
|
|
112
141
|
onEnd={param.end}
|
|
142
|
+
{...rest}
|
|
113
143
|
/>
|
|
114
144
|
);
|
|
115
145
|
}
|
|
@@ -124,6 +154,10 @@ export interface SliderProps {
|
|
|
124
154
|
vertical?: boolean;
|
|
125
155
|
label?: string;
|
|
126
156
|
disabled?: boolean;
|
|
157
|
+
/** Double-click resets to this (DAW convention). */
|
|
158
|
+
defaultValue?: number;
|
|
159
|
+
/** Value change per wheel notch fraction. 0 disables. Default 0.4. */
|
|
160
|
+
wheelSensitivity?: number;
|
|
127
161
|
trackColor?: string;
|
|
128
162
|
valueColor?: string;
|
|
129
163
|
onChange: (value: number) => void;
|
|
@@ -138,6 +172,8 @@ export function Slider({
|
|
|
138
172
|
vertical,
|
|
139
173
|
label,
|
|
140
174
|
disabled,
|
|
175
|
+
defaultValue,
|
|
176
|
+
wheelSensitivity = 0.4,
|
|
141
177
|
trackColor = "#2A2F27",
|
|
142
178
|
valueColor = "#C6F135",
|
|
143
179
|
onChange,
|
|
@@ -155,6 +191,18 @@ export function Slider({
|
|
|
155
191
|
};
|
|
156
192
|
const onDragEnd = disabled ? undefined : () => onEnd?.();
|
|
157
193
|
|
|
194
|
+
const nudge = (target: number) => {
|
|
195
|
+
onBegin?.();
|
|
196
|
+
onChange(clamp01(target));
|
|
197
|
+
onEnd?.();
|
|
198
|
+
};
|
|
199
|
+
const onDoubleClick =
|
|
200
|
+
disabled || defaultValue === undefined ? undefined : () => nudge(defaultValue);
|
|
201
|
+
const onWheel =
|
|
202
|
+
disabled || wheelSensitivity === 0
|
|
203
|
+
? undefined
|
|
204
|
+
: (e: { dy: number }) => nudge(clamped + e.dy * wheelSensitivity);
|
|
205
|
+
|
|
158
206
|
if (vertical) {
|
|
159
207
|
return (
|
|
160
208
|
<View className="items-center gap-2">
|
|
@@ -164,6 +212,8 @@ export function Slider({
|
|
|
164
212
|
onDragStart={onDragStart}
|
|
165
213
|
onDrag={disabled ? undefined : (e) => onChange(clamp01(startValue.current - e.dy / height))}
|
|
166
214
|
onDragEnd={onDragEnd}
|
|
215
|
+
onDoubleClick={onDoubleClick}
|
|
216
|
+
onWheel={onWheel}
|
|
167
217
|
>
|
|
168
218
|
<View
|
|
169
219
|
className="absolute w-[4] rounded-full left-[7] top-0 bottom-0"
|
|
@@ -193,6 +243,8 @@ export function Slider({
|
|
|
193
243
|
onDragStart={onDragStart}
|
|
194
244
|
onDrag={disabled ? undefined : (e) => onChange(clamp01(startValue.current + e.dx / width))}
|
|
195
245
|
onDragEnd={onDragEnd}
|
|
246
|
+
onDoubleClick={onDoubleClick}
|
|
247
|
+
onWheel={onWheel}
|
|
196
248
|
>
|
|
197
249
|
<View className="h-[4] rounded-full" style={{ backgroundColor: trackColor }} />
|
|
198
250
|
<View
|
|
@@ -217,22 +269,23 @@ export interface ParamSliderProps {
|
|
|
217
269
|
width?: number;
|
|
218
270
|
height?: number;
|
|
219
271
|
vertical?: boolean;
|
|
272
|
+
wheelSensitivity?: number;
|
|
220
273
|
}
|
|
221
274
|
|
|
222
|
-
/** A Slider bound to an APVTS parameter
|
|
223
|
-
|
|
275
|
+
/** A Slider bound to an APVTS parameter — double-click resets to the
|
|
276
|
+
host's default, the wheel nudges, gestures stay automation-safe. */
|
|
277
|
+
export function ParamSlider({ paramId, label, ...rest }: ParamSliderProps) {
|
|
224
278
|
const param = useParameter(paramId);
|
|
225
279
|
|
|
226
280
|
return (
|
|
227
281
|
<Slider
|
|
228
282
|
value={param.value}
|
|
229
283
|
label={label ?? param.name.toUpperCase()}
|
|
230
|
-
|
|
231
|
-
height={height}
|
|
232
|
-
vertical={vertical}
|
|
284
|
+
defaultValue={param.defaultValue}
|
|
233
285
|
onChange={param.set}
|
|
234
286
|
onBegin={param.begin}
|
|
235
287
|
onEnd={param.end}
|
|
288
|
+
{...rest}
|
|
236
289
|
/>
|
|
237
290
|
);
|
|
238
291
|
}
|
package/src/hooks.ts
CHANGED
|
@@ -46,3 +46,74 @@ export function useDebounced<T>(value: T, delayMs: number): T {
|
|
|
46
46
|
|
|
47
47
|
return debounced;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/** The value, updated at most once per `intervalMs` (leading + trailing). */
|
|
51
|
+
export function useThrottled<T>(value: T, intervalMs: number): T {
|
|
52
|
+
const [throttled, setThrottled] = useState(value);
|
|
53
|
+
const lastRun = useRef(0);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const elapsed = now - lastRun.current;
|
|
58
|
+
|
|
59
|
+
if (elapsed >= intervalMs) {
|
|
60
|
+
lastRun.current = now;
|
|
61
|
+
setThrottled(value);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const id = setTimeout(() => {
|
|
66
|
+
lastRun.current = Date.now();
|
|
67
|
+
setThrottled(value);
|
|
68
|
+
}, intervalMs - elapsed);
|
|
69
|
+
return () => clearTimeout(id);
|
|
70
|
+
}, [value, intervalMs]);
|
|
71
|
+
|
|
72
|
+
return throttled;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The value from the previous render — undefined on the first one. */
|
|
76
|
+
export function usePrevious<T>(value: T): T | undefined {
|
|
77
|
+
const previous = useRef<T | undefined>(undefined);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
previous.current = value;
|
|
81
|
+
}, [value]);
|
|
82
|
+
|
|
83
|
+
return previous.current;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Boolean state with a stable toggle — bypass buttons, panels, A/B. */
|
|
87
|
+
export function useToggle(initial = false): [boolean, () => void, (next: boolean) => void] {
|
|
88
|
+
const [on, setOn] = useState(initial);
|
|
89
|
+
const toggle = useRef(() => setOn((v) => !v)).current;
|
|
90
|
+
return [on, toggle, setOn];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A declarative interval on the host scheduler; null pauses it. The
|
|
94
|
+
callback stays fresh without restarting the timer. */
|
|
95
|
+
export function useInterval(callback: () => void, intervalMs: number | null): void {
|
|
96
|
+
const callbackRef = useRef(callback);
|
|
97
|
+
callbackRef.current = callback;
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (intervalMs === null) return;
|
|
101
|
+
const id = setInterval(() => callbackRef.current(), intervalMs);
|
|
102
|
+
return () => clearInterval(id);
|
|
103
|
+
}, [intervalMs]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Hover state + the props that drive it:
|
|
107
|
+
`const [hovered, hoverProps] = useHover(); <View {...hoverProps}>` */
|
|
108
|
+
export function useHover(): [
|
|
109
|
+
boolean,
|
|
110
|
+
{ onMouseEnter: () => void; onMouseLeave: () => void },
|
|
111
|
+
] {
|
|
112
|
+
const [hovered, setHovered] = useState(false);
|
|
113
|
+
const props = useRef({
|
|
114
|
+
onMouseEnter: () => setHovered(true),
|
|
115
|
+
onMouseLeave: () => setHovered(false),
|
|
116
|
+
}).current;
|
|
117
|
+
|
|
118
|
+
return [hovered, props];
|
|
119
|
+
}
|
package/src/hostConfig.ts
CHANGED
|
@@ -24,6 +24,8 @@ const hostTypes: Record<string, string> = {
|
|
|
24
24
|
|
|
25
25
|
const eventPropNames: Record<string, string> = {
|
|
26
26
|
onClick: "click",
|
|
27
|
+
onDoubleClick: "dblclick",
|
|
28
|
+
onWheel: "wheel",
|
|
27
29
|
onMouseEnter: "mouseenter",
|
|
28
30
|
onMouseLeave: "mouseleave",
|
|
29
31
|
onMouseDown: "mousedown",
|
package/src/index.ts
CHANGED
|
@@ -13,8 +13,23 @@ export type {
|
|
|
13
13
|
} from "./primitives";
|
|
14
14
|
export { render, unmount } from "./render";
|
|
15
15
|
export { native } from "./native";
|
|
16
|
-
export {
|
|
16
|
+
export {
|
|
17
|
+
useNativeEvent,
|
|
18
|
+
useDebounced,
|
|
19
|
+
useThrottled,
|
|
20
|
+
usePrevious,
|
|
21
|
+
useToggle,
|
|
22
|
+
useInterval,
|
|
23
|
+
useHover,
|
|
24
|
+
useLayoutRect,
|
|
25
|
+
} from "./hooks";
|
|
17
26
|
export { useOverlay, OverlayLayer } from "./overlay";
|
|
27
|
+
export { Tooltip, Modal } from "./popover";
|
|
28
|
+
export type { TooltipProps, ModalProps } from "./popover";
|
|
29
|
+
export { Button } from "./button";
|
|
30
|
+
export type { ButtonProps } from "./button";
|
|
31
|
+
export { Bars, Waveform, useRollingBuffer, pushRolling } from "./visualizers";
|
|
32
|
+
export type { BarsProps, WaveformProps } from "./visualizers";
|
|
18
33
|
export { configureTheme, tw } from "./tw";
|
|
19
34
|
export type { Style, ResolvedClasses } from "./tw";
|
|
20
35
|
export { cx } from "./cx";
|
|
@@ -56,4 +71,4 @@ export type {
|
|
|
56
71
|
} from "./controls";
|
|
57
72
|
export { Meter, usePeakHold, peakHoldStep } from "./meter";
|
|
58
73
|
export type { MeterProps, PeakHoldOptions, PeakHoldState } from "./meter";
|
|
59
|
-
export type { DragEventPayload, LayoutRect } from "./primitives";
|
|
74
|
+
export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
|
package/src/parameters.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface ParameterState {
|
|
|
9
9
|
text: string;
|
|
10
10
|
name: string;
|
|
11
11
|
label: string;
|
|
12
|
+
/** The host's normalized default — double-click-reset target. */
|
|
13
|
+
defaultValue: number;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface ParameterHandle extends ParameterState {
|
|
@@ -24,6 +26,7 @@ export interface ParameterInfo {
|
|
|
24
26
|
/** Normalized 0..1 snapshot at mount — use useParameter(id) for live values. */
|
|
25
27
|
value: number;
|
|
26
28
|
text: string;
|
|
29
|
+
defaultValue: number;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -41,6 +44,7 @@ export function useParameterList(): ParameterInfo[] {
|
|
|
41
44
|
label: String(entry?.label ?? ""),
|
|
42
45
|
value: Number(entry?.value ?? 0),
|
|
43
46
|
text: String(entry?.text ?? ""),
|
|
47
|
+
defaultValue: Number(entry?.defaultValue ?? 0),
|
|
44
48
|
}));
|
|
45
49
|
});
|
|
46
50
|
|
|
@@ -56,8 +60,9 @@ export function useParameter(id: string): ParameterHandle {
|
|
|
56
60
|
text: String(initial.text ?? ""),
|
|
57
61
|
name: String(initial.name ?? id),
|
|
58
62
|
label: String(initial.label ?? ""),
|
|
63
|
+
defaultValue: Number(initial.defaultValue ?? 0),
|
|
59
64
|
}
|
|
60
|
-
: { value: 0, text: "", name: id, label: "" };
|
|
65
|
+
: { value: 0, text: "", name: id, label: "", defaultValue: 0 };
|
|
61
66
|
});
|
|
62
67
|
|
|
63
68
|
useEffect(
|