@vsreact/core 0.0.1 → 0.0.2
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/animation.d.ts +19 -0
- package/dist/animation.js +47 -0
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.js +49 -0
- package/dist/controls.d.ts +43 -0
- package/dist/controls.js +52 -0
- package/dist/hostConfig.d.ts +44 -0
- package/dist/hostConfig.js +118 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +11 -0
- package/dist/native.d.ts +6 -0
- package/dist/native.js +16 -0
- package/dist/parameters.d.ts +12 -0
- package/dist/parameters.js +33 -0
- package/dist/primitives.d.ts +70 -0
- package/dist/primitives.js +20 -0
- package/dist/render.d.ts +5 -0
- package/dist/render.js +22 -0
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +68 -0
- package/dist/theme.d.ts +5 -0
- package/dist/theme.js +81 -0
- package/dist/tw.d.ts +13 -0
- package/dist/tw.js +260 -0
- package/package.json +31 -4
- package/src/runtime.ts +4 -1
|
@@ -0,0 +1,19 @@
|
|
|
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;
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
}
|
package/dist/bridge.d.ts
ADDED
|
@@ -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,43 @@
|
|
|
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
|
+
width?: number;
|
|
28
|
+
label?: string;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
trackColor?: string;
|
|
31
|
+
valueColor?: string;
|
|
32
|
+
onChange: (value: number) => void;
|
|
33
|
+
onBegin?: () => void;
|
|
34
|
+
onEnd?: () => void;
|
|
35
|
+
}
|
|
36
|
+
export declare function Slider({ value, width, label, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: SliderProps): import("react").JSX.Element;
|
|
37
|
+
export interface ParamSliderProps {
|
|
38
|
+
paramId: string;
|
|
39
|
+
label?: string;
|
|
40
|
+
width?: number;
|
|
41
|
+
}
|
|
42
|
+
/** A Slider bound to an APVTS parameter (via ParameterBridge). */
|
|
43
|
+
export declare function ParamSlider({ paramId, label, width }: ParamSliderProps): import("react").JSX.Element;
|
package/dist/controls.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Knob and Slider — the classic VST controls, drawn by the VSReacT painter
|
|
3
|
+
// (arc styles) and driven by drag gestures.
|
|
4
|
+
import { useRef } from "react";
|
|
5
|
+
import { View, Text } from "./primitives";
|
|
6
|
+
import { useParameter } from "./parameters";
|
|
7
|
+
/** Vertical-drag-to-value mapping shared by Knob (and tested in isolation). */
|
|
8
|
+
export function dragToValue(startValue, dy, sensitivity = 0.005) {
|
|
9
|
+
return Math.min(1, Math.max(0, startValue - dy * sensitivity));
|
|
10
|
+
}
|
|
11
|
+
const ARC_START = -135;
|
|
12
|
+
const ARC_END = 135;
|
|
13
|
+
export function Knob({ value, text, label, size = 64, disabled, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
|
|
14
|
+
const startValue = useRef(0);
|
|
15
|
+
return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `items-center justify-center ${disabled ? "opacity-40" : "cursor-pointer"}`, style: {
|
|
16
|
+
width: size,
|
|
17
|
+
height: size,
|
|
18
|
+
arcTrackColor: trackColor,
|
|
19
|
+
arcColor: valueColor,
|
|
20
|
+
arcStart: ARC_START,
|
|
21
|
+
arcEnd: ARC_END,
|
|
22
|
+
arcValueEnd: ARC_START + (ARC_END - ARC_START) * Math.min(1, Math.max(0, value)),
|
|
23
|
+
arcThickness: Math.max(3, size * 0.08),
|
|
24
|
+
}, onDragStart: disabled
|
|
25
|
+
? undefined
|
|
26
|
+
: () => {
|
|
27
|
+
startValue.current = value;
|
|
28
|
+
onBegin?.();
|
|
29
|
+
}, 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] }));
|
|
30
|
+
}
|
|
31
|
+
/** A Knob bound to an APVTS parameter (via ParameterBridge). */
|
|
32
|
+
export function ParamKnob({ paramId, label, size, trackColor, valueColor }) {
|
|
33
|
+
const param = useParameter(paramId);
|
|
34
|
+
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 }));
|
|
35
|
+
}
|
|
36
|
+
export function Slider({ value, width = 160, label, disabled, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
|
|
37
|
+
const startValue = useRef(0);
|
|
38
|
+
const clamped = Math.min(1, Math.max(0, value));
|
|
39
|
+
return (_jsxs(View, { className: "gap-2", children: [_jsxs(View, { className: `h-[18] justify-center relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width }, onDragStart: disabled
|
|
40
|
+
? undefined
|
|
41
|
+
: () => {
|
|
42
|
+
startValue.current = clamped;
|
|
43
|
+
onBegin?.();
|
|
44
|
+
}, onDrag: disabled
|
|
45
|
+
? undefined
|
|
46
|
+
: (e) => onChange(Math.min(1, Math.max(0, startValue.current + e.dx / width))), onDragEnd: disabled ? undefined : () => onEnd?.(), 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] }));
|
|
47
|
+
}
|
|
48
|
+
/** A Slider bound to an APVTS parameter (via ParameterBridge). */
|
|
49
|
+
export function ParamSlider({ paramId, label, width }) {
|
|
50
|
+
const param = useParameter(paramId);
|
|
51
|
+
return (_jsx(Slider, { value: param.value, label: label ?? param.name.toUpperCase(), width: width, onChange: param.set, onBegin: param.begin, onEnd: param.end }));
|
|
52
|
+
}
|
|
@@ -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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "./runtime";
|
|
2
|
+
import "./bridge";
|
|
3
|
+
export { View, Text, Image, TextInput, NativeView } from "./primitives";
|
|
4
|
+
export type { CommonProps, TextProps, ImageProps, TextInputProps, NativeViewProps, } from "./primitives";
|
|
5
|
+
export { render, unmount } from "./render";
|
|
6
|
+
export { native } from "./native";
|
|
7
|
+
export { configureTheme, tw } from "./tw";
|
|
8
|
+
export type { Style, ResolvedClasses } from "./tw";
|
|
9
|
+
export { useTween, lerp, Easing } from "./animation";
|
|
10
|
+
export type { TweenOptions, EasingFn } from "./animation";
|
|
11
|
+
export { useParameter } from "./parameters";
|
|
12
|
+
export type { ParameterState, ParameterHandle } from "./parameters";
|
|
13
|
+
export { Knob, Slider, ParamKnob, ParamSlider, dragToValue } from "./controls";
|
|
14
|
+
export type { KnobProps, SliderProps, ParamKnobProps, ParamSliderProps } from "./controls";
|
|
15
|
+
export type { DragEventPayload } from "./primitives";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// vsreact public API. runtime must load first — it installs the
|
|
2
|
+
// timer/console/microtask shims react depends on inside QuickJS.
|
|
3
|
+
import "./runtime";
|
|
4
|
+
import "./bridge";
|
|
5
|
+
export { View, Text, Image, TextInput, NativeView } from "./primitives";
|
|
6
|
+
export { render, unmount } from "./render";
|
|
7
|
+
export { native } from "./native";
|
|
8
|
+
export { configureTheme, tw } from "./tw";
|
|
9
|
+
export { useTween, lerp, Easing } from "./animation";
|
|
10
|
+
export { useParameter } from "./parameters";
|
|
11
|
+
export { Knob, Slider, ParamKnob, ParamSlider, dragToValue } from "./controls";
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const native: {
|
|
2
|
+
/** Synchronously invokes a C++ handler registered in RootOptions.onNativeCall. */
|
|
3
|
+
call(name: string, args?: unknown): any;
|
|
4
|
+
/** Subscribes to events pushed from C++ via RootView::sendNativeEvent. */
|
|
5
|
+
on(name: string, cb: (payload: any) => void): () => void;
|
|
6
|
+
};
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// App-level messaging with the C++ host.
|
|
2
|
+
import { addNativeListener } from "./bridge";
|
|
3
|
+
export const native = {
|
|
4
|
+
/** Synchronously invokes a C++ handler registered in RootOptions.onNativeCall. */
|
|
5
|
+
call(name, args) {
|
|
6
|
+
const fn = globalThis.__vsreact_nativeCall;
|
|
7
|
+
if (typeof fn !== "function")
|
|
8
|
+
return undefined;
|
|
9
|
+
const json = fn(name, JSON.stringify(args ?? null));
|
|
10
|
+
return json ? JSON.parse(json) : undefined;
|
|
11
|
+
},
|
|
12
|
+
/** Subscribes to events pushed from C++ via RootView::sendNativeEvent. */
|
|
13
|
+
on(name, cb) {
|
|
14
|
+
return addNativeListener(name, cb);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ParameterState {
|
|
2
|
+
value: number;
|
|
3
|
+
text: string;
|
|
4
|
+
name: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ParameterHandle extends ParameterState {
|
|
8
|
+
set: (normalized: number) => void;
|
|
9
|
+
begin: () => void;
|
|
10
|
+
end: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function useParameter(id: string): ParameterHandle;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// useParameter — two-way binding to a juce::AudioProcessorValueTreeState
|
|
2
|
+
// parameter through vsreact::ParameterBridge. Values are normalized 0..1.
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { native } from "./native";
|
|
5
|
+
export function useParameter(id) {
|
|
6
|
+
const [state, setState] = useState(() => {
|
|
7
|
+
const initial = native.call("param:get", { id });
|
|
8
|
+
return initial && typeof initial.value === "number"
|
|
9
|
+
? {
|
|
10
|
+
value: initial.value,
|
|
11
|
+
text: String(initial.text ?? ""),
|
|
12
|
+
name: String(initial.name ?? id),
|
|
13
|
+
label: String(initial.label ?? ""),
|
|
14
|
+
}
|
|
15
|
+
: { value: 0, text: "", name: id, label: "" };
|
|
16
|
+
});
|
|
17
|
+
useEffect(() => native.on("param", (p) => {
|
|
18
|
+
if (p?.id === id)
|
|
19
|
+
setState((s) => ({ ...s, value: Number(p.value), text: String(p.text ?? "") }));
|
|
20
|
+
}), [id]);
|
|
21
|
+
const set = useCallback((normalized) => {
|
|
22
|
+
const value = Math.min(1, Math.max(0, normalized));
|
|
23
|
+
setState((s) => ({ ...s, value }));
|
|
24
|
+
native.call("param:set", { id, value });
|
|
25
|
+
}, [id]);
|
|
26
|
+
const begin = useCallback(() => {
|
|
27
|
+
native.call("param:begin", { id });
|
|
28
|
+
}, [id]);
|
|
29
|
+
const end = useCallback(() => {
|
|
30
|
+
native.call("param:end", { id });
|
|
31
|
+
}, [id]);
|
|
32
|
+
return { ...state, set, begin, end };
|
|
33
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Style } from "./tw";
|
|
3
|
+
export interface DragEventPayload {
|
|
4
|
+
/** Delta from the drag start, in root coordinates. */
|
|
5
|
+
dx: number;
|
|
6
|
+
dy: number;
|
|
7
|
+
/** Current pointer position in root coordinates. */
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
}
|
|
11
|
+
export interface CommonProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: Style;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
/** Resets the scroll offset of an overflow-y-scroll container. */
|
|
16
|
+
scrollTop?: number;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
onMouseEnter?: () => void;
|
|
19
|
+
onMouseLeave?: () => void;
|
|
20
|
+
onMouseDown?: () => void;
|
|
21
|
+
onMouseUp?: () => void;
|
|
22
|
+
onDragStart?: (e: DragEventPayload) => void;
|
|
23
|
+
onDrag?: (e: DragEventPayload) => void;
|
|
24
|
+
onDragEnd?: (e: DragEventPayload) => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function View(props: CommonProps): import("react").ReactElement<CommonProps, string | import("react").JSXElementConstructor<any>>;
|
|
27
|
+
export interface TextProps extends Omit<CommonProps, "children"> {
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
export declare function Text(props: TextProps): import("react").ReactElement<TextProps, string | import("react").JSXElementConstructor<any>>;
|
|
31
|
+
export interface ImageProps extends CommonProps {
|
|
32
|
+
src: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function Image(props: ImageProps): import("react").ReactElement<ImageProps, string | import("react").JSXElementConstructor<any>>;
|
|
35
|
+
export interface TextInputProps extends Omit<CommonProps, "children" | "onChange" | "onSubmit"> {
|
|
36
|
+
value?: string;
|
|
37
|
+
defaultValue?: string;
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
onChange?: (value: string) => void;
|
|
41
|
+
onSubmit?: (value: string) => void;
|
|
42
|
+
onFocus?: () => void;
|
|
43
|
+
onBlur?: () => void;
|
|
44
|
+
}
|
|
45
|
+
export declare function TextInput({ onChange, onSubmit, ...rest }: TextInputProps): import("react").ReactElement<{
|
|
46
|
+
onChange: ((payload: any) => void) | undefined;
|
|
47
|
+
onSubmit: ((payload: any) => void) | undefined;
|
|
48
|
+
value?: string;
|
|
49
|
+
defaultValue?: string;
|
|
50
|
+
placeholder?: string;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
onFocus?: () => void;
|
|
53
|
+
onBlur?: () => void;
|
|
54
|
+
style?: Style | undefined;
|
|
55
|
+
className?: string | undefined;
|
|
56
|
+
scrollTop?: number | undefined;
|
|
57
|
+
onClick?: (() => void) | undefined;
|
|
58
|
+
onMouseEnter?: (() => void) | undefined;
|
|
59
|
+
onMouseLeave?: (() => void) | undefined;
|
|
60
|
+
onMouseDown?: (() => void) | undefined;
|
|
61
|
+
onMouseUp?: (() => void) | undefined;
|
|
62
|
+
onDragStart?: ((e: DragEventPayload) => void) | undefined;
|
|
63
|
+
onDrag?: ((e: DragEventPayload) => void) | undefined;
|
|
64
|
+
onDragEnd?: ((e: DragEventPayload) => void) | undefined;
|
|
65
|
+
}, string | import("react").JSXElementConstructor<any>>;
|
|
66
|
+
export interface NativeViewProps extends CommonProps {
|
|
67
|
+
/** Id of a component factory registered in the C++ NativeRegistry. */
|
|
68
|
+
nativeId: string;
|
|
69
|
+
}
|
|
70
|
+
export declare function NativeView(props: NativeViewProps): import("react").ReactElement<NativeViewProps, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
export function View(props) {
|
|
3
|
+
return createElement("vs-view", props);
|
|
4
|
+
}
|
|
5
|
+
export function Text(props) {
|
|
6
|
+
return createElement("vs-text", props);
|
|
7
|
+
}
|
|
8
|
+
export function Image(props) {
|
|
9
|
+
return createElement("vs-image", props);
|
|
10
|
+
}
|
|
11
|
+
export function TextInput({ onChange, onSubmit, ...rest }) {
|
|
12
|
+
return createElement("vs-textinput", {
|
|
13
|
+
...rest,
|
|
14
|
+
onChange: onChange ? (payload) => onChange(String(payload?.value ?? "")) : undefined,
|
|
15
|
+
onSubmit: onSubmit ? (payload) => onSubmit(String(payload?.value ?? "")) : undefined,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function NativeView(props) {
|
|
19
|
+
return createElement("vs-native", props);
|
|
20
|
+
}
|
package/dist/render.d.ts
ADDED
package/dist/render.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Reconciler from "react-reconciler";
|
|
2
|
+
import { LegacyRoot } from "react-reconciler/constants";
|
|
3
|
+
import { hostConfig } from "./hostConfig";
|
|
4
|
+
import { flushOps } from "./bridge";
|
|
5
|
+
const reconciler = Reconciler(hostConfig);
|
|
6
|
+
let container = null;
|
|
7
|
+
/** Mounts (or re-renders) the app into the plugin window. */
|
|
8
|
+
export function render(element) {
|
|
9
|
+
if (!container) {
|
|
10
|
+
container = reconciler.createContainer({}, LegacyRoot, null, false, null, "vsreact", (error) => console.error("[vsreact] recoverable error:", error), null);
|
|
11
|
+
}
|
|
12
|
+
reconciler.updateContainer(element, container, null, null);
|
|
13
|
+
flushOps();
|
|
14
|
+
}
|
|
15
|
+
/** Unmounts the current app (used by hot reload teardown). */
|
|
16
|
+
export function unmount() {
|
|
17
|
+
if (container) {
|
|
18
|
+
reconciler.updateContainer(null, container, null, null);
|
|
19
|
+
flushOps();
|
|
20
|
+
container = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type NativeFns = {
|
|
2
|
+
__vsreact_flush(json: string): void;
|
|
3
|
+
__vsreact_nativeCall(name: string, argsJson: string): string;
|
|
4
|
+
__vsreact_log(level: string, msg: string): void;
|
|
5
|
+
__vsreact_setTimer(id: number, ms: number): void;
|
|
6
|
+
__vsreact_clearTimer(id: number): void;
|
|
7
|
+
};
|
|
8
|
+
export declare const isHosted: boolean;
|
|
9
|
+
/** Called by bridge.ts when C++ dispatches {kind:"timer",id}. */
|
|
10
|
+
export declare function fireTimer(id: number): void;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Environment shims for QuickJS. This module MUST be imported before react —
|
|
2
|
+
// it provides the timer/console/microtask facilities react and the scheduler
|
|
3
|
+
// expect. The __vsreact_* natives are registered by the C++ host; when absent
|
|
4
|
+
// (e.g. under `bun test`) the shims fall back to the platform implementations.
|
|
5
|
+
const g = globalThis;
|
|
6
|
+
export const isHosted = typeof g.__vsreact_setTimer === "function";
|
|
7
|
+
let nextTimerId = 1;
|
|
8
|
+
const timers = new Map();
|
|
9
|
+
/** Called by bridge.ts when C++ dispatches {kind:"timer",id}. */
|
|
10
|
+
export function fireTimer(id) {
|
|
11
|
+
const entry = timers.get(id);
|
|
12
|
+
if (!entry)
|
|
13
|
+
return;
|
|
14
|
+
if (entry.intervalMs !== undefined)
|
|
15
|
+
g.__vsreact_setTimer(id, entry.intervalMs);
|
|
16
|
+
else
|
|
17
|
+
timers.delete(id);
|
|
18
|
+
entry.cb(...entry.args);
|
|
19
|
+
}
|
|
20
|
+
function stringify(value) {
|
|
21
|
+
if (typeof value === "string")
|
|
22
|
+
return value;
|
|
23
|
+
if (value instanceof Error)
|
|
24
|
+
return `${value.message}\n${value.stack ?? ""}`;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(value) ?? String(value);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (isHosted) {
|
|
33
|
+
g.setTimeout = ((cb, ms = 0, ...args) => {
|
|
34
|
+
const id = nextTimerId++;
|
|
35
|
+
timers.set(id, { cb, args });
|
|
36
|
+
g.__vsreact_setTimer(id, Math.max(0, ms | 0));
|
|
37
|
+
return id;
|
|
38
|
+
});
|
|
39
|
+
g.setInterval = ((cb, ms = 0, ...args) => {
|
|
40
|
+
const id = nextTimerId++;
|
|
41
|
+
const intervalMs = Math.max(1, ms | 0);
|
|
42
|
+
timers.set(id, { cb, args, intervalMs });
|
|
43
|
+
g.__vsreact_setTimer(id, intervalMs);
|
|
44
|
+
return id;
|
|
45
|
+
});
|
|
46
|
+
g.clearTimeout = g.clearInterval = ((id) => {
|
|
47
|
+
if (id !== undefined && timers.delete(id))
|
|
48
|
+
g.__vsreact_clearTimer(id);
|
|
49
|
+
});
|
|
50
|
+
const makeLog = (level) => (...args) => g.__vsreact_log(level, args.map(stringify).join(" "));
|
|
51
|
+
g.console = {
|
|
52
|
+
log: makeLog("log"),
|
|
53
|
+
info: makeLog("info"),
|
|
54
|
+
debug: makeLog("debug"),
|
|
55
|
+
warn: makeLog("warn"),
|
|
56
|
+
error: makeLog("error"),
|
|
57
|
+
};
|
|
58
|
+
g.queueMicrotask ??= (cb) => {
|
|
59
|
+
Promise.resolve().then(cb);
|
|
60
|
+
};
|
|
61
|
+
g.performance ??= { now: () => Date.now() };
|
|
62
|
+
// Typed loosely on purpose: the dist build has no Node/Bun types, and the
|
|
63
|
+
// shim only exists for libraries that probe NODE_ENV.
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
g.process ??= { env: { NODE_ENV: "production" } };
|
|
66
|
+
g.self ??= g;
|
|
67
|
+
g.global ??= g;
|
|
68
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type ThemeColors = Record<string, string>;
|
|
2
|
+
export declare function setThemeColors(colors: ThemeColors): void;
|
|
3
|
+
/** Resolves a color name ("zinc-900", "white", "accent", "lime-400/20",
|
|
4
|
+
"[#c6f135]") to "#rrggbb"/"#rrggbbaa", or undefined if unknown. */
|
|
5
|
+
export declare function resolveColor(name: string): string | undefined;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Tailwind v3 palettes (the curated subset VSReacT ships with).
|
|
2
|
+
const palettes = {
|
|
3
|
+
zinc: {
|
|
4
|
+
"50": "#fafafa", "100": "#f4f4f5", "200": "#e4e4e7", "300": "#d4d4d8",
|
|
5
|
+
"400": "#a1a1aa", "500": "#71717a", "600": "#52525b", "700": "#3f3f46",
|
|
6
|
+
"800": "#27272a", "900": "#18181b", "950": "#09090b",
|
|
7
|
+
},
|
|
8
|
+
neutral: {
|
|
9
|
+
"50": "#fafafa", "100": "#f5f5f5", "200": "#e5e5e5", "300": "#d4d4d4",
|
|
10
|
+
"400": "#a3a3a3", "500": "#737373", "600": "#525252", "700": "#404040",
|
|
11
|
+
"800": "#262626", "900": "#171717", "950": "#0a0a0a",
|
|
12
|
+
},
|
|
13
|
+
lime: {
|
|
14
|
+
"50": "#f7fee7", "100": "#ecfccb", "200": "#d9f99d", "300": "#bef264",
|
|
15
|
+
"400": "#a3e635", "500": "#84cc16", "600": "#65a30d", "700": "#4d7c0f",
|
|
16
|
+
"800": "#3f6212", "900": "#365314", "950": "#1a2e05",
|
|
17
|
+
},
|
|
18
|
+
red: {
|
|
19
|
+
"50": "#fef2f2", "100": "#fee2e2", "200": "#fecaca", "300": "#fca5a5",
|
|
20
|
+
"400": "#f87171", "500": "#ef4444", "600": "#dc2626", "700": "#b91c1c",
|
|
21
|
+
"800": "#991b1b", "900": "#7f1d1d", "950": "#450a0a",
|
|
22
|
+
},
|
|
23
|
+
amber: {
|
|
24
|
+
"50": "#fffbeb", "100": "#fef3c7", "200": "#fde68a", "300": "#fcd34d",
|
|
25
|
+
"400": "#fbbf24", "500": "#f59e0b", "600": "#d97706", "700": "#b45309",
|
|
26
|
+
"800": "#92400e", "900": "#78350f", "950": "#451a03",
|
|
27
|
+
},
|
|
28
|
+
emerald: {
|
|
29
|
+
"50": "#ecfdf5", "100": "#d1fae5", "200": "#a7f3d0", "300": "#6ee7b7",
|
|
30
|
+
"400": "#34d399", "500": "#10b981", "600": "#059669", "700": "#047857",
|
|
31
|
+
"800": "#065f46", "900": "#064e3b", "950": "#022c22",
|
|
32
|
+
},
|
|
33
|
+
sky: {
|
|
34
|
+
"50": "#f0f9ff", "100": "#e0f2fe", "200": "#bae6fd", "300": "#7dd3fc",
|
|
35
|
+
"400": "#38bdf8", "500": "#0ea5e9", "600": "#0284c7", "700": "#0369a1",
|
|
36
|
+
"800": "#075985", "900": "#0c4a6e", "950": "#082f49",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
let themeColors = {};
|
|
40
|
+
export function setThemeColors(colors) {
|
|
41
|
+
themeColors = { ...themeColors };
|
|
42
|
+
for (const [name, value] of Object.entries(colors))
|
|
43
|
+
themeColors[name] = value.toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
/** Resolves a color name ("zinc-900", "white", "accent", "lime-400/20",
|
|
46
|
+
"[#c6f135]") to "#rrggbb"/"#rrggbbaa", or undefined if unknown. */
|
|
47
|
+
export function resolveColor(name) {
|
|
48
|
+
let base = name;
|
|
49
|
+
let alpha;
|
|
50
|
+
const slash = name.lastIndexOf("/");
|
|
51
|
+
if (slash > 0) {
|
|
52
|
+
const pct = Number(name.slice(slash + 1));
|
|
53
|
+
if (Number.isFinite(pct)) {
|
|
54
|
+
base = name.slice(0, slash);
|
|
55
|
+
alpha = Math.round((pct / 100) * 255)
|
|
56
|
+
.toString(16)
|
|
57
|
+
.padStart(2, "0");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let hex;
|
|
61
|
+
if (base.startsWith("[#") && base.endsWith("]"))
|
|
62
|
+
hex = base.slice(1, -1).toLowerCase();
|
|
63
|
+
else if (base === "white")
|
|
64
|
+
hex = "#ffffff";
|
|
65
|
+
else if (base === "black")
|
|
66
|
+
hex = "#000000";
|
|
67
|
+
else if (base === "transparent")
|
|
68
|
+
hex = "#00000000";
|
|
69
|
+
else if (themeColors[base])
|
|
70
|
+
hex = themeColors[base];
|
|
71
|
+
else {
|
|
72
|
+
const dash = base.lastIndexOf("-");
|
|
73
|
+
if (dash > 0) {
|
|
74
|
+
const palette = palettes[base.slice(0, dash)];
|
|
75
|
+
hex = palette?.[base.slice(dash + 1)];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (hex === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
return alpha !== undefined && hex.length === 7 ? hex + alpha : hex;
|
|
81
|
+
}
|
package/dist/tw.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ThemeColors } from "./theme";
|
|
2
|
+
export type StyleValue = string | number;
|
|
3
|
+
export type Style = Record<string, StyleValue>;
|
|
4
|
+
export interface ResolvedClasses {
|
|
5
|
+
style: Style;
|
|
6
|
+
hoverStyle?: Style;
|
|
7
|
+
activeStyle?: Style;
|
|
8
|
+
focusStyle?: Style;
|
|
9
|
+
}
|
|
10
|
+
export declare function configureTheme(theme: {
|
|
11
|
+
colors?: ThemeColors;
|
|
12
|
+
}): void;
|
|
13
|
+
export declare function tw(classes: string): ResolvedClasses;
|
package/dist/tw.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { resolveColor, setThemeColors } from "./theme";
|
|
2
|
+
export function configureTheme(theme) {
|
|
3
|
+
if (theme.colors)
|
|
4
|
+
setThemeColors(theme.colors);
|
|
5
|
+
cache.clear();
|
|
6
|
+
}
|
|
7
|
+
const staticClasses = {
|
|
8
|
+
flex: {},
|
|
9
|
+
"flex-row": { flexDirection: "row" },
|
|
10
|
+
"flex-col": { flexDirection: "column" },
|
|
11
|
+
"flex-row-reverse": { flexDirection: "row-reverse" },
|
|
12
|
+
"flex-col-reverse": { flexDirection: "column-reverse" },
|
|
13
|
+
"flex-1": { flex: 1 },
|
|
14
|
+
"flex-auto": { flexGrow: 1, flexShrink: 1 },
|
|
15
|
+
"flex-none": { flexGrow: 0, flexShrink: 0 },
|
|
16
|
+
grow: { flexGrow: 1 },
|
|
17
|
+
"grow-0": { flexGrow: 0 },
|
|
18
|
+
shrink: { flexShrink: 1 },
|
|
19
|
+
"shrink-0": { flexShrink: 0 },
|
|
20
|
+
"flex-wrap": { flexWrap: "wrap" },
|
|
21
|
+
"flex-nowrap": { flexWrap: "nowrap" },
|
|
22
|
+
"items-start": { alignItems: "flex-start" },
|
|
23
|
+
"items-center": { alignItems: "center" },
|
|
24
|
+
"items-end": { alignItems: "flex-end" },
|
|
25
|
+
"items-stretch": { alignItems: "stretch" },
|
|
26
|
+
"items-baseline": { alignItems: "baseline" },
|
|
27
|
+
"justify-start": { justifyContent: "flex-start" },
|
|
28
|
+
"justify-center": { justifyContent: "center" },
|
|
29
|
+
"justify-end": { justifyContent: "flex-end" },
|
|
30
|
+
"justify-between": { justifyContent: "space-between" },
|
|
31
|
+
"justify-around": { justifyContent: "space-around" },
|
|
32
|
+
"justify-evenly": { justifyContent: "space-evenly" },
|
|
33
|
+
"self-start": { alignSelf: "flex-start" },
|
|
34
|
+
"self-center": { alignSelf: "center" },
|
|
35
|
+
"self-end": { alignSelf: "flex-end" },
|
|
36
|
+
"self-stretch": { alignSelf: "stretch" },
|
|
37
|
+
"self-auto": { alignSelf: "auto" },
|
|
38
|
+
absolute: { position: "absolute" },
|
|
39
|
+
relative: { position: "relative" },
|
|
40
|
+
"overflow-hidden": { overflow: "hidden" },
|
|
41
|
+
"overflow-visible": { overflow: "visible" },
|
|
42
|
+
"overflow-y-scroll": { overflow: "scroll" },
|
|
43
|
+
"overflow-scroll": { overflow: "scroll" },
|
|
44
|
+
"w-full": { width: "100%" },
|
|
45
|
+
"h-full": { height: "100%" },
|
|
46
|
+
"text-left": { textAlign: "left" },
|
|
47
|
+
"text-center": { textAlign: "center" },
|
|
48
|
+
"text-right": { textAlign: "right" },
|
|
49
|
+
"font-mono": { fontFamily: "monospace" },
|
|
50
|
+
"font-normal": { fontWeight: 400 },
|
|
51
|
+
"font-medium": { fontWeight: 500 },
|
|
52
|
+
"font-semibold": { fontWeight: 600 },
|
|
53
|
+
"font-bold": { fontWeight: 700 },
|
|
54
|
+
"tracking-tighter": { letterSpacing: -0.8 },
|
|
55
|
+
"tracking-tight": { letterSpacing: -0.4 },
|
|
56
|
+
"tracking-normal": { letterSpacing: 0 },
|
|
57
|
+
"tracking-wide": { letterSpacing: 0.4 },
|
|
58
|
+
"tracking-wider": { letterSpacing: 0.8 },
|
|
59
|
+
"tracking-widest": { letterSpacing: 1.6 },
|
|
60
|
+
border: { borderWidth: 1 },
|
|
61
|
+
"aspect-square": { aspectRatio: 1 },
|
|
62
|
+
"aspect-video": { aspectRatio: 16 / 9 },
|
|
63
|
+
"cursor-pointer": { cursor: "pointer" },
|
|
64
|
+
"cursor-text": { cursor: "text" },
|
|
65
|
+
"cursor-default": { cursor: "default" },
|
|
66
|
+
shadow: { shadowColor: "#00000066", shadowRadius: 4, shadowOffsetY: 1 },
|
|
67
|
+
"shadow-sm": { shadowColor: "#00000066", shadowRadius: 4, shadowOffsetY: 1 },
|
|
68
|
+
"shadow-md": { shadowColor: "#00000066", shadowRadius: 8, shadowOffsetY: 2 },
|
|
69
|
+
"shadow-lg": { shadowColor: "#00000066", shadowRadius: 12, shadowOffsetY: 4 },
|
|
70
|
+
"shadow-xl": { shadowColor: "#00000066", shadowRadius: 20, shadowOffsetY: 8 },
|
|
71
|
+
};
|
|
72
|
+
const textSizes = {
|
|
73
|
+
xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30, "4xl": 36,
|
|
74
|
+
};
|
|
75
|
+
const radiusSizes = {
|
|
76
|
+
"": 4, sm: 2, md: 6, lg: 8, xl: 12, "2xl": 16, "3xl": 24, full: 9999,
|
|
77
|
+
};
|
|
78
|
+
const radiusCorners = {
|
|
79
|
+
"": ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"],
|
|
80
|
+
t: ["borderTopLeftRadius", "borderTopRightRadius"],
|
|
81
|
+
b: ["borderBottomLeftRadius", "borderBottomRightRadius"],
|
|
82
|
+
l: ["borderTopLeftRadius", "borderBottomLeftRadius"],
|
|
83
|
+
r: ["borderTopRightRadius", "borderBottomRightRadius"],
|
|
84
|
+
tl: ["borderTopLeftRadius"],
|
|
85
|
+
tr: ["borderTopRightRadius"],
|
|
86
|
+
bl: ["borderBottomLeftRadius"],
|
|
87
|
+
br: ["borderBottomRightRadius"],
|
|
88
|
+
};
|
|
89
|
+
// Utilities whose value is a length on the 4px spacing scale.
|
|
90
|
+
const lengthKeys = {
|
|
91
|
+
w: ["width"],
|
|
92
|
+
h: ["height"],
|
|
93
|
+
"min-w": ["minWidth"],
|
|
94
|
+
"min-h": ["minHeight"],
|
|
95
|
+
"max-w": ["maxWidth"],
|
|
96
|
+
"max-h": ["maxHeight"],
|
|
97
|
+
p: ["padding"],
|
|
98
|
+
px: ["paddingLeft", "paddingRight"],
|
|
99
|
+
py: ["paddingTop", "paddingBottom"],
|
|
100
|
+
pt: ["paddingTop"],
|
|
101
|
+
pr: ["paddingRight"],
|
|
102
|
+
pb: ["paddingBottom"],
|
|
103
|
+
pl: ["paddingLeft"],
|
|
104
|
+
m: ["margin"],
|
|
105
|
+
mx: ["marginLeft", "marginRight"],
|
|
106
|
+
my: ["marginTop", "marginBottom"],
|
|
107
|
+
mt: ["marginTop"],
|
|
108
|
+
mr: ["marginRight"],
|
|
109
|
+
mb: ["marginBottom"],
|
|
110
|
+
ml: ["marginLeft"],
|
|
111
|
+
gap: ["gap"],
|
|
112
|
+
"gap-x": ["columnGap"],
|
|
113
|
+
"gap-y": ["rowGap"],
|
|
114
|
+
top: ["top"],
|
|
115
|
+
right: ["right"],
|
|
116
|
+
bottom: ["bottom"],
|
|
117
|
+
left: ["left"],
|
|
118
|
+
inset: ["left", "right", "top", "bottom"],
|
|
119
|
+
basis: ["flexBasis"],
|
|
120
|
+
};
|
|
121
|
+
const warned = new Set();
|
|
122
|
+
function warnUnknown(cls) {
|
|
123
|
+
if (warned.has(cls))
|
|
124
|
+
return;
|
|
125
|
+
warned.add(cls);
|
|
126
|
+
console.warn(`[vsreact] unknown class "${cls}" ignored`);
|
|
127
|
+
}
|
|
128
|
+
/** "4"→16, "1.5"→6, "px"→1, "1/2"→"50%", "full"→"100%", "[123]"→123, "[45%]"→"45%" */
|
|
129
|
+
function parseLength(raw) {
|
|
130
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
131
|
+
const inner = raw.slice(1, -1);
|
|
132
|
+
if (inner.endsWith("%"))
|
|
133
|
+
return inner;
|
|
134
|
+
const n = Number(inner.replace(/px$/, ""));
|
|
135
|
+
return Number.isFinite(n) ? n : undefined;
|
|
136
|
+
}
|
|
137
|
+
if (raw === "px")
|
|
138
|
+
return 1;
|
|
139
|
+
if (raw === "full")
|
|
140
|
+
return "100%";
|
|
141
|
+
if (raw === "auto")
|
|
142
|
+
return "auto";
|
|
143
|
+
if (/^\d+\/\d+$/.test(raw)) {
|
|
144
|
+
const [num, den] = raw.split("/").map(Number);
|
|
145
|
+
return `${((num / den) * 100).toFixed(4).replace(/\.?0+$/, "")}%`;
|
|
146
|
+
}
|
|
147
|
+
const n = Number(raw);
|
|
148
|
+
return Number.isFinite(n) ? n * 4 : undefined;
|
|
149
|
+
}
|
|
150
|
+
function resolveClass(cls) {
|
|
151
|
+
const negative = cls.startsWith("-");
|
|
152
|
+
const body = negative ? cls.slice(1) : cls;
|
|
153
|
+
const negate = (v) => (negative && typeof v === "number" ? -v : v);
|
|
154
|
+
const known = staticClasses[body];
|
|
155
|
+
if (known && !negative)
|
|
156
|
+
return known;
|
|
157
|
+
// rounded, rounded-lg, rounded-t-lg, rounded-br-full, rounded-[10]
|
|
158
|
+
if (body === "rounded" || body.startsWith("rounded-")) {
|
|
159
|
+
const rest = body === "rounded" ? "" : body.slice("rounded-".length);
|
|
160
|
+
const parts = rest === "" ? [] : rest.split("-");
|
|
161
|
+
let corner = "";
|
|
162
|
+
let size = "";
|
|
163
|
+
if (parts.length === 1) {
|
|
164
|
+
if (parts[0] in radiusCorners)
|
|
165
|
+
corner = parts[0];
|
|
166
|
+
else
|
|
167
|
+
size = parts[0];
|
|
168
|
+
}
|
|
169
|
+
else if (parts.length === 2) {
|
|
170
|
+
[corner, size] = parts;
|
|
171
|
+
}
|
|
172
|
+
const corners = radiusCorners[corner];
|
|
173
|
+
const radius = size.startsWith("[") ? parseLength(size) : radiusSizes[size];
|
|
174
|
+
if (corners === undefined || radius === undefined)
|
|
175
|
+
return undefined;
|
|
176
|
+
if (corner === "")
|
|
177
|
+
return { borderRadius: radius };
|
|
178
|
+
const style = {};
|
|
179
|
+
for (const key of corners)
|
|
180
|
+
style[key] = radius;
|
|
181
|
+
return style;
|
|
182
|
+
}
|
|
183
|
+
const dash = body.indexOf("-");
|
|
184
|
+
if (dash <= 0)
|
|
185
|
+
return undefined;
|
|
186
|
+
// Longest matching length-utility prefix (handles gap-x-4 vs gap-4, min-w-*).
|
|
187
|
+
for (const prefix of Object.keys(lengthKeys).sort((a, b) => b.length - a.length)) {
|
|
188
|
+
if (body.startsWith(prefix + "-")) {
|
|
189
|
+
const value = parseLength(body.slice(prefix.length + 1));
|
|
190
|
+
if (value === undefined)
|
|
191
|
+
break;
|
|
192
|
+
const style = {};
|
|
193
|
+
for (const key of lengthKeys[prefix])
|
|
194
|
+
style[key] = negate(value);
|
|
195
|
+
return style;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const prefix = body.slice(0, dash);
|
|
199
|
+
const rest = body.slice(dash + 1);
|
|
200
|
+
switch (prefix) {
|
|
201
|
+
case "bg": {
|
|
202
|
+
const color = resolveColor(rest);
|
|
203
|
+
return color !== undefined ? { backgroundColor: color } : undefined;
|
|
204
|
+
}
|
|
205
|
+
case "text": {
|
|
206
|
+
if (rest in textSizes)
|
|
207
|
+
return { fontSize: textSizes[rest] };
|
|
208
|
+
const color = resolveColor(rest);
|
|
209
|
+
return color !== undefined ? { color } : undefined;
|
|
210
|
+
}
|
|
211
|
+
case "border": {
|
|
212
|
+
const n = Number(rest);
|
|
213
|
+
if (Number.isFinite(n))
|
|
214
|
+
return { borderWidth: n };
|
|
215
|
+
const color = resolveColor(rest);
|
|
216
|
+
return color !== undefined ? { borderColor: color } : undefined;
|
|
217
|
+
}
|
|
218
|
+
case "opacity": {
|
|
219
|
+
const n = Number(rest);
|
|
220
|
+
return Number.isFinite(n) ? { opacity: n / 100 } : undefined;
|
|
221
|
+
}
|
|
222
|
+
case "leading": {
|
|
223
|
+
const n = Number(rest);
|
|
224
|
+
return Number.isFinite(n) ? { lineHeight: n * 4 } : undefined;
|
|
225
|
+
}
|
|
226
|
+
case "flex": {
|
|
227
|
+
const n = Number(rest);
|
|
228
|
+
return Number.isFinite(n) ? { flex: n } : undefined;
|
|
229
|
+
}
|
|
230
|
+
default:
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const cache = new Map();
|
|
235
|
+
export function tw(classes) {
|
|
236
|
+
const cached = cache.get(classes);
|
|
237
|
+
if (cached)
|
|
238
|
+
return cached;
|
|
239
|
+
const result = { style: {} };
|
|
240
|
+
for (const cls of classes.split(/\s+/)) {
|
|
241
|
+
if (cls === "")
|
|
242
|
+
continue;
|
|
243
|
+
let bucket = "style";
|
|
244
|
+
let body = cls;
|
|
245
|
+
if (body.startsWith("hover:"))
|
|
246
|
+
[bucket, body] = ["hoverStyle", body.slice(6)];
|
|
247
|
+
else if (body.startsWith("active:"))
|
|
248
|
+
[bucket, body] = ["activeStyle", body.slice(7)];
|
|
249
|
+
else if (body.startsWith("focus:"))
|
|
250
|
+
[bucket, body] = ["focusStyle", body.slice(6)];
|
|
251
|
+
const resolved = resolveClass(body);
|
|
252
|
+
if (resolved === undefined) {
|
|
253
|
+
warnUnknown(cls);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
result[bucket] = Object.assign(result[bucket] ?? {}, resolved);
|
|
257
|
+
}
|
|
258
|
+
cache.set(classes, result);
|
|
259
|
+
return result;
|
|
260
|
+
}
|
package/package.json
CHANGED
|
@@ -1,29 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vsreact/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Write React. Ship native VST. A React renderer for JUCE audio plugins — QuickJS + Yoga + juce::Graphics, no webview.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"juce",
|
|
8
|
+
"vst",
|
|
9
|
+
"vst3",
|
|
10
|
+
"audio-plugin",
|
|
11
|
+
"renderer",
|
|
12
|
+
"quickjs",
|
|
13
|
+
"yoga",
|
|
14
|
+
"native",
|
|
15
|
+
"daw"
|
|
16
|
+
],
|
|
4
17
|
"type": "module",
|
|
5
18
|
"license": "MIT",
|
|
19
|
+
"homepage": "https://vsreact.n9records.com",
|
|
20
|
+
"bugs": "https://github.com/N9RecordsTechnologiesIL/VSReacT/issues",
|
|
6
21
|
"repository": {
|
|
7
22
|
"type": "git",
|
|
8
23
|
"url": "https://github.com/N9RecordsTechnologiesIL/VSReacT.git",
|
|
9
24
|
"directory": "vsreact/js"
|
|
10
25
|
},
|
|
11
|
-
"main": "
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"bun": "./src/index.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
12
36
|
"files": [
|
|
37
|
+
"dist",
|
|
13
38
|
"src",
|
|
14
39
|
"build.ts"
|
|
15
40
|
],
|
|
16
41
|
"scripts": {
|
|
17
42
|
"test": "bun test",
|
|
18
43
|
"typecheck": "tsc --noEmit",
|
|
19
|
-
"build": "bun run build.ts"
|
|
44
|
+
"build": "bun run build.ts",
|
|
45
|
+
"build:dist": "tsc -p tsconfig.build.json",
|
|
46
|
+
"prepublishOnly": "bun run build:dist"
|
|
20
47
|
},
|
|
21
48
|
"dependencies": {
|
|
49
|
+
"@types/react": "^18.3.3",
|
|
22
50
|
"react": "18.3.1",
|
|
23
51
|
"react-reconciler": "0.29.2"
|
|
24
52
|
},
|
|
25
53
|
"devDependencies": {
|
|
26
|
-
"@types/react": "^18.3.3",
|
|
27
54
|
"@types/react-reconciler": "0.28.8",
|
|
28
55
|
"bun-types": "^1.3.14",
|
|
29
56
|
"typescript": "^5.5.0"
|
package/src/runtime.ts
CHANGED
|
@@ -83,7 +83,10 @@ if (isHosted) {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
g.performance ??= { now: () => Date.now() } as Performance;
|
|
86
|
-
|
|
86
|
+
// Typed loosely on purpose: the dist build has no Node/Bun types, and the
|
|
87
|
+
// shim only exists for libraries that probe NODE_ENV.
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
g.process ??= { env: { NODE_ENV: "production" } } as any;
|
|
87
90
|
g.self ??= g as unknown as Window & typeof globalThis;
|
|
88
91
|
g.global ??= g;
|
|
89
92
|
}
|