@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.
@@ -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
- arcValueEnd: ARC_START + (ARC_END - ARC_START) * Math.min(1, Math.max(0, value)),
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 = value;
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 (via ParameterBridge). */
99
- export function ParamKnob({ paramId, label, size, trackColor, valueColor }: ParamKnobProps) {
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
- size={size}
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 (via ParameterBridge). */
223
- export function ParamSlider({ paramId, label, width, height, vertical }: ParamSliderProps) {
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
- width={width}
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 { useNativeEvent, useDebounced, useLayoutRect } from "./hooks";
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(