@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.
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from "react";
2
+ export interface ButtonProps {
3
+ label?: string;
4
+ children?: ReactNode;
5
+ onClick: () => void;
6
+ disabled?: boolean;
7
+ /** solid (default) — filled; outline — bordered; ghost — text only. */
8
+ variant?: "solid" | "outline" | "ghost";
9
+ size?: "sm" | "md" | "lg";
10
+ accentColor?: string;
11
+ /** Text color for the solid variant (default near-black). */
12
+ textColor?: string;
13
+ }
14
+ /** A pressable with sensible plugin styling — solid/outline/ghost. */
15
+ export declare function Button({ label, children, onClick, disabled, variant, size, accentColor, textColor, }: ButtonProps): import("react").JSX.Element;
package/dist/button.js ADDED
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { View, Text } from "./primitives";
3
+ import { cx } from "./cx";
4
+ const SIZES = {
5
+ sm: { pad: "px-3 py-[5]", text: 11 },
6
+ md: { pad: "px-4 py-[8]", text: 12 },
7
+ lg: { pad: "px-5 py-[11]", text: 13 },
8
+ };
9
+ /** A pressable with sensible plugin styling — solid/outline/ghost. */
10
+ export function Button({ label, children, onClick, disabled, variant = "solid", size = "md", accentColor = "#C6F135", textColor = "#09090b", }) {
11
+ const { pad, text } = SIZES[size];
12
+ const className = cx("flex-row items-center justify-center gap-2 rounded-lg", pad, disabled ? "opacity-40" : "cursor-pointer", !disabled && variant === "solid" && "hover:opacity-90 active:opacity-75", !disabled && variant !== "solid" && "hover:bg-white/10 active:bg-white/15", variant === "outline" && "border");
13
+ const style = variant === "solid"
14
+ ? { backgroundColor: accentColor }
15
+ : variant === "outline"
16
+ ? { borderColor: accentColor }
17
+ : {};
18
+ const color = variant === "solid" ? textColor : accentColor;
19
+ return (_jsxs(View, { className: className, style: style, onClick: disabled ? undefined : onClick, children: [label !== undefined ? (_jsx(Text, { className: "font-bold tracking-wide", style: { fontSize: text, color }, children: label })) : null, children] }));
20
+ }
@@ -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;
package/dist/controls.js CHANGED
@@ -15,8 +15,16 @@ export function dragToValue(startValue, dy, sensitivity = 0.005) {
15
15
  }
16
16
  const ARC_START = -135;
17
17
  const ARC_END = 135;
18
- 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, }) {
19
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
+ };
20
28
  return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `items-center justify-center ${disabled ? "opacity-40" : "cursor-pointer"}`, style: {
21
29
  width: size,
22
30
  height: size,
@@ -24,21 +32,25 @@ export function Knob({ value, text, label, size = 64, disabled, trackColor = "#2
24
32
  arcColor: valueColor,
25
33
  arcStart: ARC_START,
26
34
  arcEnd: ARC_END,
27
- 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,
28
37
  arcThickness: Math.max(3, size * 0.08),
29
38
  }, onDragStart: disabled
30
39
  ? undefined
31
40
  : () => {
32
- startValue.current = value;
41
+ startValue.current = clamped;
33
42
  onBegin?.();
34
- }, 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] }));
35
46
  }
36
- /** A Knob bound to an APVTS parameter (via ParameterBridge). */
37
- 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 }) {
38
50
  const param = useParameter(paramId);
39
- 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 }));
40
52
  }
41
- 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, }) {
42
54
  const startValue = useRef(0);
43
55
  const clamped = clamp01(value);
44
56
  const onDragStart = disabled
@@ -48,15 +60,25 @@ export function Slider({ value, width = 160, height = 160, vertical, label, disa
48
60
  onBegin?.();
49
61
  };
50
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);
51
72
  if (vertical) {
52
- 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] }));
53
74
  }
54
- 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] }));
55
76
  }
56
- /** A Slider bound to an APVTS parameter (via ParameterBridge). */
57
- 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 }) {
58
80
  const param = useParameter(paramId);
59
- 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 }));
60
82
  }
61
83
  /** A switch with a spring-animated thumb — bypass, on/off, A/B. */
62
84
  export function Toggle({ on, label, size = 22, disabled, trackColor = "#2A2F27", onColor = "#C6F135", thumbColor = "#F4F4F5", onChange, }) {
package/dist/hooks.d.ts CHANGED
@@ -22,3 +22,21 @@ export declare function useNativeEvent(name: string, handler: (payload: any) =>
22
22
  * useEffect(() => { native.call("library:search", { query }); }, [query]);
23
23
  */
24
24
  export declare function useDebounced<T>(value: T, delayMs: number): T;
25
+ /** The value, updated at most once per `intervalMs` (leading + trailing). */
26
+ export declare function useThrottled<T>(value: T, intervalMs: number): T;
27
+ /** The value from the previous render — undefined on the first one. */
28
+ export declare function usePrevious<T>(value: T): T | undefined;
29
+ /** Boolean state with a stable toggle — bypass buttons, panels, A/B. */
30
+ export declare function useToggle(initial?: boolean): [boolean, () => void, (next: boolean) => void];
31
+ /** A declarative interval on the host scheduler; null pauses it. The
32
+ callback stays fresh without restarting the timer. */
33
+ export declare function useInterval(callback: () => void, intervalMs: number | null): void;
34
+ /** Hover state + the props that drive it:
35
+ `const [hovered, hoverProps] = useHover(); <View {...hoverProps}>` */
36
+ export declare function useHover(): [
37
+ boolean,
38
+ {
39
+ onMouseEnter: () => void;
40
+ onMouseLeave: () => void;
41
+ }
42
+ ];
package/dist/hooks.js CHANGED
@@ -38,3 +38,59 @@ export function useDebounced(value, delayMs) {
38
38
  }, [value, delayMs]);
39
39
  return debounced;
40
40
  }
41
+ /** The value, updated at most once per `intervalMs` (leading + trailing). */
42
+ export function useThrottled(value, intervalMs) {
43
+ const [throttled, setThrottled] = useState(value);
44
+ const lastRun = useRef(0);
45
+ useEffect(() => {
46
+ const now = Date.now();
47
+ const elapsed = now - lastRun.current;
48
+ if (elapsed >= intervalMs) {
49
+ lastRun.current = now;
50
+ setThrottled(value);
51
+ return;
52
+ }
53
+ const id = setTimeout(() => {
54
+ lastRun.current = Date.now();
55
+ setThrottled(value);
56
+ }, intervalMs - elapsed);
57
+ return () => clearTimeout(id);
58
+ }, [value, intervalMs]);
59
+ return throttled;
60
+ }
61
+ /** The value from the previous render — undefined on the first one. */
62
+ export function usePrevious(value) {
63
+ const previous = useRef(undefined);
64
+ useEffect(() => {
65
+ previous.current = value;
66
+ }, [value]);
67
+ return previous.current;
68
+ }
69
+ /** Boolean state with a stable toggle — bypass buttons, panels, A/B. */
70
+ export function useToggle(initial = false) {
71
+ const [on, setOn] = useState(initial);
72
+ const toggle = useRef(() => setOn((v) => !v)).current;
73
+ return [on, toggle, setOn];
74
+ }
75
+ /** A declarative interval on the host scheduler; null pauses it. The
76
+ callback stays fresh without restarting the timer. */
77
+ export function useInterval(callback, intervalMs) {
78
+ const callbackRef = useRef(callback);
79
+ callbackRef.current = callback;
80
+ useEffect(() => {
81
+ if (intervalMs === null)
82
+ return;
83
+ const id = setInterval(() => callbackRef.current(), intervalMs);
84
+ return () => clearInterval(id);
85
+ }, [intervalMs]);
86
+ }
87
+ /** Hover state + the props that drive it:
88
+ `const [hovered, hoverProps] = useHover(); <View {...hoverProps}>` */
89
+ export function useHover() {
90
+ const [hovered, setHovered] = useState(false);
91
+ const props = useRef({
92
+ onMouseEnter: () => setHovered(true),
93
+ onMouseLeave: () => setHovered(false),
94
+ }).current;
95
+ return [hovered, props];
96
+ }
@@ -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",
package/dist/index.d.ts CHANGED
@@ -4,8 +4,14 @@ 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, useLayoutRect } from "./hooks";
7
+ export { useNativeEvent, useDebounced, useThrottled, usePrevious, useToggle, useInterval, useHover, useLayoutRect, } from "./hooks";
8
8
  export { useOverlay, OverlayLayer } from "./overlay";
9
+ export { Tooltip, Modal } from "./popover";
10
+ export type { TooltipProps, ModalProps } from "./popover";
11
+ export { Button } from "./button";
12
+ export type { ButtonProps } from "./button";
13
+ export { Bars, Waveform, useRollingBuffer, pushRolling } from "./visualizers";
14
+ export type { BarsProps, WaveformProps } from "./visualizers";
9
15
  export { configureTheme, tw } from "./tw";
10
16
  export type { Style, ResolvedClasses } from "./tw";
11
17
  export { cx } from "./cx";
@@ -18,4 +24,4 @@ export { Knob, Slider, Toggle, XYPad, Segmented, Select, GenericEditor, ParamKno
18
24
  export type { KnobProps, SliderProps, ToggleProps, XYPadProps, SegmentedProps, SelectProps, GenericEditorProps, ParamKnobProps, ParamSliderProps, ParamToggleProps, ParamXYPadProps, ParamSegmentedProps, ParamSelectProps, } from "./controls";
19
25
  export { Meter, usePeakHold, peakHoldStep } from "./meter";
20
26
  export type { MeterProps, PeakHoldOptions, PeakHoldState } from "./meter";
21
- export type { DragEventPayload, LayoutRect } from "./primitives";
27
+ export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
package/dist/index.js CHANGED
@@ -5,8 +5,11 @@ 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, useLayoutRect } from "./hooks";
8
+ export { useNativeEvent, useDebounced, useThrottled, usePrevious, useToggle, useInterval, useHover, useLayoutRect, } from "./hooks";
9
9
  export { useOverlay, OverlayLayer } from "./overlay";
10
+ export { Tooltip, Modal } from "./popover";
11
+ export { Button } from "./button";
12
+ export { Bars, Waveform, useRollingBuffer, pushRolling } from "./visualizers";
10
13
  export { configureTheme, tw } from "./tw";
11
14
  export { cx } from "./cx";
12
15
  export { useTween, useSpring, springStep, lerp, Easing } from "./animation";
@@ -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
+ }
@@ -15,6 +15,11 @@ export interface LayoutRect {
15
15
  width: number;
16
16
  height: number;
17
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
+ }
18
23
  export interface CommonProps {
19
24
  className?: string;
20
25
  style?: Style;
@@ -22,6 +27,9 @@ export interface CommonProps {
22
27
  /** Resets the scroll offset of an overflow-y-scroll container. */
23
28
  scrollTop?: number;
24
29
  onClick?: () => void;
30
+ onDoubleClick?: () => void;
31
+ /** Wheel over this node (controls win the wheel over scroll containers). */
32
+ onWheel?: (e: WheelEventPayload) => void;
25
33
  onMouseEnter?: () => void;
26
34
  onMouseLeave?: () => void;
27
35
  onMouseDown?: () => void;
@@ -65,6 +73,8 @@ export declare function TextInput({ onChange, onSubmit, ...rest }: TextInputProp
65
73
  className?: string | undefined;
66
74
  scrollTop?: number | undefined;
67
75
  onClick?: (() => void) | undefined;
76
+ onDoubleClick?: (() => void) | undefined;
77
+ onWheel?: ((e: WheelEventPayload) => void) | undefined;
68
78
  onMouseEnter?: (() => void) | undefined;
69
79
  onMouseLeave?: (() => void) | undefined;
70
80
  onMouseDown?: (() => void) | undefined;
@@ -0,0 +1,37 @@
1
+ /** Pure rolling-window step (exported for tests): drop the oldest, push
2
+ the newest, pad with zeros to `length`. */
3
+ export declare function pushRolling(previous: number[], value: number, length: number): number[];
4
+ /** A rolling window of the last `length` values — turns a live scalar
5
+ (meter level, envelope) into data for <Waveform>/<Bars> history. */
6
+ export declare function useRollingBuffer(value: number, length?: number): number[];
7
+ export interface BarsProps {
8
+ /** One bar per entry, 0..1. */
9
+ values: number[];
10
+ width?: number;
11
+ height?: number;
12
+ /** Gap between bars. Default 2. */
13
+ gap?: number;
14
+ /** Bars at or above this turn hot. Default 0.85; 1 disables. */
15
+ hotFrom?: number;
16
+ trackColor?: string;
17
+ color?: string;
18
+ hotColor?: string;
19
+ label?: string;
20
+ }
21
+ /** Bottom-anchored bars — spectrum analyzers, band meters. */
22
+ export declare function Bars({ values, width, height, gap, hotFrom, trackColor, color, hotColor, label, }: BarsProps): import("react").JSX.Element;
23
+ export interface WaveformProps {
24
+ /** One bar per entry, −1..1 (or 0..1 — bars mirror around the centre). */
25
+ values: number[];
26
+ width?: number;
27
+ height?: number;
28
+ gap?: number;
29
+ trackColor?: string;
30
+ color?: string;
31
+ /** Draw the centre line. Default true. */
32
+ centerLine?: boolean;
33
+ centerLineColor?: string;
34
+ label?: string;
35
+ }
36
+ /** Centre-mirrored bars — waveform overviews, envelope history. */
37
+ export declare function Waveform({ values, width, height, gap, trackColor, color, centerLine, centerLineColor, label, }: WaveformProps): import("react").JSX.Element;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Audio visualizers — bar-based displays fed by arrays of values
3
+ // (typically pushed from C++ via useNativeEvent). For audio-rate scopes,
4
+ // host a juce::Component with <NativeView>; these cover the JS-rate
5
+ // cases: spectrum bars, envelope history, waveform overviews.
6
+ import { useEffect, useState } from "react";
7
+ import { View, Text } from "./primitives";
8
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
9
+ /** Pure rolling-window step (exported for tests): drop the oldest, push
10
+ the newest, pad with zeros to `length`. */
11
+ export function pushRolling(previous, value, length) {
12
+ const next = previous.slice(Math.max(0, previous.length - length + 1));
13
+ next.push(value);
14
+ while (next.length < length)
15
+ next.unshift(0);
16
+ return next;
17
+ }
18
+ /** A rolling window of the last `length` values — turns a live scalar
19
+ (meter level, envelope) into data for <Waveform>/<Bars> history. */
20
+ export function useRollingBuffer(value, length = 64) {
21
+ const [buffer, setBuffer] = useState(() => Array(length).fill(0));
22
+ useEffect(() => {
23
+ setBuffer((previous) => pushRolling(previous, value, length));
24
+ }, [value, length]);
25
+ return buffer;
26
+ }
27
+ /** Bottom-anchored bars — spectrum analyzers, band meters. */
28
+ export function Bars({ values, width = 160, height = 60, gap = 2, hotFrom = 0.85, trackColor = "#141714", color = "#C6F135", hotColor = "#FF4545", label, }) {
29
+ const bars = (_jsx(View, { className: "flex-row items-end rounded overflow-hidden px-[2]", style: { width, height, backgroundColor: trackColor, columnGap: gap }, children: values.map((value, index) => {
30
+ const level = clamp01(value);
31
+ return (_jsx(View, { className: "flex-1 rounded-[1]", style: {
32
+ height: Math.max(1, level * (height - 4)),
33
+ backgroundColor: level >= hotFrom ? hotColor : color,
34
+ } }, index));
35
+ }) }));
36
+ if (label === undefined)
37
+ return bars;
38
+ return (_jsxs(View, { className: "items-center gap-2", children: [bars, _jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })] }));
39
+ }
40
+ /** Centre-mirrored bars — waveform overviews, envelope history. */
41
+ export function Waveform({ values, width = 160, height = 60, gap = 2, trackColor = "#141714", color = "#C6F135", centerLine = true, centerLineColor = "#FFFFFF22", label, }) {
42
+ const wave = (_jsxs(View, { className: "relative flex-row items-center rounded overflow-hidden px-[2]", style: { width, height, backgroundColor: trackColor, columnGap: gap }, children: [centerLine ? (_jsx(View, { className: "absolute left-0 right-0 h-[1]", style: { top: height / 2, backgroundColor: centerLineColor } })) : null, values.map((value, index) => (_jsx(View, { className: "flex-1 rounded-[1]", style: {
43
+ height: Math.max(1, Math.min(1, Math.abs(value)) * (height - 4)),
44
+ backgroundColor: color,
45
+ } }, index)))] }));
46
+ if (label === undefined)
47
+ return wave;
48
+ return (_jsxs(View, { className: "items-center gap-2", children: [wave, _jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })] }));
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsreact/core",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
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",
package/src/button.tsx ADDED
@@ -0,0 +1,68 @@
1
+ // Button — the pressable with hover/active states baked in.
2
+
3
+ import type { ReactNode } from "react";
4
+ import { View, Text } from "./primitives";
5
+ import { cx } from "./cx";
6
+ import type { Style } from "./tw";
7
+
8
+ export interface ButtonProps {
9
+ label?: string;
10
+ children?: ReactNode;
11
+ onClick: () => void;
12
+ disabled?: boolean;
13
+ /** solid (default) — filled; outline — bordered; ghost — text only. */
14
+ variant?: "solid" | "outline" | "ghost";
15
+ size?: "sm" | "md" | "lg";
16
+ accentColor?: string;
17
+ /** Text color for the solid variant (default near-black). */
18
+ textColor?: string;
19
+ }
20
+
21
+ const SIZES = {
22
+ sm: { pad: "px-3 py-[5]", text: 11 },
23
+ md: { pad: "px-4 py-[8]", text: 12 },
24
+ lg: { pad: "px-5 py-[11]", text: 13 },
25
+ } as const;
26
+
27
+ /** A pressable with sensible plugin styling — solid/outline/ghost. */
28
+ export function Button({
29
+ label,
30
+ children,
31
+ onClick,
32
+ disabled,
33
+ variant = "solid",
34
+ size = "md",
35
+ accentColor = "#C6F135",
36
+ textColor = "#09090b",
37
+ }: ButtonProps) {
38
+ const { pad, text } = SIZES[size];
39
+
40
+ const className = cx(
41
+ "flex-row items-center justify-center gap-2 rounded-lg",
42
+ pad,
43
+ disabled ? "opacity-40" : "cursor-pointer",
44
+ !disabled && variant === "solid" && "hover:opacity-90 active:opacity-75",
45
+ !disabled && variant !== "solid" && "hover:bg-white/10 active:bg-white/15",
46
+ variant === "outline" && "border",
47
+ );
48
+
49
+ const style: Style =
50
+ variant === "solid"
51
+ ? { backgroundColor: accentColor }
52
+ : variant === "outline"
53
+ ? { borderColor: accentColor }
54
+ : {};
55
+
56
+ const color = variant === "solid" ? textColor : accentColor;
57
+
58
+ return (
59
+ <View className={className} style={style} onClick={disabled ? undefined : onClick}>
60
+ {label !== undefined ? (
61
+ <Text className="font-bold tracking-wide" style={{ fontSize: text, color }}>
62
+ {label}
63
+ </Text>
64
+ ) : null}
65
+ {children}
66
+ </View>
67
+ );
68
+ }