@vsreact/core 0.0.1 → 0.0.3

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,38 @@
1
+ export type EasingFn = (t: number) => number;
2
+ export declare const Easing: {
3
+ linear: EasingFn;
4
+ outCubic: EasingFn;
5
+ inOutCubic: EasingFn;
6
+ outExpo: EasingFn;
7
+ outBack: EasingFn;
8
+ outQuint: EasingFn;
9
+ };
10
+ export interface TweenOptions {
11
+ /** Milliseconds the tween runs for (after the delay). */
12
+ duration: number;
13
+ delay?: number;
14
+ easing?: EasingFn;
15
+ onComplete?: () => void;
16
+ }
17
+ /** Eased progress 0→1 that starts when the component mounts. */
18
+ export declare function useTween({ duration, delay, easing, onComplete }: TweenOptions): number;
19
+ export declare function lerp(from: number, to: number, t: number): number;
20
+ export interface SpringOptions {
21
+ /** Spring constant — higher snaps faster. Default 170. */
22
+ stiffness?: number;
23
+ /** Velocity drag — higher settles with less bounce. Default 24. */
24
+ damping?: number;
25
+ mass?: number;
26
+ /** Distance from target below which the spring snaps and stops. */
27
+ restDelta?: number;
28
+ }
29
+ /** One semi-implicit Euler integration step; exported for tests and for
30
+ driving springs from your own loops. Returns [position, velocity]. */
31
+ export declare function springStep(position: number, velocity: number, target: number, { stiffness, damping, mass }: SpringOptions, dtMs: number): [number, number];
32
+ /**
33
+ * A value that springs toward `target` whenever it changes — for
34
+ * interactive motion where a fixed-duration tween feels wrong (toggle
35
+ * thumbs, drawers, meters chasing levels). Runs on the host timer like
36
+ * useTween; starts at rest on the initial target.
37
+ */
38
+ export declare function useSpring(target: number, options?: SpringOptions): number;
@@ -0,0 +1,90 @@
1
+ // Frame-driven animation for VSReacT. Tweens run on the host timer (16ms
2
+ // ticks through the C++ Scheduler) and re-render by setting React state, so
3
+ // every animated style flows through the normal setProps → repaint path.
4
+ import { useEffect, useRef, useState } from "react";
5
+ export const Easing = {
6
+ linear: ((t) => t),
7
+ outCubic: ((t) => 1 - Math.pow(1 - t, 3)),
8
+ inOutCubic: ((t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),
9
+ outExpo: ((t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t))),
10
+ outBack: ((t) => {
11
+ const c1 = 1.70158;
12
+ const c3 = c1 + 1;
13
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
14
+ }),
15
+ outQuint: ((t) => 1 - Math.pow(1 - t, 5)),
16
+ };
17
+ const FRAME_MS = 16;
18
+ /** Eased progress 0→1 that starts when the component mounts. */
19
+ export function useTween({ duration, delay = 0, easing = Easing.outCubic, onComplete }) {
20
+ const [progress, setProgress] = useState(0);
21
+ const doneRef = useRef(false);
22
+ const completeRef = useRef(onComplete);
23
+ completeRef.current = onComplete;
24
+ useEffect(() => {
25
+ let elapsed = -delay;
26
+ const id = setInterval(() => {
27
+ elapsed += FRAME_MS;
28
+ if (elapsed <= 0)
29
+ return;
30
+ const t = Math.min(1, elapsed / duration);
31
+ setProgress(easing(t));
32
+ if (t >= 1) {
33
+ clearInterval(id);
34
+ if (!doneRef.current) {
35
+ doneRef.current = true;
36
+ completeRef.current?.();
37
+ }
38
+ }
39
+ }, FRAME_MS);
40
+ return () => clearInterval(id);
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, []);
43
+ return progress;
44
+ }
45
+ export function lerp(from, to, t) {
46
+ return from + (to - from) * t;
47
+ }
48
+ /** One semi-implicit Euler integration step; exported for tests and for
49
+ driving springs from your own loops. Returns [position, velocity]. */
50
+ export function springStep(position, velocity, target, { stiffness = 170, damping = 24, mass = 1 }, dtMs) {
51
+ const dt = dtMs / 1000;
52
+ const acceleration = (-stiffness * (position - target) - damping * velocity) / mass;
53
+ const v = velocity + acceleration * dt;
54
+ return [position + v * dt, v];
55
+ }
56
+ /**
57
+ * A value that springs toward `target` whenever it changes — for
58
+ * interactive motion where a fixed-duration tween feels wrong (toggle
59
+ * thumbs, drawers, meters chasing levels). Runs on the host timer like
60
+ * useTween; starts at rest on the initial target.
61
+ */
62
+ export function useSpring(target, options = {}) {
63
+ const [value, setValue] = useState(target);
64
+ const state = useRef({ position: target, velocity: 0 });
65
+ const targetRef = useRef(target);
66
+ targetRef.current = target;
67
+ const optionsRef = useRef(options);
68
+ optionsRef.current = options;
69
+ useEffect(() => {
70
+ const restDelta = optionsRef.current.restDelta ?? 0.001;
71
+ if (Math.abs(state.current.position - targetRef.current) <= restDelta &&
72
+ Math.abs(state.current.velocity) <= restDelta) {
73
+ return;
74
+ }
75
+ const id = setInterval(() => {
76
+ const s = state.current;
77
+ const [p, v] = springStep(s.position, s.velocity, targetRef.current, optionsRef.current, FRAME_MS);
78
+ s.position = p;
79
+ s.velocity = v;
80
+ if (Math.abs(p - targetRef.current) <= restDelta && Math.abs(v) <= restDelta) {
81
+ s.position = targetRef.current;
82
+ s.velocity = 0;
83
+ clearInterval(id);
84
+ }
85
+ setValue(s.position);
86
+ }, FRAME_MS);
87
+ return () => clearInterval(id);
88
+ }, [target]);
89
+ return value;
90
+ }
@@ -0,0 +1,7 @@
1
+ export type Op = unknown[];
2
+ export declare function queueOp(op: Op): void;
3
+ export declare function flushOps(): void;
4
+ export type EventHandler = (payload: unknown) => void;
5
+ export declare function setHandlers(nodeId: number, handlers: Map<string, EventHandler>): void;
6
+ export declare function removeHandlers(nodeId: number): void;
7
+ export declare function addNativeListener(name: string, cb: EventHandler): () => void;
package/dist/bridge.js ADDED
@@ -0,0 +1,49 @@
1
+ // JS side of the mutation/event bridge. Ops queue up during a React commit
2
+ // and flush to C++ as one JSON batch; C++ pushes events back through the
3
+ // __vsreact_dispatch global registered here.
4
+ import { fireTimer } from "./runtime";
5
+ let queue = [];
6
+ export function queueOp(op) {
7
+ queue.push(op);
8
+ }
9
+ export function flushOps() {
10
+ if (queue.length === 0)
11
+ return;
12
+ const json = JSON.stringify(queue);
13
+ queue = [];
14
+ globalThis.__vsreact_flush?.(json);
15
+ }
16
+ const eventHandlers = new Map();
17
+ export function setHandlers(nodeId, handlers) {
18
+ if (handlers.size === 0)
19
+ eventHandlers.delete(nodeId);
20
+ else
21
+ eventHandlers.set(nodeId, handlers);
22
+ }
23
+ export function removeHandlers(nodeId) {
24
+ eventHandlers.delete(nodeId);
25
+ }
26
+ const nativeListeners = new Map();
27
+ export function addNativeListener(name, cb) {
28
+ let set = nativeListeners.get(name);
29
+ if (!set)
30
+ nativeListeners.set(name, (set = new Set()));
31
+ set.add(cb);
32
+ return () => {
33
+ set.delete(cb);
34
+ if (set.size === 0)
35
+ nativeListeners.delete(name);
36
+ };
37
+ }
38
+ globalThis.__vsreact_dispatch = (json) => {
39
+ const msg = JSON.parse(json);
40
+ if (msg.kind === "timer" && msg.id !== undefined) {
41
+ fireTimer(msg.id);
42
+ }
43
+ else if (msg.kind === "event" && msg.nodeId !== undefined && msg.type) {
44
+ eventHandlers.get(msg.nodeId)?.get(msg.type)?.(msg.payload);
45
+ }
46
+ else if (msg.kind === "native" && msg.name) {
47
+ nativeListeners.get(msg.name)?.forEach((cb) => cb(msg.payload));
48
+ }
49
+ };
@@ -0,0 +1,126 @@
1
+ /** Vertical-drag-to-value mapping shared by Knob (and tested in isolation). */
2
+ export declare function dragToValue(startValue: number, dy: number, sensitivity?: number): number;
3
+ export interface KnobProps {
4
+ value: number;
5
+ text?: string;
6
+ label?: string;
7
+ size?: number;
8
+ disabled?: boolean;
9
+ trackColor?: string;
10
+ valueColor?: string;
11
+ onChange: (value: number) => void;
12
+ onBegin?: () => void;
13
+ onEnd?: () => void;
14
+ }
15
+ export declare function Knob({ value, text, label, size, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: KnobProps): import("react").JSX.Element;
16
+ export interface ParamKnobProps {
17
+ paramId: string;
18
+ label?: string;
19
+ size?: number;
20
+ trackColor?: string;
21
+ valueColor?: string;
22
+ }
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;
25
+ export interface SliderProps {
26
+ value: number;
27
+ /** Track length when horizontal (default 160). */
28
+ width?: number;
29
+ /** Track length when vertical (default 160). */
30
+ height?: number;
31
+ /** Vertical fader — drag up for more, fill rises from the bottom. */
32
+ vertical?: boolean;
33
+ label?: string;
34
+ disabled?: boolean;
35
+ trackColor?: string;
36
+ valueColor?: string;
37
+ onChange: (value: number) => void;
38
+ onBegin?: () => void;
39
+ onEnd?: () => void;
40
+ }
41
+ export declare function Slider({ value, width, height, vertical, label, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: SliderProps): import("react").JSX.Element;
42
+ export interface ParamSliderProps {
43
+ paramId: string;
44
+ label?: string;
45
+ width?: number;
46
+ height?: number;
47
+ vertical?: boolean;
48
+ }
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;
51
+ export interface ToggleProps {
52
+ on: boolean;
53
+ label?: string;
54
+ /** Track height; width is 1.8×. Default 22. */
55
+ size?: number;
56
+ disabled?: boolean;
57
+ trackColor?: string;
58
+ onColor?: string;
59
+ thumbColor?: string;
60
+ onChange: (on: boolean) => void;
61
+ }
62
+ /** A switch with a spring-animated thumb — bypass, on/off, A/B. */
63
+ export declare function Toggle({ on, label, size, disabled, trackColor, onColor, thumbColor, onChange, }: ToggleProps): import("react").JSX.Element;
64
+ export interface ParamToggleProps {
65
+ paramId: string;
66
+ label?: string;
67
+ size?: number;
68
+ trackColor?: string;
69
+ onColor?: string;
70
+ thumbColor?: string;
71
+ }
72
+ /** A Toggle bound to a bool-style APVTS parameter (on = value ≥ 0.5). */
73
+ export declare function ParamToggle({ paramId, label, ...rest }: ParamToggleProps): import("react").JSX.Element;
74
+ export interface XYPadProps {
75
+ /** Normalized 0..1. */
76
+ x: number;
77
+ /** Normalized 0..1 — 1 is the TOP of the pad (drag up for more). */
78
+ y: number;
79
+ width?: number;
80
+ height?: number;
81
+ label?: string;
82
+ disabled?: boolean;
83
+ trackColor?: string;
84
+ valueColor?: string;
85
+ onChange: (x: number, y: number) => void;
86
+ onBegin?: () => void;
87
+ onEnd?: () => void;
88
+ }
89
+ /** A 2D drag pad with crosshair — filter cutoff/resonance, pan/depth… */
90
+ export declare function XYPad({ x, y, width, height, label, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: XYPadProps): import("react").JSX.Element;
91
+ export interface ParamXYPadProps {
92
+ paramX: string;
93
+ paramY: string;
94
+ width?: number;
95
+ height?: number;
96
+ label?: string;
97
+ trackColor?: string;
98
+ valueColor?: string;
99
+ }
100
+ /** An XYPad driving two APVTS parameters at once. */
101
+ export declare function ParamXYPad({ paramX, paramY, label, ...rest }: ParamXYPadProps): import("react").JSX.Element;
102
+ export interface SegmentedProps {
103
+ options: string[];
104
+ index: number;
105
+ label?: string;
106
+ disabled?: boolean;
107
+ trackColor?: string;
108
+ activeColor?: string;
109
+ textColor?: string;
110
+ activeTextColor?: string;
111
+ onChange: (index: number) => void;
112
+ }
113
+ /** A row of exclusive options — oscillator shapes, filter modes, A/B/C. */
114
+ export declare function Segmented({ options, index, label, disabled, trackColor, activeColor, textColor, activeTextColor, onChange, }: SegmentedProps): import("react").JSX.Element;
115
+ export interface ParamSegmentedProps {
116
+ paramId: string;
117
+ options: string[];
118
+ label?: string;
119
+ trackColor?: string;
120
+ activeColor?: string;
121
+ textColor?: string;
122
+ activeTextColor?: string;
123
+ }
124
+ /** A Segmented bound to a choice-style APVTS parameter: the normalized
125
+ value maps to an option index (index / (count − 1)). */
126
+ export declare function ParamSegmented({ paramId, options, label, ...rest }: ParamSegmentedProps): import("react").JSX.Element;
@@ -0,0 +1,130 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The built-in VST controls — knobs, sliders, toggles, XY pads, segmented
3
+ // switches — drawn by the VSReacT painter and driven by drag gestures.
4
+ // Each has a Param* variant bound to an APVTS parameter.
5
+ import { useRef } from "react";
6
+ import { View, Text } from "./primitives";
7
+ import { useParameter } from "./parameters";
8
+ import { useSpring } from "./animation";
9
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
10
+ /** Vertical-drag-to-value mapping shared by Knob (and tested in isolation). */
11
+ export function dragToValue(startValue, dy, sensitivity = 0.005) {
12
+ return clamp01(startValue - dy * sensitivity);
13
+ }
14
+ const ARC_START = -135;
15
+ const ARC_END = 135;
16
+ export function Knob({ value, text, label, size = 64, disabled, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
17
+ const startValue = useRef(0);
18
+ return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `items-center justify-center ${disabled ? "opacity-40" : "cursor-pointer"}`, style: {
19
+ width: size,
20
+ height: size,
21
+ arcTrackColor: trackColor,
22
+ arcColor: valueColor,
23
+ arcStart: ARC_START,
24
+ arcEnd: ARC_END,
25
+ arcValueEnd: ARC_START + (ARC_END - ARC_START) * Math.min(1, Math.max(0, value)),
26
+ arcThickness: Math.max(3, size * 0.08),
27
+ }, onDragStart: disabled
28
+ ? undefined
29
+ : () => {
30
+ startValue.current = value;
31
+ 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] }));
33
+ }
34
+ /** A Knob bound to an APVTS parameter (via ParameterBridge). */
35
+ export function ParamKnob({ paramId, label, size, trackColor, valueColor }) {
36
+ 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 }));
38
+ }
39
+ export function Slider({ value, width = 160, height = 160, vertical, label, disabled, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
40
+ const startValue = useRef(0);
41
+ const clamped = clamp01(value);
42
+ const onDragStart = disabled
43
+ ? undefined
44
+ : () => {
45
+ startValue.current = clamped;
46
+ onBegin?.();
47
+ };
48
+ const onDragEnd = disabled ? undefined : () => onEnd?.();
49
+ 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] }));
51
+ }
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] }));
53
+ }
54
+ /** A Slider bound to an APVTS parameter (via ParameterBridge). */
55
+ export function ParamSlider({ paramId, label, width, height, vertical }) {
56
+ 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 }));
58
+ }
59
+ /** A switch with a spring-animated thumb — bypass, on/off, A/B. */
60
+ export function Toggle({ on, label, size = 22, disabled, trackColor = "#2A2F27", onColor = "#C6F135", thumbColor = "#F4F4F5", onChange, }) {
61
+ const t = useSpring(on ? 1 : 0, { stiffness: 300, damping: 28 });
62
+ const trackWidth = Math.round(size * 1.8);
63
+ const thumb = size - 6;
64
+ const travel = trackWidth - thumb - 6;
65
+ return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `relative rounded-full ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width: trackWidth, height: size, backgroundColor: on ? onColor : trackColor }, onClick: disabled ? undefined : () => onChange(!on), children: _jsx(View, { className: "absolute rounded-full", style: {
66
+ width: thumb,
67
+ height: thumb,
68
+ top: 3,
69
+ left: 3 + clamp01(t) * travel,
70
+ backgroundColor: thumbColor,
71
+ } }) }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
72
+ }
73
+ /** A Toggle bound to a bool-style APVTS parameter (on = value ≥ 0.5). */
74
+ export function ParamToggle({ paramId, label, ...rest }) {
75
+ const param = useParameter(paramId);
76
+ return (_jsx(Toggle, { on: param.value >= 0.5, label: label ?? param.name.toUpperCase(), onChange: (next) => {
77
+ param.begin();
78
+ param.set(next ? 1 : 0);
79
+ param.end();
80
+ }, ...rest }));
81
+ }
82
+ /** A 2D drag pad with crosshair — filter cutoff/resonance, pan/depth… */
83
+ export function XYPad({ x, y, width = 160, height = 160, label, disabled, trackColor = "#161B17", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
84
+ const start = useRef({ x: 0, y: 0 });
85
+ const cxv = clamp01(x);
86
+ const cyv = clamp01(y);
87
+ const thumb = 14;
88
+ const tx = cxv * (width - thumb);
89
+ const ty = (1 - cyv) * (height - thumb);
90
+ return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: `relative rounded-lg overflow-hidden border ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width, height, backgroundColor: trackColor, borderColor: "#00000055" }, onDragStart: disabled
91
+ ? undefined
92
+ : () => {
93
+ start.current = { x: cxv, y: cyv };
94
+ onBegin?.();
95
+ }, onDrag: disabled
96
+ ? undefined
97
+ : (e) => onChange(clamp01(start.current.x + e.dx / width), clamp01(start.current.y - e.dy / height)), onDragEnd: disabled ? undefined : () => onEnd?.(), children: [_jsx(View, { className: "absolute left-0 right-0 h-[1]", style: { top: ty + thumb / 2, backgroundColor: valueColor, opacity: 0.35 } }), _jsx(View, { className: "absolute top-0 bottom-0 w-[1]", style: { left: tx + thumb / 2, backgroundColor: valueColor, opacity: 0.35 } }), _jsx(View, { className: "absolute rounded-full", style: { width: thumb, height: thumb, left: tx, top: ty, backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
98
+ }
99
+ /** An XYPad driving two APVTS parameters at once. */
100
+ export function ParamXYPad({ paramX, paramY, label, ...rest }) {
101
+ const px = useParameter(paramX);
102
+ const py = useParameter(paramY);
103
+ return (_jsx(XYPad, { x: px.value, y: py.value, label: label ?? `${px.name} / ${py.name}`.toUpperCase(), onChange: (nx, ny) => {
104
+ px.set(nx);
105
+ py.set(ny);
106
+ }, onBegin: () => {
107
+ px.begin();
108
+ py.begin();
109
+ }, onEnd: () => {
110
+ px.end();
111
+ py.end();
112
+ }, ...rest }));
113
+ }
114
+ /** A row of exclusive options — oscillator shapes, filter modes, A/B/C. */
115
+ export function Segmented({ options, index, label, disabled, trackColor = "#2A2F27", activeColor = "#C6F135", textColor = "#a1a1aa", activeTextColor = "#09090b", onChange, }) {
116
+ const current = Math.min(options.length - 1, Math.max(0, index));
117
+ return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `flex-row rounded-lg p-[3] gap-[3] ${disabled ? "opacity-40" : ""}`, style: { backgroundColor: trackColor }, children: options.map((option, i) => (_jsx(View, { className: `px-3 py-[6] rounded-md ${disabled ? "" : "cursor-pointer"}`, style: { backgroundColor: i === current ? activeColor : "#00000000" }, onClick: disabled ? undefined : () => onChange(i), children: _jsx(Text, { className: "text-[11] font-bold tracking-wide", style: { color: i === current ? activeTextColor : textColor }, children: option }) }, `${option}-${i}`))) }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
118
+ }
119
+ /** A Segmented bound to a choice-style APVTS parameter: the normalized
120
+ value maps to an option index (index / (count − 1)). */
121
+ export function ParamSegmented({ paramId, options, label, ...rest }) {
122
+ const param = useParameter(paramId);
123
+ const count = Math.max(1, options.length);
124
+ const index = Math.round(param.value * (count - 1));
125
+ return (_jsx(Segmented, { options: options, index: index, label: label ?? param.name.toUpperCase(), onChange: (i) => {
126
+ param.begin();
127
+ param.set(count <= 1 ? 0 : i / (count - 1));
128
+ param.end();
129
+ }, ...rest }));
130
+ }
package/dist/cx.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type ClassValue = string | number | null | false | undefined | ClassValue[] | Record<string, unknown>;
2
+ /**
3
+ * `cx("flex", active && "bg-red-500", { "opacity-40": disabled })`
4
+ * → `"flex bg-red-500 opacity-40"`.
5
+ */
6
+ export declare function cx(...inputs: ClassValue[]): string;
package/dist/cx.js ADDED
@@ -0,0 +1,26 @@
1
+ // cx — compose conditional className strings (a tiny clsx).
2
+ /**
3
+ * `cx("flex", active && "bg-red-500", { "opacity-40": disabled })`
4
+ * → `"flex bg-red-500 opacity-40"`.
5
+ */
6
+ export function cx(...inputs) {
7
+ const out = [];
8
+ for (const input of inputs) {
9
+ if (!input && input !== 0)
10
+ continue;
11
+ if (typeof input === "string" || typeof input === "number") {
12
+ out.push(String(input));
13
+ }
14
+ else if (Array.isArray(input)) {
15
+ const nested = cx(...input);
16
+ if (nested)
17
+ out.push(nested);
18
+ }
19
+ else if (typeof input === "object") {
20
+ for (const [key, value] of Object.entries(input))
21
+ if (value)
22
+ out.push(key);
23
+ }
24
+ }
25
+ return out.join(" ");
26
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Subscribes to a C++ event (RootView::sendNativeEvent) for the lifetime
3
+ * of the component. The handler can close over fresh state — it is kept
4
+ * current without resubscribing.
5
+ *
6
+ * useNativeEvent("download:progress", (p) => setProgress(p.ratio));
7
+ */
8
+ export declare function useNativeEvent(name: string, handler: (payload: any) => void): void;
package/dist/hooks.js ADDED
@@ -0,0 +1,15 @@
1
+ // Convenience hooks over the native bridge.
2
+ import { useEffect, useRef } from "react";
3
+ import { native } from "./native";
4
+ /**
5
+ * Subscribes to a C++ event (RootView::sendNativeEvent) for the lifetime
6
+ * of the component. The handler can close over fresh state — it is kept
7
+ * current without resubscribing.
8
+ *
9
+ * useNativeEvent("download:progress", (p) => setProgress(p.ratio));
10
+ */
11
+ export function useNativeEvent(name, handler) {
12
+ const handlerRef = useRef(handler);
13
+ handlerRef.current = handler;
14
+ useEffect(() => native.on(name, (payload) => handlerRef.current(payload)), [name]);
15
+ }
@@ -0,0 +1,44 @@
1
+ export interface Instance {
2
+ id: number;
3
+ type: string;
4
+ }
5
+ export type TextInstance = Instance;
6
+ export declare const hostConfig: {
7
+ supportsMutation: boolean;
8
+ supportsPersistence: boolean;
9
+ supportsHydration: boolean;
10
+ isPrimaryRenderer: boolean;
11
+ supportsMicrotasks: boolean;
12
+ scheduleMicrotask: (fn: () => void) => void;
13
+ scheduleTimeout: (fn: (...args: unknown[]) => void, delay?: number) => number;
14
+ cancelTimeout: (id: ReturnType<typeof setTimeout>) => void;
15
+ noTimeout: -1;
16
+ getRootHostContext: () => {};
17
+ getChildHostContext: (parent: object) => object;
18
+ getPublicInstance: (instance: Instance) => Instance;
19
+ prepareForCommit: () => null;
20
+ resetAfterCommit: () => void;
21
+ shouldSetTextContent: () => boolean;
22
+ createInstance(type: string, props: Record<string, unknown>): Instance;
23
+ createTextInstance(text: string): TextInstance;
24
+ appendInitialChild: (parent: Instance, child: Instance) => void;
25
+ appendChild: (parent: Instance, child: Instance) => void;
26
+ appendChildToContainer: (_container: unknown, child: Instance) => void;
27
+ insertBefore: (parent: Instance, child: Instance, before: Instance) => void;
28
+ insertInContainerBefore: (_container: unknown, child: Instance, before: Instance) => void;
29
+ removeChild: (parent: Instance, child: Instance) => void;
30
+ removeChildFromContainer: (_container: unknown, child: Instance) => void;
31
+ finalizeInitialChildren: () => boolean;
32
+ prepareUpdate: () => boolean;
33
+ commitUpdate(instance: Instance, _updatePayload: unknown, _type: string, _prevProps: Record<string, unknown>, nextProps: Record<string, unknown>): void;
34
+ commitTextUpdate(instance: TextInstance, _oldText: string, newText: string): void;
35
+ clearContainer: () => void;
36
+ detachDeletedInstance(instance: Instance): void;
37
+ getCurrentEventPriority: () => number;
38
+ getInstanceFromNode: () => null;
39
+ getInstanceFromScope: () => null;
40
+ beforeActiveInstanceBlur: () => void;
41
+ afterActiveInstanceBlur: () => void;
42
+ prepareScopeUpdate: () => void;
43
+ preparePortalMount: () => void;
44
+ };
@@ -0,0 +1,118 @@
1
+ // react-reconciler host config (mutation mode). React commits become batched
2
+ // mutation ops; event props become handler-registry entries + listener lists.
3
+ import { DefaultEventPriority } from "react-reconciler/constants";
4
+ import { queueOp, flushOps, setHandlers, removeHandlers } from "./bridge";
5
+ import { tw } from "./tw";
6
+ let nextId = 1;
7
+ const hostTypes = {
8
+ "vs-view": "view",
9
+ "vs-text": "text",
10
+ "vs-image": "image",
11
+ "vs-textinput": "textinput",
12
+ "vs-native": "native",
13
+ };
14
+ const eventPropNames = {
15
+ onClick: "click",
16
+ onMouseEnter: "mouseenter",
17
+ onMouseLeave: "mouseleave",
18
+ onMouseDown: "mousedown",
19
+ onMouseUp: "mouseup",
20
+ onDragStart: "dragstart",
21
+ onDrag: "drag",
22
+ onDragEnd: "dragend",
23
+ onChange: "change",
24
+ onSubmit: "submit",
25
+ onFocus: "focus",
26
+ onBlur: "blur",
27
+ };
28
+ function normalizeProps(props) {
29
+ const payload = {};
30
+ const handlers = new Map();
31
+ const listeners = [];
32
+ const resolved = typeof props.className === "string" ? tw(props.className) : { style: {} };
33
+ payload.style = { ...resolved.style, ...(props.style ?? {}) };
34
+ if (resolved.hoverStyle)
35
+ payload.hoverStyle = resolved.hoverStyle;
36
+ if (resolved.activeStyle)
37
+ payload.activeStyle = resolved.activeStyle;
38
+ if (resolved.focusStyle)
39
+ payload.focusStyle = resolved.focusStyle;
40
+ for (const [key, value] of Object.entries(props)) {
41
+ if (value === undefined || key === "children" || key === "className" || key === "style")
42
+ continue;
43
+ const eventType = eventPropNames[key];
44
+ if (eventType && typeof value === "function") {
45
+ handlers.set(eventType, value);
46
+ listeners.push(eventType);
47
+ continue;
48
+ }
49
+ if (typeof value !== "function" && typeof value !== "object")
50
+ payload[key] = value;
51
+ }
52
+ if (listeners.length > 0)
53
+ payload.listeners = listeners;
54
+ return { payload, handlers };
55
+ }
56
+ function applyProps(instance, props) {
57
+ const { payload, handlers } = normalizeProps(props);
58
+ setHandlers(instance.id, handlers);
59
+ queueOp(["setProps", instance.id, payload]);
60
+ }
61
+ export const hostConfig = {
62
+ supportsMutation: true,
63
+ supportsPersistence: false,
64
+ supportsHydration: false,
65
+ isPrimaryRenderer: true,
66
+ supportsMicrotasks: true,
67
+ scheduleMicrotask: (fn) => queueMicrotask(fn),
68
+ scheduleTimeout: (fn, delay) => setTimeout(fn, delay),
69
+ cancelTimeout: (id) => clearTimeout(id),
70
+ noTimeout: -1,
71
+ getRootHostContext: () => ({}),
72
+ getChildHostContext: (parent) => parent,
73
+ getPublicInstance: (instance) => instance,
74
+ prepareForCommit: () => null,
75
+ resetAfterCommit: () => flushOps(),
76
+ shouldSetTextContent: () => false,
77
+ createInstance(type, props) {
78
+ const hostType = hostTypes[type];
79
+ if (!hostType)
80
+ throw new Error(`Unknown VSReacT element <${type}>`);
81
+ const instance = { id: nextId++, type: hostType };
82
+ queueOp(["create", instance.id, hostType]);
83
+ applyProps(instance, props);
84
+ return instance;
85
+ },
86
+ createTextInstance(text) {
87
+ const instance = { id: nextId++, type: "rawtext" };
88
+ queueOp(["create", instance.id, "rawtext"]);
89
+ queueOp(["setText", instance.id, text]);
90
+ return instance;
91
+ },
92
+ appendInitialChild: (parent, child) => queueOp(["appendChild", parent.id, child.id]),
93
+ appendChild: (parent, child) => queueOp(["appendChild", parent.id, child.id]),
94
+ appendChildToContainer: (_container, child) => queueOp(["appendChild", 0, child.id]),
95
+ insertBefore: (parent, child, before) => queueOp(["insertBefore", parent.id, child.id, before.id]),
96
+ insertInContainerBefore: (_container, child, before) => queueOp(["insertBefore", 0, child.id, before.id]),
97
+ removeChild: (parent, child) => queueOp(["removeChild", parent.id, child.id]),
98
+ removeChildFromContainer: (_container, child) => queueOp(["removeChild", 0, child.id]),
99
+ finalizeInitialChildren: () => false,
100
+ prepareUpdate: () => true,
101
+ commitUpdate(instance, _updatePayload, _type, _prevProps, nextProps) {
102
+ applyProps(instance, nextProps);
103
+ },
104
+ commitTextUpdate(instance, _oldText, newText) {
105
+ queueOp(["setText", instance.id, newText]);
106
+ },
107
+ clearContainer: () => queueOp(["clearContainer"]),
108
+ detachDeletedInstance(instance) {
109
+ removeHandlers(instance.id);
110
+ },
111
+ getCurrentEventPriority: () => DefaultEventPriority,
112
+ getInstanceFromNode: () => null,
113
+ getInstanceFromScope: () => null,
114
+ beforeActiveInstanceBlur: () => { },
115
+ afterActiveInstanceBlur: () => { },
116
+ prepareScopeUpdate: () => { },
117
+ preparePortalMount: () => { },
118
+ };