@vsreact/core 0.0.10 → 0.0.12

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/src/hostConfig.ts CHANGED
@@ -1,164 +1,164 @@
1
- // react-reconciler host config (mutation mode). React commits become batched
2
- // mutation ops; event props become handler-registry entries + listener lists.
3
-
4
- import { DefaultEventPriority } from "react-reconciler/constants";
5
- import { queueOp, flushOps, setHandlers, removeHandlers, type EventHandler } from "./bridge";
6
- import { tw, type Style } from "./tw";
7
-
8
- export interface Instance {
9
- id: number;
10
- type: string;
11
- }
12
-
13
- export type TextInstance = Instance;
14
-
15
- let nextId = 1;
16
-
17
- const hostTypes: Record<string, string> = {
18
- "vs-view": "view",
19
- "vs-text": "text",
20
- "vs-image": "image",
21
- "vs-textinput": "textinput",
22
- "vs-native": "native",
23
- };
24
-
25
- const eventPropNames: Record<string, string> = {
26
- onClick: "click",
27
- onDoubleClick: "dblclick",
28
- onWheel: "wheel",
29
- onMouseEnter: "mouseenter",
30
- onMouseLeave: "mouseleave",
31
- onMouseDown: "mousedown",
32
- onMouseUp: "mouseup",
33
- onDragStart: "dragstart",
34
- onDrag: "drag",
35
- onDragEnd: "dragend",
36
- onLayout: "layout",
37
- onChange: "change",
38
- onSubmit: "submit",
39
- onFocus: "focus",
40
- onBlur: "blur",
41
- };
42
-
43
- function normalizeProps(props: Record<string, unknown>): {
44
- payload: Record<string, unknown>;
45
- handlers: Map<string, EventHandler>;
46
- } {
47
- const payload: Record<string, unknown> = {};
48
- const handlers = new Map<string, EventHandler>();
49
- const listeners: string[] = [];
50
-
51
- const resolved = typeof props.className === "string" ? tw(props.className) : { style: {} };
52
- payload.style = { ...resolved.style, ...((props.style as Style) ?? {}) };
53
- if (resolved.hoverStyle) payload.hoverStyle = resolved.hoverStyle;
54
- if (resolved.activeStyle) payload.activeStyle = resolved.activeStyle;
55
- if (resolved.focusStyle) payload.focusStyle = resolved.focusStyle;
56
-
57
- for (const [key, value] of Object.entries(props)) {
58
- if (value === undefined || key === "children" || key === "className" || key === "style")
59
- continue;
60
-
61
- const eventType = eventPropNames[key];
62
-
63
- if (eventType && typeof value === "function") {
64
- handlers.set(eventType, value as EventHandler);
65
- listeners.push(eventType);
66
- continue;
67
- }
68
-
69
- if (typeof value !== "function" && typeof value !== "object") payload[key] = value;
70
- }
71
-
72
- if (listeners.length > 0) payload.listeners = listeners;
73
-
74
- return { payload, handlers };
75
- }
76
-
77
- function applyProps(instance: Instance, props: Record<string, unknown>): void {
78
- const { payload, handlers } = normalizeProps(props);
79
- setHandlers(instance.id, handlers);
80
- queueOp(["setProps", instance.id, payload]);
81
- }
82
-
83
- export const hostConfig = {
84
- supportsMutation: true,
85
- supportsPersistence: false,
86
- supportsHydration: false,
87
- isPrimaryRenderer: true,
88
- supportsMicrotasks: true,
89
- scheduleMicrotask: (fn: () => void) => queueMicrotask(fn),
90
- scheduleTimeout: (fn: (...args: unknown[]) => void, delay?: number) => setTimeout(fn, delay),
91
- cancelTimeout: (id: ReturnType<typeof setTimeout>) => clearTimeout(id),
92
- noTimeout: -1 as const,
93
-
94
- getRootHostContext: () => ({}),
95
- getChildHostContext: (parent: object) => parent,
96
- getPublicInstance: (instance: Instance) => instance,
97
- prepareForCommit: () => null,
98
- resetAfterCommit: () => flushOps(),
99
- shouldSetTextContent: () => false,
100
-
101
- createInstance(type: string, props: Record<string, unknown>): Instance {
102
- const hostType = hostTypes[type];
103
- if (!hostType) throw new Error(`Unknown VSReacT element <${type}>`);
104
-
105
- const instance: Instance = { id: nextId++, type: hostType };
106
- queueOp(["create", instance.id, hostType]);
107
- applyProps(instance, props);
108
- return instance;
109
- },
110
-
111
- createTextInstance(text: string): TextInstance {
112
- const instance: TextInstance = { id: nextId++, type: "rawtext" };
113
- queueOp(["create", instance.id, "rawtext"]);
114
- queueOp(["setText", instance.id, text]);
115
- return instance;
116
- },
117
-
118
- appendInitialChild: (parent: Instance, child: Instance) =>
119
- queueOp(["appendChild", parent.id, child.id]),
120
- appendChild: (parent: Instance, child: Instance) =>
121
- queueOp(["appendChild", parent.id, child.id]),
122
- appendChildToContainer: (_container: unknown, child: Instance) =>
123
- queueOp(["appendChild", 0, child.id]),
124
- insertBefore: (parent: Instance, child: Instance, before: Instance) =>
125
- queueOp(["insertBefore", parent.id, child.id, before.id]),
126
- insertInContainerBefore: (_container: unknown, child: Instance, before: Instance) =>
127
- queueOp(["insertBefore", 0, child.id, before.id]),
128
- removeChild: (parent: Instance, child: Instance) =>
129
- queueOp(["removeChild", parent.id, child.id]),
130
- removeChildFromContainer: (_container: unknown, child: Instance) =>
131
- queueOp(["removeChild", 0, child.id]),
132
-
133
- finalizeInitialChildren: () => false,
134
-
135
- prepareUpdate: () => true,
136
-
137
- commitUpdate(
138
- instance: Instance,
139
- _updatePayload: unknown,
140
- _type: string,
141
- _prevProps: Record<string, unknown>,
142
- nextProps: Record<string, unknown>,
143
- ): void {
144
- applyProps(instance, nextProps);
145
- },
146
-
147
- commitTextUpdate(instance: TextInstance, _oldText: string, newText: string): void {
148
- queueOp(["setText", instance.id, newText]);
149
- },
150
-
151
- clearContainer: () => queueOp(["clearContainer"]),
152
-
153
- detachDeletedInstance(instance: Instance): void {
154
- removeHandlers(instance.id);
155
- },
156
-
157
- getCurrentEventPriority: () => DefaultEventPriority,
158
- getInstanceFromNode: () => null,
159
- getInstanceFromScope: () => null,
160
- beforeActiveInstanceBlur: () => {},
161
- afterActiveInstanceBlur: () => {},
162
- prepareScopeUpdate: () => {},
163
- preparePortalMount: () => {},
164
- };
1
+ // react-reconciler host config (mutation mode). React commits become batched
2
+ // mutation ops; event props become handler-registry entries + listener lists.
3
+
4
+ import { DefaultEventPriority } from "react-reconciler/constants";
5
+ import { queueOp, flushOps, setHandlers, removeHandlers, type EventHandler } from "./bridge";
6
+ import { tw, type Style } from "./tw";
7
+
8
+ export interface Instance {
9
+ id: number;
10
+ type: string;
11
+ }
12
+
13
+ export type TextInstance = Instance;
14
+
15
+ let nextId = 1;
16
+
17
+ const hostTypes: Record<string, string> = {
18
+ "vs-view": "view",
19
+ "vs-text": "text",
20
+ "vs-image": "image",
21
+ "vs-textinput": "textinput",
22
+ "vs-native": "native",
23
+ };
24
+
25
+ const eventPropNames: Record<string, string> = {
26
+ onClick: "click",
27
+ onDoubleClick: "dblclick",
28
+ onWheel: "wheel",
29
+ onMouseEnter: "mouseenter",
30
+ onMouseLeave: "mouseleave",
31
+ onMouseDown: "mousedown",
32
+ onMouseUp: "mouseup",
33
+ onDragStart: "dragstart",
34
+ onDrag: "drag",
35
+ onDragEnd: "dragend",
36
+ onLayout: "layout",
37
+ onChange: "change",
38
+ onSubmit: "submit",
39
+ onFocus: "focus",
40
+ onBlur: "blur",
41
+ };
42
+
43
+ function normalizeProps(props: Record<string, unknown>): {
44
+ payload: Record<string, unknown>;
45
+ handlers: Map<string, EventHandler>;
46
+ } {
47
+ const payload: Record<string, unknown> = {};
48
+ const handlers = new Map<string, EventHandler>();
49
+ const listeners: string[] = [];
50
+
51
+ const resolved = typeof props.className === "string" ? tw(props.className) : { style: {} };
52
+ payload.style = { ...resolved.style, ...((props.style as Style) ?? {}) };
53
+ if (resolved.hoverStyle) payload.hoverStyle = resolved.hoverStyle;
54
+ if (resolved.activeStyle) payload.activeStyle = resolved.activeStyle;
55
+ if (resolved.focusStyle) payload.focusStyle = resolved.focusStyle;
56
+
57
+ for (const [key, value] of Object.entries(props)) {
58
+ if (value === undefined || key === "children" || key === "className" || key === "style")
59
+ continue;
60
+
61
+ const eventType = eventPropNames[key];
62
+
63
+ if (eventType && typeof value === "function") {
64
+ handlers.set(eventType, value as EventHandler);
65
+ listeners.push(eventType);
66
+ continue;
67
+ }
68
+
69
+ if (typeof value !== "function" && typeof value !== "object") payload[key] = value;
70
+ }
71
+
72
+ if (listeners.length > 0) payload.listeners = listeners;
73
+
74
+ return { payload, handlers };
75
+ }
76
+
77
+ function applyProps(instance: Instance, props: Record<string, unknown>): void {
78
+ const { payload, handlers } = normalizeProps(props);
79
+ setHandlers(instance.id, handlers);
80
+ queueOp(["setProps", instance.id, payload]);
81
+ }
82
+
83
+ export const hostConfig = {
84
+ supportsMutation: true,
85
+ supportsPersistence: false,
86
+ supportsHydration: false,
87
+ isPrimaryRenderer: true,
88
+ supportsMicrotasks: true,
89
+ scheduleMicrotask: (fn: () => void) => queueMicrotask(fn),
90
+ scheduleTimeout: (fn: (...args: unknown[]) => void, delay?: number) => setTimeout(fn, delay),
91
+ cancelTimeout: (id: ReturnType<typeof setTimeout>) => clearTimeout(id),
92
+ noTimeout: -1 as const,
93
+
94
+ getRootHostContext: () => ({}),
95
+ getChildHostContext: (parent: object) => parent,
96
+ getPublicInstance: (instance: Instance) => instance,
97
+ prepareForCommit: () => null,
98
+ resetAfterCommit: () => flushOps(),
99
+ shouldSetTextContent: () => false,
100
+
101
+ createInstance(type: string, props: Record<string, unknown>): Instance {
102
+ const hostType = hostTypes[type];
103
+ if (!hostType) throw new Error(`Unknown VSReacT element <${type}>`);
104
+
105
+ const instance: Instance = { id: nextId++, type: hostType };
106
+ queueOp(["create", instance.id, hostType]);
107
+ applyProps(instance, props);
108
+ return instance;
109
+ },
110
+
111
+ createTextInstance(text: string): TextInstance {
112
+ const instance: TextInstance = { id: nextId++, type: "rawtext" };
113
+ queueOp(["create", instance.id, "rawtext"]);
114
+ queueOp(["setText", instance.id, text]);
115
+ return instance;
116
+ },
117
+
118
+ appendInitialChild: (parent: Instance, child: Instance) =>
119
+ queueOp(["appendChild", parent.id, child.id]),
120
+ appendChild: (parent: Instance, child: Instance) =>
121
+ queueOp(["appendChild", parent.id, child.id]),
122
+ appendChildToContainer: (_container: unknown, child: Instance) =>
123
+ queueOp(["appendChild", 0, child.id]),
124
+ insertBefore: (parent: Instance, child: Instance, before: Instance) =>
125
+ queueOp(["insertBefore", parent.id, child.id, before.id]),
126
+ insertInContainerBefore: (_container: unknown, child: Instance, before: Instance) =>
127
+ queueOp(["insertBefore", 0, child.id, before.id]),
128
+ removeChild: (parent: Instance, child: Instance) =>
129
+ queueOp(["removeChild", parent.id, child.id]),
130
+ removeChildFromContainer: (_container: unknown, child: Instance) =>
131
+ queueOp(["removeChild", 0, child.id]),
132
+
133
+ finalizeInitialChildren: () => false,
134
+
135
+ prepareUpdate: () => true,
136
+
137
+ commitUpdate(
138
+ instance: Instance,
139
+ _updatePayload: unknown,
140
+ _type: string,
141
+ _prevProps: Record<string, unknown>,
142
+ nextProps: Record<string, unknown>,
143
+ ): void {
144
+ applyProps(instance, nextProps);
145
+ },
146
+
147
+ commitTextUpdate(instance: TextInstance, _oldText: string, newText: string): void {
148
+ queueOp(["setText", instance.id, newText]);
149
+ },
150
+
151
+ clearContainer: () => queueOp(["clearContainer"]),
152
+
153
+ detachDeletedInstance(instance: Instance): void {
154
+ removeHandlers(instance.id);
155
+ },
156
+
157
+ getCurrentEventPriority: () => DefaultEventPriority,
158
+ getInstanceFromNode: () => null,
159
+ getInstanceFromScope: () => null,
160
+ beforeActiveInstanceBlur: () => {},
161
+ afterActiveInstanceBlur: () => {},
162
+ prepareScopeUpdate: () => {},
163
+ preparePortalMount: () => {},
164
+ };
package/src/index.ts CHANGED
@@ -90,4 +90,35 @@ export type {
90
90
  } from "./fields";
91
91
  export { ProgressBar, Spinner } from "./feedback";
92
92
  export type { ProgressBarProps, SpinnerProps } from "./feedback";
93
+ export { PianoKeyboard } from "./keyboard";
94
+ export type { PianoKeyboardProps } from "./keyboard";
95
+ export { StepSequencer } from "./sequencer";
96
+ export type { StepSequencerProps } from "./sequencer";
97
+ export {
98
+ mapRange,
99
+ formatDb,
100
+ formatHz,
101
+ formatMs,
102
+ formatPercent,
103
+ formatSemitones,
104
+ midiNoteName,
105
+ } from "./format";
106
+ export {
107
+ MacroPad,
108
+ ParamMacroPad,
109
+ HardwareKnob,
110
+ ParamHardwareKnob,
111
+ Crossfader,
112
+ ParamCrossfader,
113
+ PulseOrb,
114
+ } from "./specialty";
115
+ export type {
116
+ MacroPadProps,
117
+ ParamMacroPadProps,
118
+ HardwareKnobProps,
119
+ ParamHardwareKnobProps,
120
+ CrossfaderProps,
121
+ ParamCrossfaderProps,
122
+ PulseOrbProps,
123
+ } from "./specialty";
93
124
  export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
@@ -0,0 +1,151 @@
1
+ // The playable piano keyboard — preview a patch without touching the
2
+ // host. Views only (white row + absolutely-positioned blacks); press
3
+ // for note-on, release for note-off, drag across keys for glissando.
4
+
5
+ import { useRef, useState } from "react";
6
+ import { View, Text } from "./primitives";
7
+ import { midiNoteName } from "./format";
8
+
9
+ const BLACK_SEMIS = new Set([1, 3, 6, 8, 10]);
10
+ const isBlack = (note: number) => BLACK_SEMIS.has(((note % 12) + 12) % 12);
11
+
12
+ export interface PianoKeyboardProps {
13
+ /** MIDI note of the leftmost key (snapped down to a white key).
14
+ Default 48 (C3). */
15
+ startNote?: number;
16
+ /** Whole octaves; the keyboard ends on the top C. Default 2. */
17
+ octaves?: number;
18
+ /** Width of one white key. Default 24. */
19
+ whiteKeyWidth?: number;
20
+ /** White-key height; blacks are 60% of it. Default 96. */
21
+ height?: number;
22
+ /** Externally-held notes (host MIDI in) — painted active. */
23
+ heldNotes?: number[];
24
+ /** Prints "C3" on each C. Default true. */
25
+ showOctaveLabels?: boolean;
26
+ disabled?: boolean;
27
+ whiteColor?: string;
28
+ blackColor?: string;
29
+ activeColor?: string;
30
+ borderColor?: string;
31
+ onNoteOn?: (note: number) => void;
32
+ onNoteOff?: (note: number) => void;
33
+ }
34
+
35
+ export function PianoKeyboard({
36
+ startNote = 48,
37
+ octaves = 2,
38
+ whiteKeyWidth = 24,
39
+ height = 96,
40
+ heldNotes,
41
+ showOctaveLabels = true,
42
+ disabled,
43
+ whiteColor = "#ECF2E8",
44
+ blackColor = "#17191C",
45
+ activeColor = "#C6F135",
46
+ borderColor = "#00000066",
47
+ onNoteOn,
48
+ onNoteOff,
49
+ }: PianoKeyboardProps) {
50
+ const [pressed, setPressed] = useState<number | null>(null);
51
+ const active = useRef<number | null>(null);
52
+
53
+ // Snap the start onto a white key so the layout math stays honest.
54
+ let base = Math.round(startNote);
55
+ while (isBlack(base)) base += 1;
56
+
57
+ const whiteCount = Math.max(1, Math.round(octaves)) * 7 + 1;
58
+ const blackWidth = whiteKeyWidth * 0.62;
59
+ const blackHeight = height * 0.6;
60
+
61
+ const whites: number[] = [];
62
+ const blacks: Array<{ note: number; left: number }> = [];
63
+ for (let note = base; whites.length < whiteCount; note++) {
64
+ if (isBlack(note)) {
65
+ blacks.push({ note, left: whites.length * whiteKeyWidth - blackWidth / 2 });
66
+ } else {
67
+ whites.push(note);
68
+ }
69
+ }
70
+
71
+ const press = (note: number) => {
72
+ if (active.current === note) return;
73
+ if (active.current !== null) onNoteOff?.(active.current);
74
+ active.current = note;
75
+ setPressed(note);
76
+ onNoteOn?.(note);
77
+ };
78
+
79
+ const release = () => {
80
+ if (active.current === null) return;
81
+ onNoteOff?.(active.current);
82
+ active.current = null;
83
+ setPressed(null);
84
+ };
85
+
86
+ /** The key under an approximate pointer position (glissando). */
87
+ const noteAt = (x: number, y: number): number => {
88
+ if (y <= blackHeight) {
89
+ for (const black of blacks) {
90
+ if (x >= black.left && x <= black.left + blackWidth) return black.note;
91
+ }
92
+ }
93
+ const index = Math.min(whiteCount - 1, Math.max(0, Math.floor(x / whiteKeyWidth)));
94
+ return whites[index];
95
+ };
96
+
97
+ const isActive = (note: number) =>
98
+ pressed === note || (heldNotes !== undefined && heldNotes.includes(note));
99
+
100
+ const keyHandlers = (note: number, centerX: number, centerY: number) =>
101
+ disabled
102
+ ? {}
103
+ : {
104
+ onMouseDown: () => press(note),
105
+ onMouseUp: release,
106
+ onDrag: (e: { dx: number; dy: number }) => press(noteAt(centerX + e.dx, centerY + e.dy)),
107
+ onDragEnd: release,
108
+ };
109
+
110
+ return (
111
+ <View
112
+ className={`flex-row relative ${disabled ? "opacity-40" : "cursor-pointer"}`}
113
+ style={{ width: whiteCount * whiteKeyWidth, height }}
114
+ >
115
+ {whites.map((note, i) => (
116
+ <View
117
+ key={note}
118
+ className="border rounded-b-[3] justify-end items-center pb-1"
119
+ style={{
120
+ width: whiteKeyWidth,
121
+ height,
122
+ backgroundColor: isActive(note) ? activeColor : whiteColor,
123
+ borderColor,
124
+ }}
125
+ {...keyHandlers(note, (i + 0.5) * whiteKeyWidth, height * 0.8)}
126
+ >
127
+ {showOctaveLabels && ((note % 12) + 12) % 12 === 0 ? (
128
+ <Text className="text-[8] font-bold" style={{ color: "#00000088" }}>
129
+ {midiNoteName(note)}
130
+ </Text>
131
+ ) : null}
132
+ </View>
133
+ ))}
134
+ {blacks.map((black) => (
135
+ <View
136
+ key={black.note}
137
+ className="absolute border rounded-b-[3]"
138
+ style={{
139
+ left: black.left,
140
+ top: 0,
141
+ width: blackWidth,
142
+ height: blackHeight,
143
+ backgroundColor: isActive(black.note) ? activeColor : blackColor,
144
+ borderColor,
145
+ }}
146
+ {...keyHandlers(black.note, black.left + blackWidth / 2, blackHeight / 2)}
147
+ />
148
+ ))}
149
+ </View>
150
+ );
151
+ }
package/src/native.ts CHANGED
@@ -1,18 +1,18 @@
1
- // App-level messaging with the C++ host.
2
-
3
- import { addNativeListener, type EventHandler } from "./bridge";
4
-
5
- export const native = {
6
- /** Synchronously invokes a C++ handler registered in RootOptions.onNativeCall. */
7
- call(name: string, args?: unknown): any {
8
- const fn = (globalThis as Record<string, any>).__vsreact_nativeCall;
9
- if (typeof fn !== "function") return undefined;
10
- const json = fn(name, JSON.stringify(args ?? null));
11
- return json ? JSON.parse(json) : undefined;
12
- },
13
-
14
- /** Subscribes to events pushed from C++ via RootView::sendNativeEvent. */
15
- on(name: string, cb: (payload: any) => void): () => void {
16
- return addNativeListener(name, cb as EventHandler);
17
- },
18
- };
1
+ // App-level messaging with the C++ host.
2
+
3
+ import { addNativeListener, type EventHandler } from "./bridge";
4
+
5
+ export const native = {
6
+ /** Synchronously invokes a C++ handler registered in RootOptions.onNativeCall. */
7
+ call(name: string, args?: unknown): any {
8
+ const fn = (globalThis as Record<string, any>).__vsreact_nativeCall;
9
+ if (typeof fn !== "function") return undefined;
10
+ const json = fn(name, JSON.stringify(args ?? null));
11
+ return json ? JSON.parse(json) : undefined;
12
+ },
13
+
14
+ /** Subscribes to events pushed from C++ via RootView::sendNativeEvent. */
15
+ on(name: string, cb: (payload: any) => void): () => void {
16
+ return addNativeListener(name, cb as EventHandler);
17
+ },
18
+ };