@vsreact/core 0.0.5 → 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.
- package/dist/controls.d.ts +22 -6
- package/dist/controls.js +35 -13
- package/dist/hostConfig.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/parameters.d.ts +3 -0
- package/dist/parameters.js +3 -1
- package/dist/popover.d.ts +27 -0
- package/dist/popover.js +55 -0
- package/dist/primitives.d.ts +10 -0
- package/package.json +1 -1
- package/src/controls.test.tsx +131 -0
- package/src/controls.tsx +65 -12
- package/src/hostConfig.ts +2 -0
- package/src/index.ts +3 -1
- package/src/parameters.ts +6 -1
- package/src/popover.tsx +137 -0
- package/src/primitives.tsx +9 -0
package/dist/controls.d.ts
CHANGED
|
@@ -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
|
|
24
|
-
|
|
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
|
|
50
|
-
|
|
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
|
-
|
|
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 =
|
|
41
|
+
startValue.current = clamped;
|
|
33
42
|
onBegin?.();
|
|
34
|
-
}, onDrag: disabled ? undefined : (e) => onChange(dragToValue(startValue.current, e.dy)), onDragEnd: disabled ? undefined : () => onEnd?.(),
|
|
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
|
|
37
|
-
|
|
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(),
|
|
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
|
|
57
|
-
|
|
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(),
|
|
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/hostConfig.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { render, unmount } from "./render";
|
|
|
6
6
|
export { native } from "./native";
|
|
7
7
|
export { useNativeEvent, useDebounced, useLayoutRect } from "./hooks";
|
|
8
8
|
export { useOverlay, OverlayLayer } from "./overlay";
|
|
9
|
+
export { Tooltip, Modal } from "./popover";
|
|
10
|
+
export type { TooltipProps, ModalProps } from "./popover";
|
|
9
11
|
export { configureTheme, tw } from "./tw";
|
|
10
12
|
export type { Style, ResolvedClasses } from "./tw";
|
|
11
13
|
export { cx } from "./cx";
|
|
@@ -18,4 +20,4 @@ export { Knob, Slider, Toggle, XYPad, Segmented, Select, GenericEditor, ParamKno
|
|
|
18
20
|
export type { KnobProps, SliderProps, ToggleProps, XYPadProps, SegmentedProps, SelectProps, GenericEditorProps, ParamKnobProps, ParamSliderProps, ParamToggleProps, ParamXYPadProps, ParamSegmentedProps, ParamSelectProps, } from "./controls";
|
|
19
21
|
export { Meter, usePeakHold, peakHoldStep } from "./meter";
|
|
20
22
|
export type { MeterProps, PeakHoldOptions, PeakHoldState } from "./meter";
|
|
21
|
-
export type { DragEventPayload, LayoutRect } from "./primitives";
|
|
23
|
+
export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { render, unmount } from "./render";
|
|
|
7
7
|
export { native } from "./native";
|
|
8
8
|
export { useNativeEvent, useDebounced, useLayoutRect } from "./hooks";
|
|
9
9
|
export { useOverlay, OverlayLayer } from "./overlay";
|
|
10
|
+
export { Tooltip, Modal } from "./popover";
|
|
10
11
|
export { configureTheme, tw } from "./tw";
|
|
11
12
|
export { cx } from "./cx";
|
|
12
13
|
export { useTween, useSpring, springStep, lerp, Easing } from "./animation";
|
package/dist/parameters.d.ts
CHANGED
|
@@ -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
|
package/dist/parameters.js
CHANGED
|
@@ -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;
|
package/dist/popover.js
ADDED
|
@@ -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
|
+
}
|
package/dist/primitives.d.ts
CHANGED
|
@@ -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;
|
package/package.json
CHANGED
package/src/controls.test.tsx
CHANGED
|
@@ -302,3 +302,134 @@ 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
|
+
});
|
package/src/controls.tsx
CHANGED
|
@@ -25,6 +25,13 @@ export interface KnobProps {
|
|
|
25
25
|
label?: string;
|
|
26
26
|
size?: number;
|
|
27
27
|
disabled?: boolean;
|
|
28
|
+
/** Double-click resets to this (DAW convention). */
|
|
29
|
+
defaultValue?: number;
|
|
30
|
+
/** Value arc sweeps from 12 o'clock instead of the left stop — for
|
|
31
|
+
centre-based parameters like pan. */
|
|
32
|
+
bipolar?: boolean;
|
|
33
|
+
/** Value change per wheel notch fraction. 0 disables. Default 0.4. */
|
|
34
|
+
wheelSensitivity?: number;
|
|
28
35
|
trackColor?: string;
|
|
29
36
|
valueColor?: string;
|
|
30
37
|
onChange: (value: number) => void;
|
|
@@ -38,6 +45,9 @@ export function Knob({
|
|
|
38
45
|
label,
|
|
39
46
|
size = 64,
|
|
40
47
|
disabled,
|
|
48
|
+
defaultValue,
|
|
49
|
+
bipolar,
|
|
50
|
+
wheelSensitivity = 0.4,
|
|
41
51
|
trackColor = "#2A2F27",
|
|
42
52
|
valueColor = "#C6F135",
|
|
43
53
|
onChange,
|
|
@@ -45,6 +55,15 @@ export function Knob({
|
|
|
45
55
|
onEnd,
|
|
46
56
|
}: KnobProps) {
|
|
47
57
|
const startValue = useRef(0);
|
|
58
|
+
const clamped = clamp01(value);
|
|
59
|
+
const angle = ARC_START + (ARC_END - ARC_START) * clamped;
|
|
60
|
+
const center = (ARC_START + ARC_END) / 2;
|
|
61
|
+
|
|
62
|
+
const nudge = (target: number) => {
|
|
63
|
+
onBegin?.();
|
|
64
|
+
onChange(clamp01(target));
|
|
65
|
+
onEnd?.();
|
|
66
|
+
};
|
|
48
67
|
|
|
49
68
|
return (
|
|
50
69
|
<View className="items-center gap-2">
|
|
@@ -57,19 +76,28 @@ export function Knob({
|
|
|
57
76
|
arcColor: valueColor,
|
|
58
77
|
arcStart: ARC_START,
|
|
59
78
|
arcEnd: ARC_END,
|
|
60
|
-
|
|
79
|
+
arcValueStart: bipolar ? Math.min(center, angle) : ARC_START,
|
|
80
|
+
arcValueEnd: bipolar ? Math.max(center, angle) : angle,
|
|
61
81
|
arcThickness: Math.max(3, size * 0.08),
|
|
62
82
|
}}
|
|
63
83
|
onDragStart={
|
|
64
84
|
disabled
|
|
65
85
|
? undefined
|
|
66
86
|
: () => {
|
|
67
|
-
startValue.current =
|
|
87
|
+
startValue.current = clamped;
|
|
68
88
|
onBegin?.();
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
onDrag={disabled ? undefined : (e) => onChange(dragToValue(startValue.current, e.dy))}
|
|
72
92
|
onDragEnd={disabled ? undefined : () => onEnd?.()}
|
|
93
|
+
onDoubleClick={
|
|
94
|
+
disabled || defaultValue === undefined ? undefined : () => nudge(defaultValue)
|
|
95
|
+
}
|
|
96
|
+
onWheel={
|
|
97
|
+
disabled || wheelSensitivity === 0
|
|
98
|
+
? undefined
|
|
99
|
+
: (e) => nudge(clamped + e.dy * wheelSensitivity)
|
|
100
|
+
}
|
|
73
101
|
>
|
|
74
102
|
{text !== undefined ? (
|
|
75
103
|
<Text
|
|
@@ -91,12 +119,15 @@ export interface ParamKnobProps {
|
|
|
91
119
|
paramId: string;
|
|
92
120
|
label?: string;
|
|
93
121
|
size?: number;
|
|
122
|
+
bipolar?: boolean;
|
|
123
|
+
wheelSensitivity?: number;
|
|
94
124
|
trackColor?: string;
|
|
95
125
|
valueColor?: string;
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
/** A Knob bound to an APVTS parameter
|
|
99
|
-
|
|
128
|
+
/** A Knob bound to an APVTS parameter — double-click resets to the
|
|
129
|
+
host's default, the wheel nudges, gestures stay automation-safe. */
|
|
130
|
+
export function ParamKnob({ paramId, label, ...rest }: ParamKnobProps) {
|
|
100
131
|
const param = useParameter(paramId);
|
|
101
132
|
|
|
102
133
|
return (
|
|
@@ -104,12 +135,11 @@ export function ParamKnob({ paramId, label, size, trackColor, valueColor }: Para
|
|
|
104
135
|
value={param.value}
|
|
105
136
|
text={param.text}
|
|
106
137
|
label={label ?? param.name.toUpperCase()}
|
|
107
|
-
|
|
108
|
-
trackColor={trackColor}
|
|
109
|
-
valueColor={valueColor}
|
|
138
|
+
defaultValue={param.defaultValue}
|
|
110
139
|
onChange={param.set}
|
|
111
140
|
onBegin={param.begin}
|
|
112
141
|
onEnd={param.end}
|
|
142
|
+
{...rest}
|
|
113
143
|
/>
|
|
114
144
|
);
|
|
115
145
|
}
|
|
@@ -124,6 +154,10 @@ export interface SliderProps {
|
|
|
124
154
|
vertical?: boolean;
|
|
125
155
|
label?: string;
|
|
126
156
|
disabled?: boolean;
|
|
157
|
+
/** Double-click resets to this (DAW convention). */
|
|
158
|
+
defaultValue?: number;
|
|
159
|
+
/** Value change per wheel notch fraction. 0 disables. Default 0.4. */
|
|
160
|
+
wheelSensitivity?: number;
|
|
127
161
|
trackColor?: string;
|
|
128
162
|
valueColor?: string;
|
|
129
163
|
onChange: (value: number) => void;
|
|
@@ -138,6 +172,8 @@ export function Slider({
|
|
|
138
172
|
vertical,
|
|
139
173
|
label,
|
|
140
174
|
disabled,
|
|
175
|
+
defaultValue,
|
|
176
|
+
wheelSensitivity = 0.4,
|
|
141
177
|
trackColor = "#2A2F27",
|
|
142
178
|
valueColor = "#C6F135",
|
|
143
179
|
onChange,
|
|
@@ -155,6 +191,18 @@ export function Slider({
|
|
|
155
191
|
};
|
|
156
192
|
const onDragEnd = disabled ? undefined : () => onEnd?.();
|
|
157
193
|
|
|
194
|
+
const nudge = (target: number) => {
|
|
195
|
+
onBegin?.();
|
|
196
|
+
onChange(clamp01(target));
|
|
197
|
+
onEnd?.();
|
|
198
|
+
};
|
|
199
|
+
const onDoubleClick =
|
|
200
|
+
disabled || defaultValue === undefined ? undefined : () => nudge(defaultValue);
|
|
201
|
+
const onWheel =
|
|
202
|
+
disabled || wheelSensitivity === 0
|
|
203
|
+
? undefined
|
|
204
|
+
: (e: { dy: number }) => nudge(clamped + e.dy * wheelSensitivity);
|
|
205
|
+
|
|
158
206
|
if (vertical) {
|
|
159
207
|
return (
|
|
160
208
|
<View className="items-center gap-2">
|
|
@@ -164,6 +212,8 @@ export function Slider({
|
|
|
164
212
|
onDragStart={onDragStart}
|
|
165
213
|
onDrag={disabled ? undefined : (e) => onChange(clamp01(startValue.current - e.dy / height))}
|
|
166
214
|
onDragEnd={onDragEnd}
|
|
215
|
+
onDoubleClick={onDoubleClick}
|
|
216
|
+
onWheel={onWheel}
|
|
167
217
|
>
|
|
168
218
|
<View
|
|
169
219
|
className="absolute w-[4] rounded-full left-[7] top-0 bottom-0"
|
|
@@ -193,6 +243,8 @@ export function Slider({
|
|
|
193
243
|
onDragStart={onDragStart}
|
|
194
244
|
onDrag={disabled ? undefined : (e) => onChange(clamp01(startValue.current + e.dx / width))}
|
|
195
245
|
onDragEnd={onDragEnd}
|
|
246
|
+
onDoubleClick={onDoubleClick}
|
|
247
|
+
onWheel={onWheel}
|
|
196
248
|
>
|
|
197
249
|
<View className="h-[4] rounded-full" style={{ backgroundColor: trackColor }} />
|
|
198
250
|
<View
|
|
@@ -217,22 +269,23 @@ export interface ParamSliderProps {
|
|
|
217
269
|
width?: number;
|
|
218
270
|
height?: number;
|
|
219
271
|
vertical?: boolean;
|
|
272
|
+
wheelSensitivity?: number;
|
|
220
273
|
}
|
|
221
274
|
|
|
222
|
-
/** A Slider bound to an APVTS parameter
|
|
223
|
-
|
|
275
|
+
/** A Slider bound to an APVTS parameter — double-click resets to the
|
|
276
|
+
host's default, the wheel nudges, gestures stay automation-safe. */
|
|
277
|
+
export function ParamSlider({ paramId, label, ...rest }: ParamSliderProps) {
|
|
224
278
|
const param = useParameter(paramId);
|
|
225
279
|
|
|
226
280
|
return (
|
|
227
281
|
<Slider
|
|
228
282
|
value={param.value}
|
|
229
283
|
label={label ?? param.name.toUpperCase()}
|
|
230
|
-
|
|
231
|
-
height={height}
|
|
232
|
-
vertical={vertical}
|
|
284
|
+
defaultValue={param.defaultValue}
|
|
233
285
|
onChange={param.set}
|
|
234
286
|
onBegin={param.begin}
|
|
235
287
|
onEnd={param.end}
|
|
288
|
+
{...rest}
|
|
236
289
|
/>
|
|
237
290
|
);
|
|
238
291
|
}
|
package/src/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
|
@@ -15,6 +15,8 @@ export { render, unmount } from "./render";
|
|
|
15
15
|
export { native } from "./native";
|
|
16
16
|
export { useNativeEvent, useDebounced, useLayoutRect } from "./hooks";
|
|
17
17
|
export { useOverlay, OverlayLayer } from "./overlay";
|
|
18
|
+
export { Tooltip, Modal } from "./popover";
|
|
19
|
+
export type { TooltipProps, ModalProps } from "./popover";
|
|
18
20
|
export { configureTheme, tw } from "./tw";
|
|
19
21
|
export type { Style, ResolvedClasses } from "./tw";
|
|
20
22
|
export { cx } from "./cx";
|
|
@@ -56,4 +58,4 @@ export type {
|
|
|
56
58
|
} from "./controls";
|
|
57
59
|
export { Meter, usePeakHold, peakHoldStep } from "./meter";
|
|
58
60
|
export type { MeterProps, PeakHoldOptions, PeakHoldState } from "./meter";
|
|
59
|
-
export type { DragEventPayload, LayoutRect } from "./primitives";
|
|
61
|
+
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(
|
package/src/popover.tsx
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Tooltip + Modal — the dialog kit, built on the overlay layer and
|
|
2
|
+
// onLayout. Both paint above everything via useOverlay.
|
|
3
|
+
|
|
4
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
5
|
+
import { View, Text } from "./primitives";
|
|
6
|
+
import { useLayoutRect } from "./hooks";
|
|
7
|
+
import { useOverlay } from "./overlay";
|
|
8
|
+
|
|
9
|
+
export interface TooltipProps {
|
|
10
|
+
/** The tip text. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Hover dwell before showing. Default 450ms. */
|
|
13
|
+
delayMs?: number;
|
|
14
|
+
/** Gap between the child and the tip. Default 6. */
|
|
15
|
+
offset?: number;
|
|
16
|
+
backgroundColor?: string;
|
|
17
|
+
color?: string;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Wraps its child; hovering it long enough shows a tip below. */
|
|
22
|
+
export function Tooltip({
|
|
23
|
+
label,
|
|
24
|
+
delayMs = 450,
|
|
25
|
+
offset = 6,
|
|
26
|
+
backgroundColor = "#20241F",
|
|
27
|
+
color = "#d4d4d8",
|
|
28
|
+
children,
|
|
29
|
+
}: TooltipProps) {
|
|
30
|
+
const [rect, onLayout] = useLayoutRect();
|
|
31
|
+
const [visible, setVisible] = useState(false);
|
|
32
|
+
const overlay = useOverlay();
|
|
33
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!visible || rect === null) {
|
|
37
|
+
overlay.hide();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
overlay.show(
|
|
42
|
+
<View
|
|
43
|
+
className="absolute rounded-md border px-2 py-[4]"
|
|
44
|
+
style={{
|
|
45
|
+
left: rect.x,
|
|
46
|
+
top: rect.y + rect.height + offset,
|
|
47
|
+
backgroundColor,
|
|
48
|
+
borderColor: "#00000066",
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<Text className="text-[11]" style={{ color }}>
|
|
52
|
+
{label}
|
|
53
|
+
</Text>
|
|
54
|
+
</View>,
|
|
55
|
+
);
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, [visible, rect, label, offset, backgroundColor, color]);
|
|
58
|
+
|
|
59
|
+
useEffect(
|
|
60
|
+
() => () => {
|
|
61
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
62
|
+
},
|
|
63
|
+
[],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<View
|
|
68
|
+
onLayout={onLayout}
|
|
69
|
+
onMouseEnter={() => {
|
|
70
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
71
|
+
timer.current = setTimeout(() => setVisible(true), delayMs);
|
|
72
|
+
}}
|
|
73
|
+
onMouseLeave={() => {
|
|
74
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
75
|
+
timer.current = null;
|
|
76
|
+
setVisible(false);
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ModalProps {
|
|
85
|
+
open: boolean;
|
|
86
|
+
/** Backdrop click (and nothing else) calls this. */
|
|
87
|
+
onClose: () => void;
|
|
88
|
+
title?: string;
|
|
89
|
+
width?: number;
|
|
90
|
+
backgroundColor?: string;
|
|
91
|
+
backdropColor?: string;
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A centered dialog over a click-away backdrop — confirms, settings,
|
|
96
|
+
about boxes. Content is yours; the chrome is minimal. */
|
|
97
|
+
export function Modal({
|
|
98
|
+
open,
|
|
99
|
+
onClose,
|
|
100
|
+
title,
|
|
101
|
+
width = 320,
|
|
102
|
+
backgroundColor = "#1C201B",
|
|
103
|
+
backdropColor = "#000000B0",
|
|
104
|
+
children,
|
|
105
|
+
}: ModalProps) {
|
|
106
|
+
const overlay = useOverlay();
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!open) {
|
|
110
|
+
overlay.hide();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
overlay.show(
|
|
115
|
+
<View
|
|
116
|
+
className="absolute inset-0 items-center justify-center"
|
|
117
|
+
style={{ backgroundColor: backdropColor }}
|
|
118
|
+
onClick={onClose}
|
|
119
|
+
>
|
|
120
|
+
{/* swallow clicks inside the panel so they don't reach the backdrop */}
|
|
121
|
+
<View
|
|
122
|
+
className="rounded-xl border p-5 gap-3 shadow-xl"
|
|
123
|
+
style={{ width, backgroundColor, borderColor: "#00000066" }}
|
|
124
|
+
onClick={() => {}}
|
|
125
|
+
>
|
|
126
|
+
{title !== undefined ? (
|
|
127
|
+
<Text className="text-[13] font-bold tracking-wide">{title}</Text>
|
|
128
|
+
) : null}
|
|
129
|
+
{children}
|
|
130
|
+
</View>
|
|
131
|
+
</View>,
|
|
132
|
+
);
|
|
133
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
+
}, [open, title, width, backgroundColor, backdropColor, children]);
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
package/src/primitives.tsx
CHANGED
|
@@ -18,6 +18,12 @@ export interface LayoutRect {
|
|
|
18
18
|
height: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** Mouse-wheel payload. dy is JUCE's notch fraction (~0.1 per notch,
|
|
22
|
+
positive = wheel up). */
|
|
23
|
+
export interface WheelEventPayload {
|
|
24
|
+
dy: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
export interface CommonProps {
|
|
22
28
|
className?: string;
|
|
23
29
|
style?: Style;
|
|
@@ -25,6 +31,9 @@ export interface CommonProps {
|
|
|
25
31
|
/** Resets the scroll offset of an overflow-y-scroll container. */
|
|
26
32
|
scrollTop?: number;
|
|
27
33
|
onClick?: () => void;
|
|
34
|
+
onDoubleClick?: () => void;
|
|
35
|
+
/** Wheel over this node (controls win the wheel over scroll containers). */
|
|
36
|
+
onWheel?: (e: WheelEventPayload) => void;
|
|
28
37
|
onMouseEnter?: () => void;
|
|
29
38
|
onMouseLeave?: () => void;
|
|
30
39
|
onMouseDown?: () => void;
|