@vsreact/core 0.0.4 → 0.0.6

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.
@@ -6,22 +6,32 @@ export interface KnobProps {
6
6
  label?: string;
7
7
  size?: number;
8
8
  disabled?: boolean;
9
+ /** Double-click resets to this (DAW convention). */
10
+ defaultValue?: number;
11
+ /** Value arc sweeps from 12 o'clock instead of the left stop — for
12
+ centre-based parameters like pan. */
13
+ bipolar?: boolean;
14
+ /** Value change per wheel notch fraction. 0 disables. Default 0.4. */
15
+ wheelSensitivity?: number;
9
16
  trackColor?: string;
10
17
  valueColor?: string;
11
18
  onChange: (value: number) => void;
12
19
  onBegin?: () => void;
13
20
  onEnd?: () => void;
14
21
  }
15
- export declare function Knob({ value, text, label, size, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: KnobProps): import("react").JSX.Element;
22
+ export declare function Knob({ value, text, label, size, disabled, defaultValue, bipolar, wheelSensitivity, trackColor, valueColor, onChange, onBegin, onEnd, }: KnobProps): import("react").JSX.Element;
16
23
  export interface ParamKnobProps {
17
24
  paramId: string;
18
25
  label?: string;
19
26
  size?: number;
27
+ bipolar?: boolean;
28
+ wheelSensitivity?: number;
20
29
  trackColor?: string;
21
30
  valueColor?: string;
22
31
  }
23
- /** A Knob bound to an APVTS parameter (via ParameterBridge). */
24
- export declare function ParamKnob({ paramId, label, size, trackColor, valueColor }: ParamKnobProps): import("react").JSX.Element;
32
+ /** A Knob bound to an APVTS parameter double-click resets to the
33
+ host's default, the wheel nudges, gestures stay automation-safe. */
34
+ export declare function ParamKnob({ paramId, label, ...rest }: ParamKnobProps): import("react").JSX.Element;
25
35
  export interface SliderProps {
26
36
  value: number;
27
37
  /** Track length when horizontal (default 160). */
@@ -32,22 +42,28 @@ export interface SliderProps {
32
42
  vertical?: boolean;
33
43
  label?: string;
34
44
  disabled?: boolean;
45
+ /** Double-click resets to this (DAW convention). */
46
+ defaultValue?: number;
47
+ /** Value change per wheel notch fraction. 0 disables. Default 0.4. */
48
+ wheelSensitivity?: number;
35
49
  trackColor?: string;
36
50
  valueColor?: string;
37
51
  onChange: (value: number) => void;
38
52
  onBegin?: () => void;
39
53
  onEnd?: () => void;
40
54
  }
41
- export declare function Slider({ value, width, height, vertical, label, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: SliderProps): import("react").JSX.Element;
55
+ export declare function Slider({ value, width, height, vertical, label, disabled, defaultValue, wheelSensitivity, trackColor, valueColor, onChange, onBegin, onEnd, }: SliderProps): import("react").JSX.Element;
42
56
  export interface ParamSliderProps {
43
57
  paramId: string;
44
58
  label?: string;
45
59
  width?: number;
46
60
  height?: number;
47
61
  vertical?: boolean;
62
+ wheelSensitivity?: number;
48
63
  }
49
- /** A Slider bound to an APVTS parameter (via ParameterBridge). */
50
- export declare function ParamSlider({ paramId, label, width, height, vertical }: ParamSliderProps): import("react").JSX.Element;
64
+ /** A Slider bound to an APVTS parameter double-click resets to the
65
+ host's default, the wheel nudges, gestures stay automation-safe. */
66
+ export declare function ParamSlider({ paramId, label, ...rest }: ParamSliderProps): import("react").JSX.Element;
51
67
  export interface ToggleProps {
52
68
  on: boolean;
53
69
  label?: string;
@@ -135,3 +151,38 @@ export interface GenericEditorProps {
135
151
  /** The zero-effort editor: one knob per APVTS parameter, laid out in
136
152
  rows. `render(<GenericEditor />)` is a complete, working plugin UI. */
137
153
  export declare function GenericEditor({ columns, size, trackColor, valueColor }: GenericEditorProps): import("react").JSX.Element;
154
+ export interface SelectProps {
155
+ options: string[];
156
+ index: number;
157
+ /** Trigger width; the menu matches it. Default 160. */
158
+ width?: number;
159
+ label?: string;
160
+ disabled?: boolean;
161
+ trackColor?: string;
162
+ menuColor?: string;
163
+ activeColor?: string;
164
+ textColor?: string;
165
+ activeTextColor?: string;
166
+ /** Menu scrolls beyond this height. Default 190. */
167
+ maxMenuHeight?: number;
168
+ onChange: (index: number) => void;
169
+ }
170
+ /** A dropdown: the menu renders in the overlay layer, positioned under
171
+ the trigger via onLayout, with a click-away backdrop and a scrolling
172
+ option list. */
173
+ export declare function Select({ options, index, width, label, disabled, trackColor, menuColor, activeColor, textColor, activeTextColor, maxMenuHeight, onChange, }: SelectProps): import("react").JSX.Element;
174
+ export interface ParamSelectProps {
175
+ paramId: string;
176
+ options: string[];
177
+ width?: number;
178
+ label?: string;
179
+ trackColor?: string;
180
+ menuColor?: string;
181
+ activeColor?: string;
182
+ textColor?: string;
183
+ activeTextColor?: string;
184
+ maxMenuHeight?: number;
185
+ }
186
+ /** A Select bound to a choice-style APVTS parameter (same value↔index
187
+ mapping as ParamSegmented). */
188
+ export declare function ParamSelect({ paramId, options, label, ...rest }: ParamSelectProps): import("react").JSX.Element;
package/dist/controls.js CHANGED
@@ -2,10 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // The built-in VST controls — knobs, sliders, toggles, XY pads, segmented
3
3
  // switches — drawn by the VSReacT painter and driven by drag gestures.
4
4
  // Each has a Param* variant bound to an APVTS parameter.
5
- import { useRef } from "react";
5
+ import { useEffect, useRef, useState } from "react";
6
6
  import { View, Text } from "./primitives";
7
7
  import { useParameter, useParameterList } from "./parameters";
8
8
  import { useSpring } from "./animation";
9
+ import { useLayoutRect } from "./hooks";
10
+ import { useOverlay } from "./overlay";
9
11
  const clamp01 = (v) => Math.min(1, Math.max(0, v));
10
12
  /** Vertical-drag-to-value mapping shared by Knob (and tested in isolation). */
11
13
  export function dragToValue(startValue, dy, sensitivity = 0.005) {
@@ -13,8 +15,16 @@ export function dragToValue(startValue, dy, sensitivity = 0.005) {
13
15
  }
14
16
  const ARC_START = -135;
15
17
  const ARC_END = 135;
16
- export function Knob({ value, text, label, size = 64, disabled, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
18
+ export function Knob({ value, text, label, size = 64, disabled, defaultValue, bipolar, wheelSensitivity = 0.4, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
17
19
  const startValue = useRef(0);
20
+ const clamped = clamp01(value);
21
+ const angle = ARC_START + (ARC_END - ARC_START) * clamped;
22
+ const center = (ARC_START + ARC_END) / 2;
23
+ const nudge = (target) => {
24
+ onBegin?.();
25
+ onChange(clamp01(target));
26
+ onEnd?.();
27
+ };
18
28
  return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `items-center justify-center ${disabled ? "opacity-40" : "cursor-pointer"}`, style: {
19
29
  width: size,
20
30
  height: size,
@@ -22,21 +32,25 @@ export function Knob({ value, text, label, size = 64, disabled, trackColor = "#2
22
32
  arcColor: valueColor,
23
33
  arcStart: ARC_START,
24
34
  arcEnd: ARC_END,
25
- arcValueEnd: ARC_START + (ARC_END - ARC_START) * Math.min(1, Math.max(0, value)),
35
+ arcValueStart: bipolar ? Math.min(center, angle) : ARC_START,
36
+ arcValueEnd: bipolar ? Math.max(center, angle) : angle,
26
37
  arcThickness: Math.max(3, size * 0.08),
27
38
  }, onDragStart: disabled
28
39
  ? undefined
29
40
  : () => {
30
- startValue.current = value;
41
+ startValue.current = clamped;
31
42
  onBegin?.();
32
- }, onDrag: disabled ? undefined : (e) => onChange(dragToValue(startValue.current, e.dy)), onDragEnd: disabled ? undefined : () => onEnd?.(), children: text !== undefined ? (_jsx(Text, { className: "text-text font-bold text-center", style: { fontSize: Math.max(10, size * 0.16) }, children: text })) : null }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
43
+ }, onDrag: disabled ? undefined : (e) => onChange(dragToValue(startValue.current, e.dy)), onDragEnd: disabled ? undefined : () => onEnd?.(), onDoubleClick: disabled || defaultValue === undefined ? undefined : () => nudge(defaultValue), onWheel: disabled || wheelSensitivity === 0
44
+ ? undefined
45
+ : (e) => nudge(clamped + e.dy * wheelSensitivity), children: text !== undefined ? (_jsx(Text, { className: "text-text font-bold text-center", style: { fontSize: Math.max(10, size * 0.16) }, children: text })) : null }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
33
46
  }
34
- /** A Knob bound to an APVTS parameter (via ParameterBridge). */
35
- export function ParamKnob({ paramId, label, size, trackColor, valueColor }) {
47
+ /** A Knob bound to an APVTS parameter double-click resets to the
48
+ host's default, the wheel nudges, gestures stay automation-safe. */
49
+ export function ParamKnob({ paramId, label, ...rest }) {
36
50
  const param = useParameter(paramId);
37
- return (_jsx(Knob, { value: param.value, text: param.text, label: label ?? param.name.toUpperCase(), size: size, trackColor: trackColor, valueColor: valueColor, onChange: param.set, onBegin: param.begin, onEnd: param.end }));
51
+ return (_jsx(Knob, { value: param.value, text: param.text, label: label ?? param.name.toUpperCase(), defaultValue: param.defaultValue, onChange: param.set, onBegin: param.begin, onEnd: param.end, ...rest }));
38
52
  }
39
- export function Slider({ value, width = 160, height = 160, vertical, label, disabled, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
53
+ export function Slider({ value, width = 160, height = 160, vertical, label, disabled, defaultValue, wheelSensitivity = 0.4, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
40
54
  const startValue = useRef(0);
41
55
  const clamped = clamp01(value);
42
56
  const onDragStart = disabled
@@ -46,15 +60,25 @@ export function Slider({ value, width = 160, height = 160, vertical, label, disa
46
60
  onBegin?.();
47
61
  };
48
62
  const onDragEnd = disabled ? undefined : () => onEnd?.();
63
+ const nudge = (target) => {
64
+ onBegin?.();
65
+ onChange(clamp01(target));
66
+ onEnd?.();
67
+ };
68
+ const onDoubleClick = disabled || defaultValue === undefined ? undefined : () => nudge(defaultValue);
69
+ const onWheel = disabled || wheelSensitivity === 0
70
+ ? undefined
71
+ : (e) => nudge(clamped + e.dy * wheelSensitivity);
49
72
  if (vertical) {
50
- return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: `w-[18] relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { height }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current - e.dy / height)), onDragEnd: onDragEnd, children: [_jsx(View, { className: "absolute w-[4] rounded-full left-[7] top-0 bottom-0", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute w-[4] rounded-full left-[7] bottom-0", style: { height: clamped * height, backgroundColor: valueColor } }), _jsx(View, { className: "absolute w-[12] h-[12] rounded-full left-[3]", style: { top: (1 - clamped) * (height - 12), backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
73
+ return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: `w-[18] relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { height }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current - e.dy / height)), onDragEnd: onDragEnd, onDoubleClick: onDoubleClick, onWheel: onWheel, children: [_jsx(View, { className: "absolute w-[4] rounded-full left-[7] top-0 bottom-0", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute w-[4] rounded-full left-[7] bottom-0", style: { height: clamped * height, backgroundColor: valueColor } }), _jsx(View, { className: "absolute w-[12] h-[12] rounded-full left-[3]", style: { top: (1 - clamped) * (height - 12), backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
51
74
  }
52
- return (_jsxs(View, { className: "gap-2", children: [_jsxs(View, { className: `h-[18] justify-center relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current + e.dx / width)), onDragEnd: onDragEnd, children: [_jsx(View, { className: "h-[4] rounded-full", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute h-[4] rounded-full top-[7]", style: { width: clamped * width, backgroundColor: valueColor } }), _jsx(View, { className: "absolute w-[12] h-[12] rounded-full top-[3]", style: { left: clamped * (width - 12), backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
75
+ return (_jsxs(View, { className: "gap-2", children: [_jsxs(View, { className: `h-[18] justify-center relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current + e.dx / width)), onDragEnd: onDragEnd, onDoubleClick: onDoubleClick, onWheel: onWheel, children: [_jsx(View, { className: "h-[4] rounded-full", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute h-[4] rounded-full top-[7]", style: { width: clamped * width, backgroundColor: valueColor } }), _jsx(View, { className: "absolute w-[12] h-[12] rounded-full top-[3]", style: { left: clamped * (width - 12), backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
53
76
  }
54
- /** A Slider bound to an APVTS parameter (via ParameterBridge). */
55
- export function ParamSlider({ paramId, label, width, height, vertical }) {
77
+ /** A Slider bound to an APVTS parameter double-click resets to the
78
+ host's default, the wheel nudges, gestures stay automation-safe. */
79
+ export function ParamSlider({ paramId, label, ...rest }) {
56
80
  const param = useParameter(paramId);
57
- return (_jsx(Slider, { value: param.value, label: label ?? param.name.toUpperCase(), width: width, height: height, vertical: vertical, onChange: param.set, onBegin: param.begin, onEnd: param.end }));
81
+ return (_jsx(Slider, { value: param.value, label: label ?? param.name.toUpperCase(), defaultValue: param.defaultValue, onChange: param.set, onBegin: param.begin, onEnd: param.end, ...rest }));
58
82
  }
59
83
  /** A switch with a spring-animated thumb — bypass, on/off, A/B. */
60
84
  export function Toggle({ on, label, size = 22, disabled, trackColor = "#2A2F27", onColor = "#C6F135", thumbColor = "#F4F4F5", onChange, }) {
@@ -137,3 +161,44 @@ export function GenericEditor({ columns = 4, size = 72, trackColor, valueColor }
137
161
  rows.push(params.slice(i, i + Math.max(1, columns)));
138
162
  return (_jsx(View, { className: "flex-1 flex-col items-center justify-center gap-6 p-4", children: rows.map((row, index) => (_jsx(View, { className: "flex-row gap-7", children: row.map((param) => (_jsx(ParamKnob, { paramId: param.id, size: size, trackColor: trackColor, valueColor: valueColor }, param.id))) }, index))) }));
139
163
  }
164
+ const SELECT_ROW_HEIGHT = 30;
165
+ /** A dropdown: the menu renders in the overlay layer, positioned under
166
+ the trigger via onLayout, with a click-away backdrop and a scrolling
167
+ option list. */
168
+ export function Select({ options, index, width = 160, label, disabled, trackColor = "#2A2F27", menuColor = "#20241F", activeColor = "#C6F135", textColor = "#d4d4d8", activeTextColor = "#09090b", maxMenuHeight = 190, onChange, }) {
169
+ const [open, setOpen] = useState(false);
170
+ const [rect, onLayout] = useLayoutRect();
171
+ const overlay = useOverlay();
172
+ const current = Math.min(options.length - 1, Math.max(0, index));
173
+ useEffect(() => {
174
+ if (!open || rect === null) {
175
+ overlay.hide();
176
+ return;
177
+ }
178
+ const menuHeight = Math.min(options.length * SELECT_ROW_HEIGHT + 8, maxMenuHeight);
179
+ overlay.show(_jsx(View, { className: "absolute inset-0", onClick: () => setOpen(false), children: _jsx(View, { className: "absolute rounded-lg border shadow-lg overflow-hidden", style: {
180
+ left: rect.x,
181
+ top: rect.y + rect.height + 4,
182
+ width: rect.width,
183
+ backgroundColor: menuColor,
184
+ borderColor: "#00000066",
185
+ }, children: _jsx(View, { className: "overflow-y-scroll p-[4] gap-[2]", style: { height: menuHeight }, children: options.map((option, i) => (_jsx(View, { className: "px-3 py-[6] rounded cursor-pointer hover:bg-white/10", style: { backgroundColor: i === current ? activeColor : "#00000000" }, onClick: () => {
186
+ onChange(i);
187
+ setOpen(false);
188
+ }, children: _jsx(Text, { className: "text-[12] font-medium", style: { color: i === current ? activeTextColor : textColor }, children: option }) }, `${option}-${i}`))) }) }) }));
189
+ // eslint-disable-next-line react-hooks/exhaustive-deps
190
+ }, [open, rect, options, current, menuColor, activeColor, textColor, activeTextColor, maxMenuHeight]);
191
+ return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: `flex-row items-center justify-between rounded-lg border px-3 py-[8] ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width, backgroundColor: trackColor, borderColor: "#00000055" }, onLayout: onLayout, onClick: disabled ? undefined : () => setOpen((v) => !v), children: [_jsx(Text, { className: "text-[12] font-medium", style: { color: textColor }, children: options[current] ?? "" }), _jsx(Text, { className: "text-[9]", style: { color: textColor, opacity: 0.7 }, children: open ? "▲" : "▼" })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
192
+ }
193
+ /** A Select bound to a choice-style APVTS parameter (same value↔index
194
+ mapping as ParamSegmented). */
195
+ export function ParamSelect({ paramId, options, label, ...rest }) {
196
+ const param = useParameter(paramId);
197
+ const count = Math.max(1, options.length);
198
+ const index = Math.round(param.value * (count - 1));
199
+ return (_jsx(Select, { options: options, index: index, label: label ?? param.name.toUpperCase(), onChange: (i) => {
200
+ param.begin();
201
+ param.set(count <= 1 ? 0 : i / (count - 1));
202
+ param.end();
203
+ }, ...rest }));
204
+ }
package/dist/hooks.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ import type { LayoutRect } from "./primitives";
2
+ /**
3
+ * Captures a node's root-space rect from its onLayout prop:
4
+ *
5
+ * const [rect, onLayout] = useLayoutRect();
6
+ * <View onLayout={onLayout} /> // rect updates whenever layout moves it
7
+ */
8
+ export declare function useLayoutRect(): [LayoutRect | null, (rect: LayoutRect) => void];
1
9
  /**
2
10
  * Subscribes to a C++ event (RootView::sendNativeEvent) for the lifetime
3
11
  * of the component. The handler can close over fresh state — it is kept
package/dist/hooks.js CHANGED
@@ -1,6 +1,16 @@
1
1
  // Convenience hooks over the native bridge.
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { native } from "./native";
4
+ /**
5
+ * Captures a node's root-space rect from its onLayout prop:
6
+ *
7
+ * const [rect, onLayout] = useLayoutRect();
8
+ * <View onLayout={onLayout} /> // rect updates whenever layout moves it
9
+ */
10
+ export function useLayoutRect() {
11
+ const [rect, setRect] = useState(null);
12
+ return [rect, setRect];
13
+ }
4
14
  /**
5
15
  * Subscribes to a C++ event (RootView::sendNativeEvent) for the lifetime
6
16
  * of the component. The handler can close over fresh state — it is kept
@@ -13,6 +13,8 @@ const hostTypes = {
13
13
  };
14
14
  const eventPropNames = {
15
15
  onClick: "click",
16
+ onDoubleClick: "dblclick",
17
+ onWheel: "wheel",
16
18
  onMouseEnter: "mouseenter",
17
19
  onMouseLeave: "mouseleave",
18
20
  onMouseDown: "mousedown",
@@ -20,6 +22,7 @@ const eventPropNames = {
20
22
  onDragStart: "dragstart",
21
23
  onDrag: "drag",
22
24
  onDragEnd: "dragend",
25
+ onLayout: "layout",
23
26
  onChange: "change",
24
27
  onSubmit: "submit",
25
28
  onFocus: "focus",
package/dist/index.d.ts CHANGED
@@ -4,7 +4,10 @@ export { View, Text, Image, TextInput, NativeView } from "./primitives";
4
4
  export type { CommonProps, TextProps, ImageProps, TextInputProps, NativeViewProps, } from "./primitives";
5
5
  export { render, unmount } from "./render";
6
6
  export { native } from "./native";
7
- export { useNativeEvent, useDebounced } from "./hooks";
7
+ export { useNativeEvent, useDebounced, useLayoutRect } from "./hooks";
8
+ export { useOverlay, OverlayLayer } from "./overlay";
9
+ export { Tooltip, Modal } from "./popover";
10
+ export type { TooltipProps, ModalProps } from "./popover";
8
11
  export { configureTheme, tw } from "./tw";
9
12
  export type { Style, ResolvedClasses } from "./tw";
10
13
  export { cx } from "./cx";
@@ -13,8 +16,8 @@ export { useTween, useSpring, springStep, lerp, Easing } from "./animation";
13
16
  export type { TweenOptions, SpringOptions, EasingFn } from "./animation";
14
17
  export { useParameter, useParameterList } from "./parameters";
15
18
  export type { ParameterState, ParameterHandle, ParameterInfo } from "./parameters";
16
- export { Knob, Slider, Toggle, XYPad, Segmented, GenericEditor, ParamKnob, ParamSlider, ParamToggle, ParamXYPad, ParamSegmented, dragToValue, } from "./controls";
17
- export type { KnobProps, SliderProps, ToggleProps, XYPadProps, SegmentedProps, GenericEditorProps, ParamKnobProps, ParamSliderProps, ParamToggleProps, ParamXYPadProps, ParamSegmentedProps, } from "./controls";
19
+ export { Knob, Slider, Toggle, XYPad, Segmented, Select, GenericEditor, ParamKnob, ParamSlider, ParamToggle, ParamXYPad, ParamSegmented, ParamSelect, dragToValue, } from "./controls";
20
+ export type { KnobProps, SliderProps, ToggleProps, XYPadProps, SegmentedProps, SelectProps, GenericEditorProps, ParamKnobProps, ParamSliderProps, ParamToggleProps, ParamXYPadProps, ParamSegmentedProps, ParamSelectProps, } from "./controls";
18
21
  export { Meter, usePeakHold, peakHoldStep } from "./meter";
19
22
  export type { MeterProps, PeakHoldOptions, PeakHoldState } from "./meter";
20
- export type { DragEventPayload } from "./primitives";
23
+ export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
package/dist/index.js CHANGED
@@ -5,10 +5,12 @@ import "./bridge";
5
5
  export { View, Text, Image, TextInput, NativeView } from "./primitives";
6
6
  export { render, unmount } from "./render";
7
7
  export { native } from "./native";
8
- export { useNativeEvent, useDebounced } from "./hooks";
8
+ export { useNativeEvent, useDebounced, useLayoutRect } from "./hooks";
9
+ export { useOverlay, OverlayLayer } from "./overlay";
10
+ export { Tooltip, Modal } from "./popover";
9
11
  export { configureTheme, tw } from "./tw";
10
12
  export { cx } from "./cx";
11
13
  export { useTween, useSpring, springStep, lerp, Easing } from "./animation";
12
14
  export { useParameter, useParameterList } from "./parameters";
13
- export { Knob, Slider, Toggle, XYPad, Segmented, GenericEditor, ParamKnob, ParamSlider, ParamToggle, ParamXYPad, ParamSegmented, dragToValue, } from "./controls";
15
+ export { Knob, Slider, Toggle, XYPad, Segmented, Select, GenericEditor, ParamKnob, ParamSlider, ParamToggle, ParamXYPad, ParamSegmented, ParamSelect, dragToValue, } from "./controls";
14
16
  export { Meter, usePeakHold, peakHoldStep } from "./meter";
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from "react";
2
+ /** A per-component slot in the overlay layer. show() replaces the slot's
3
+ content; hide() removes it; unmounting cleans up automatically. */
4
+ export declare function useOverlay(): {
5
+ show: (node: ReactNode) => void;
6
+ hide: () => void;
7
+ };
8
+ /** Mounted automatically by render() as the last sibling of your app. */
9
+ export declare function OverlayLayer(): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // The overlay layer — content that paints above everything else (menus,
3
+ // tooltips, modals). render() mounts <OverlayLayer/> after your app
4
+ // automatically, so painting order puts overlays on top and hit-testing
5
+ // reaches them first. Position entries absolutely using rects from
6
+ // onLayout / useLayoutRect.
7
+ import { Fragment, useCallback, useEffect, useRef, useSyncExternalStore } from "react";
8
+ let entries = [];
9
+ const subscribers = new Set();
10
+ let nextOverlayKey = 1;
11
+ function emit() {
12
+ for (const subscriber of subscribers)
13
+ subscriber();
14
+ }
15
+ function setEntry(key, node) {
16
+ entries = [...entries.filter((e) => e.key !== key), { key, node }];
17
+ emit();
18
+ }
19
+ function clearEntry(key) {
20
+ if (!entries.some((e) => e.key === key))
21
+ return;
22
+ entries = entries.filter((e) => e.key !== key);
23
+ emit();
24
+ }
25
+ /** A per-component slot in the overlay layer. show() replaces the slot's
26
+ content; hide() removes it; unmounting cleans up automatically. */
27
+ export function useOverlay() {
28
+ const key = useRef(0);
29
+ if (key.current === 0)
30
+ key.current = nextOverlayKey++;
31
+ useEffect(() => {
32
+ const k = key.current;
33
+ return () => clearEntry(k);
34
+ }, []);
35
+ return {
36
+ show: useCallback((node) => setEntry(key.current, node), []),
37
+ hide: useCallback(() => clearEntry(key.current), []),
38
+ };
39
+ }
40
+ /** Mounted automatically by render() as the last sibling of your app. */
41
+ export function OverlayLayer() {
42
+ const list = useSyncExternalStore((onStoreChange) => {
43
+ subscribers.add(onStoreChange);
44
+ return () => subscribers.delete(onStoreChange);
45
+ }, () => entries);
46
+ return (_jsx(_Fragment, { children: list.map((entry) => (_jsx(Fragment, { children: entry.node }, entry.key))) }));
47
+ }
@@ -3,6 +3,8 @@ export interface ParameterState {
3
3
  text: string;
4
4
  name: string;
5
5
  label: string;
6
+ /** The host's normalized default — double-click-reset target. */
7
+ defaultValue: number;
6
8
  }
7
9
  export interface ParameterHandle extends ParameterState {
8
10
  set: (normalized: number) => void;
@@ -16,6 +18,7 @@ export interface ParameterInfo {
16
18
  /** Normalized 0..1 snapshot at mount — use useParameter(id) for live values. */
17
19
  value: number;
18
20
  text: string;
21
+ defaultValue: number;
19
22
  }
20
23
  /**
21
24
  * Enumerates every APVTS parameter (via param:list) once at mount — the
@@ -18,6 +18,7 @@ export function useParameterList() {
18
18
  label: String(entry?.label ?? ""),
19
19
  value: Number(entry?.value ?? 0),
20
20
  text: String(entry?.text ?? ""),
21
+ defaultValue: Number(entry?.defaultValue ?? 0),
21
22
  }));
22
23
  });
23
24
  return list;
@@ -31,8 +32,9 @@ export function useParameter(id) {
31
32
  text: String(initial.text ?? ""),
32
33
  name: String(initial.name ?? id),
33
34
  label: String(initial.label ?? ""),
35
+ defaultValue: Number(initial.defaultValue ?? 0),
34
36
  }
35
- : { value: 0, text: "", name: id, label: "" };
37
+ : { value: 0, text: "", name: id, label: "", defaultValue: 0 };
36
38
  });
37
39
  useEffect(() => native.on("param", (p) => {
38
40
  if (p?.id === id)
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from "react";
2
+ export interface TooltipProps {
3
+ /** The tip text. */
4
+ label: string;
5
+ /** Hover dwell before showing. Default 450ms. */
6
+ delayMs?: number;
7
+ /** Gap between the child and the tip. Default 6. */
8
+ offset?: number;
9
+ backgroundColor?: string;
10
+ color?: string;
11
+ children: ReactNode;
12
+ }
13
+ /** Wraps its child; hovering it long enough shows a tip below. */
14
+ export declare function Tooltip({ label, delayMs, offset, backgroundColor, color, children, }: TooltipProps): import("react").JSX.Element;
15
+ export interface ModalProps {
16
+ open: boolean;
17
+ /** Backdrop click (and nothing else) calls this. */
18
+ onClose: () => void;
19
+ title?: string;
20
+ width?: number;
21
+ backgroundColor?: string;
22
+ backdropColor?: string;
23
+ children: ReactNode;
24
+ }
25
+ /** A centered dialog over a click-away backdrop — confirms, settings,
26
+ about boxes. Content is yours; the chrome is minimal. */
27
+ export declare function Modal({ open, onClose, title, width, backgroundColor, backdropColor, children, }: ModalProps): null;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Tooltip + Modal — the dialog kit, built on the overlay layer and
3
+ // onLayout. Both paint above everything via useOverlay.
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { View, Text } from "./primitives";
6
+ import { useLayoutRect } from "./hooks";
7
+ import { useOverlay } from "./overlay";
8
+ /** Wraps its child; hovering it long enough shows a tip below. */
9
+ export function Tooltip({ label, delayMs = 450, offset = 6, backgroundColor = "#20241F", color = "#d4d4d8", children, }) {
10
+ const [rect, onLayout] = useLayoutRect();
11
+ const [visible, setVisible] = useState(false);
12
+ const overlay = useOverlay();
13
+ const timer = useRef(null);
14
+ useEffect(() => {
15
+ if (!visible || rect === null) {
16
+ overlay.hide();
17
+ return;
18
+ }
19
+ overlay.show(_jsx(View, { className: "absolute rounded-md border px-2 py-[4]", style: {
20
+ left: rect.x,
21
+ top: rect.y + rect.height + offset,
22
+ backgroundColor,
23
+ borderColor: "#00000066",
24
+ }, children: _jsx(Text, { className: "text-[11]", style: { color }, children: label }) }));
25
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ }, [visible, rect, label, offset, backgroundColor, color]);
27
+ useEffect(() => () => {
28
+ if (timer.current !== null)
29
+ clearTimeout(timer.current);
30
+ }, []);
31
+ return (_jsx(View, { onLayout: onLayout, onMouseEnter: () => {
32
+ if (timer.current !== null)
33
+ clearTimeout(timer.current);
34
+ timer.current = setTimeout(() => setVisible(true), delayMs);
35
+ }, onMouseLeave: () => {
36
+ if (timer.current !== null)
37
+ clearTimeout(timer.current);
38
+ timer.current = null;
39
+ setVisible(false);
40
+ }, children: children }));
41
+ }
42
+ /** A centered dialog over a click-away backdrop — confirms, settings,
43
+ about boxes. Content is yours; the chrome is minimal. */
44
+ export function Modal({ open, onClose, title, width = 320, backgroundColor = "#1C201B", backdropColor = "#000000B0", children, }) {
45
+ const overlay = useOverlay();
46
+ useEffect(() => {
47
+ if (!open) {
48
+ overlay.hide();
49
+ return;
50
+ }
51
+ overlay.show(_jsx(View, { className: "absolute inset-0 items-center justify-center", style: { backgroundColor: backdropColor }, onClick: onClose, children: _jsxs(View, { className: "rounded-xl border p-5 gap-3 shadow-xl", style: { width, backgroundColor, borderColor: "#00000066" }, onClick: () => { }, children: [title !== undefined ? (_jsx(Text, { className: "text-[13] font-bold tracking-wide", children: title })) : null, children] }) }));
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [open, title, width, backgroundColor, backdropColor, children]);
54
+ return null;
55
+ }
@@ -8,6 +8,18 @@ export interface DragEventPayload {
8
8
  x: number;
9
9
  y: number;
10
10
  }
11
+ /** A node's laid-out rect in root coordinates (scroll-adjusted). */
12
+ export interface LayoutRect {
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ }
18
+ /** Mouse-wheel payload. dy is JUCE's notch fraction (~0.1 per notch,
19
+ positive = wheel up). */
20
+ export interface WheelEventPayload {
21
+ dy: number;
22
+ }
11
23
  export interface CommonProps {
12
24
  className?: string;
13
25
  style?: Style;
@@ -15,6 +27,9 @@ export interface CommonProps {
15
27
  /** Resets the scroll offset of an overflow-y-scroll container. */
16
28
  scrollTop?: number;
17
29
  onClick?: () => void;
30
+ onDoubleClick?: () => void;
31
+ /** Wheel over this node (controls win the wheel over scroll containers). */
32
+ onWheel?: (e: WheelEventPayload) => void;
18
33
  onMouseEnter?: () => void;
19
34
  onMouseLeave?: () => void;
20
35
  onMouseDown?: () => void;
@@ -22,6 +37,9 @@ export interface CommonProps {
22
37
  onDragStart?: (e: DragEventPayload) => void;
23
38
  onDrag?: (e: DragEventPayload) => void;
24
39
  onDragEnd?: (e: DragEventPayload) => void;
40
+ /** Fires after layout whenever this node's root-space rect changes —
41
+ the foundation for popovers, menus, and tooltips. */
42
+ onLayout?: (rect: LayoutRect) => void;
25
43
  }
26
44
  export declare function View(props: CommonProps): import("react").ReactElement<CommonProps, string | import("react").JSXElementConstructor<any>>;
27
45
  export interface TextProps extends Omit<CommonProps, "children"> {
@@ -55,6 +73,8 @@ export declare function TextInput({ onChange, onSubmit, ...rest }: TextInputProp
55
73
  className?: string | undefined;
56
74
  scrollTop?: number | undefined;
57
75
  onClick?: (() => void) | undefined;
76
+ onDoubleClick?: (() => void) | undefined;
77
+ onWheel?: ((e: WheelEventPayload) => void) | undefined;
58
78
  onMouseEnter?: (() => void) | undefined;
59
79
  onMouseLeave?: (() => void) | undefined;
60
80
  onMouseDown?: (() => void) | undefined;
@@ -62,6 +82,7 @@ export declare function TextInput({ onChange, onSubmit, ...rest }: TextInputProp
62
82
  onDragStart?: ((e: DragEventPayload) => void) | undefined;
63
83
  onDrag?: ((e: DragEventPayload) => void) | undefined;
64
84
  onDragEnd?: ((e: DragEventPayload) => void) | undefined;
85
+ onLayout?: ((rect: LayoutRect) => void) | undefined;
65
86
  }, string | import("react").JSXElementConstructor<any>>;
66
87
  export interface NativeViewProps extends CommonProps {
67
88
  /** Id of a component factory registered in the C++ NativeRegistry. */
package/dist/render.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
- /** Mounts (or re-renders) the app into the plugin window. */
2
+ /** Mounts (or re-renders) the app into the plugin window. The overlay
3
+ layer (menus, tooltips) mounts after the app so it paints on top. */
3
4
  export declare function render(element: ReactNode): void;
4
5
  /** Unmounts the current app (used by hot reload teardown). */
5
6
  export declare function unmount(): void;
package/dist/render.js CHANGED
@@ -1,15 +1,18 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
2
  import Reconciler from "react-reconciler";
2
3
  import { LegacyRoot } from "react-reconciler/constants";
3
4
  import { hostConfig } from "./hostConfig";
4
5
  import { flushOps } from "./bridge";
6
+ import { OverlayLayer } from "./overlay";
5
7
  const reconciler = Reconciler(hostConfig);
6
8
  let container = null;
7
- /** Mounts (or re-renders) the app into the plugin window. */
9
+ /** Mounts (or re-renders) the app into the plugin window. The overlay
10
+ layer (menus, tooltips) mounts after the app so it paints on top. */
8
11
  export function render(element) {
9
12
  if (!container) {
10
13
  container = reconciler.createContainer({}, LegacyRoot, null, false, null, "vsreact", (error) => console.error("[vsreact] recoverable error:", error), null);
11
14
  }
12
- reconciler.updateContainer(element, container, null, null);
15
+ reconciler.updateContainer(_jsxs(_Fragment, { children: [element, _jsx(OverlayLayer, {})] }), container, null, null);
13
16
  flushOps();
14
17
  }
15
18
  /** Unmounts the current app (used by hot reload teardown). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsreact/core",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Write React. Ship native VST. A React renderer for JUCE audio plugins — QuickJS + Yoga + juce::Graphics, no webview.",
5
5
  "keywords": [
6
6
  "react",
@@ -20,7 +20,7 @@
20
20
  "bugs": "https://github.com/N9RecordsTechnologiesIL/VSReacT/issues",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/N9RecordsTechnologiesIL/VSReacT.git",
23
+ "url": "git+https://github.com/N9RecordsTechnologiesIL/VSReacT.git",
24
24
  "directory": "vsreact/js"
25
25
  },
26
26
  "main": "dist/index.js",