@vsreact/core 0.0.11 → 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/build.ts +24 -24
- package/dist/components.d.ts +4 -0
- package/dist/components.js +2 -0
- package/dist/feedback.d.ts +4 -1
- package/dist/feedback.js +12 -2
- package/dist/format.d.ts +16 -0
- package/dist/format.js +50 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/keyboard.d.ts +23 -0
- package/dist/keyboard.js +79 -0
- package/dist/sequencer.d.ts +23 -0
- package/dist/sequencer.js +20 -0
- package/dist/tw.js +5 -0
- package/package.json +64 -64
- package/src/bridge.ts +66 -66
- package/src/components.ts +4 -0
- package/src/feedback.tsx +28 -6
- package/src/format.ts +62 -0
- package/src/hostConfig.test.tsx +120 -120
- package/src/hostConfig.ts +164 -164
- package/src/index.ts +13 -0
- package/src/keyboard.tsx +151 -0
- package/src/native.ts +18 -18
- package/src/perform.test.tsx +188 -0
- package/src/primitives.tsx +95 -95
- package/src/render.tsx +47 -47
- package/src/runtime.ts +92 -92
- package/src/sequencer.tsx +83 -0
- package/src/theme.ts +159 -159
- package/src/tw.test.ts +119 -113
- package/src/tw.ts +288 -284
package/build.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
// Bundles a VSReacT app entry into a single QuickJS-ready IIFE.
|
|
2
|
-
// bun run build.ts [entry] [outfile]
|
|
3
|
-
export {};
|
|
4
|
-
|
|
5
|
-
const entry = process.argv[2] ?? "demo/main.tsx";
|
|
6
|
-
const outfile = process.argv[3] ?? "demo/build/main.js";
|
|
7
|
-
|
|
8
|
-
const result = await Bun.build({
|
|
9
|
-
entrypoints: [entry],
|
|
10
|
-
target: "browser",
|
|
11
|
-
format: "iife",
|
|
12
|
-
minify: false,
|
|
13
|
-
define: {
|
|
14
|
-
"process.env.NODE_ENV": '"production"',
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
if (!result.success) {
|
|
19
|
-
for (const log of result.logs) console.error(log);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
await Bun.write(outfile, await result.outputs[0].text());
|
|
24
|
-
console.log(`built ${entry} -> ${outfile}`);
|
|
1
|
+
// Bundles a VSReacT app entry into a single QuickJS-ready IIFE.
|
|
2
|
+
// bun run build.ts [entry] [outfile]
|
|
3
|
+
export {};
|
|
4
|
+
|
|
5
|
+
const entry = process.argv[2] ?? "demo/main.tsx";
|
|
6
|
+
const outfile = process.argv[3] ?? "demo/build/main.js";
|
|
7
|
+
|
|
8
|
+
const result = await Bun.build({
|
|
9
|
+
entrypoints: [entry],
|
|
10
|
+
target: "browser",
|
|
11
|
+
format: "iife",
|
|
12
|
+
minify: false,
|
|
13
|
+
define: {
|
|
14
|
+
"process.env.NODE_ENV": '"production"',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!result.success) {
|
|
19
|
+
for (const log of result.logs) console.error(log);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await Bun.write(outfile, await result.outputs[0].text());
|
|
24
|
+
console.log(`built ${entry} -> ${outfile}`);
|
package/dist/components.d.ts
CHANGED
|
@@ -12,5 +12,9 @@ export { NumberBox, Checkbox, RadioGroup, ParamNumberBox, ParamCheckbox, ParamRa
|
|
|
12
12
|
export type { NumberBoxProps, CheckboxProps, RadioGroupProps, ParamNumberBoxProps, ParamCheckboxProps, ParamRadioGroupProps, } from "./fields";
|
|
13
13
|
export { ProgressBar, Spinner } from "./feedback";
|
|
14
14
|
export type { ProgressBarProps, SpinnerProps } from "./feedback";
|
|
15
|
+
export { PianoKeyboard } from "./keyboard";
|
|
16
|
+
export type { PianoKeyboardProps } from "./keyboard";
|
|
17
|
+
export { StepSequencer } from "./sequencer";
|
|
18
|
+
export type { StepSequencerProps } from "./sequencer";
|
|
15
19
|
export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
|
|
16
20
|
export type { MacroPadProps, ParamMacroPadProps, HardwareKnobProps, ParamHardwareKnobProps, CrossfaderProps, ParamCrossfaderProps, PulseOrbProps, } from "./specialty";
|
package/dist/components.js
CHANGED
|
@@ -12,4 +12,6 @@ export { Button } from "./button";
|
|
|
12
12
|
export { Tooltip, Modal } from "./popover";
|
|
13
13
|
export { NumberBox, Checkbox, RadioGroup, ParamNumberBox, ParamCheckbox, ParamRadioGroup, snapToStep, } from "./fields";
|
|
14
14
|
export { ProgressBar, Spinner } from "./feedback";
|
|
15
|
+
export { PianoKeyboard } from "./keyboard";
|
|
16
|
+
export { StepSequencer } from "./sequencer";
|
|
15
17
|
export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
|
package/dist/feedback.d.ts
CHANGED
|
@@ -5,12 +5,15 @@ export interface ProgressBarProps {
|
|
|
5
5
|
height?: number;
|
|
6
6
|
/** Renders "42%" alongside the bar. Default false. */
|
|
7
7
|
showPercent?: boolean;
|
|
8
|
+
/** Unknown duration — a segment sweeps the track and `value` is
|
|
9
|
+
ignored. Default false. */
|
|
10
|
+
indeterminate?: boolean;
|
|
8
11
|
trackColor?: string;
|
|
9
12
|
color?: string;
|
|
10
13
|
label?: string;
|
|
11
14
|
}
|
|
12
15
|
/** A determinate progress bar — downloads, renders, analysis passes. */
|
|
13
|
-
export declare function ProgressBar({ value, width, height, showPercent, trackColor, color, label, }: ProgressBarProps): import("react").JSX.Element;
|
|
16
|
+
export declare function ProgressBar({ value, width, height, showPercent, indeterminate, trackColor, color, label, }: ProgressBarProps): import("react").JSX.Element;
|
|
14
17
|
export interface SpinnerProps {
|
|
15
18
|
/** Diameter. Default 28. */
|
|
16
19
|
size?: number;
|
package/dist/feedback.js
CHANGED
|
@@ -6,9 +6,19 @@ import { View, Text } from "./primitives";
|
|
|
6
6
|
import { useInterval } from "./hooks";
|
|
7
7
|
const clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
8
8
|
/** A determinate progress bar — downloads, renders, analysis passes. */
|
|
9
|
-
export function ProgressBar({ value, width = 200, height = 8, showPercent = false, trackColor = "#2A2F27", color = "#C6F135", label, }) {
|
|
9
|
+
export function ProgressBar({ value, width = 200, height = 8, showPercent = false, indeterminate = false, trackColor = "#2A2F27", color = "#C6F135", label, }) {
|
|
10
10
|
const level = clamp01(value);
|
|
11
|
-
|
|
11
|
+
// The indeterminate sweep: a 30%-width segment marching left → right.
|
|
12
|
+
const [sweep, setSweep] = useState(0);
|
|
13
|
+
useInterval(() => setSweep((t) => (t + 0.018) % 1), indeterminate ? 16 : null);
|
|
14
|
+
const segment = width * 0.3;
|
|
15
|
+
const bar = (_jsxs(View, { className: "flex-row items-center gap-3", children: [_jsx(View, { className: "rounded-full overflow-hidden relative", style: { width, height, backgroundColor: trackColor }, children: indeterminate ? (_jsx(View, { className: "rounded-full absolute", style: {
|
|
16
|
+
left: sweep * (width + segment) - segment,
|
|
17
|
+
top: 0,
|
|
18
|
+
width: segment,
|
|
19
|
+
height,
|
|
20
|
+
backgroundColor: color,
|
|
21
|
+
} })) : (_jsx(View, { className: "rounded-full", style: { width: level * width, height, backgroundColor: color } })) }), showPercent && !indeterminate ? (_jsx(Text, { className: "text-faint text-[11] font-bold", style: { width: 34 }, children: `${Math.round(level * 100)}%` })) : null] }));
|
|
12
22
|
if (label === undefined)
|
|
13
23
|
return bar;
|
|
14
24
|
return (_jsxs(View, { className: "items-center gap-2", children: [bar, _jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })] }));
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Linear interpolation of `value` from [inMin, inMax] to [outMin, outMax].
|
|
2
|
+
`clampOut` pins the result inside the output range. */
|
|
3
|
+
export declare function mapRange(value: number, inMin: number, inMax: number, outMin: number, outMax: number, clampOut?: boolean): number;
|
|
4
|
+
/** Decibels with an explicit sign: "+6.0 dB", "0.0 dB", "-12.5 dB".
|
|
5
|
+
-Infinity (a gain fader on the floor) reads "-inf dB". */
|
|
6
|
+
export declare function formatDb(db: number, decimals?: number): string;
|
|
7
|
+
/** Frequency with automatic kHz: "440 Hz", "1.2 kHz". */
|
|
8
|
+
export declare function formatHz(hz: number, decimals?: number): string;
|
|
9
|
+
/** Time with automatic seconds: "350 ms", "1.25 s". */
|
|
10
|
+
export declare function formatMs(ms: number, decimals?: number): string;
|
|
11
|
+
/** A 0..1 value as "42%". */
|
|
12
|
+
export declare function formatPercent(value01: number, decimals?: number): string;
|
|
13
|
+
/** Semitones with an explicit sign: "+7 st", "0 st", "-12 st". */
|
|
14
|
+
export declare function formatSemitones(semitones: number, decimals?: number): string;
|
|
15
|
+
/** A MIDI note number as scientific pitch: 60 → "C4", 69 → "A4". */
|
|
16
|
+
export declare function midiNoteName(note: number): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Value formatting for parameter readouts — the strings DAW users
|
|
2
|
+
// expect next to a knob. Pure functions; pair them with NumberBox's
|
|
3
|
+
// `format` prop or any Text.
|
|
4
|
+
const trim = (value, decimals) => String(parseFloat(value.toFixed(decimals)));
|
|
5
|
+
/** Linear interpolation of `value` from [inMin, inMax] to [outMin, outMax].
|
|
6
|
+
`clampOut` pins the result inside the output range. */
|
|
7
|
+
export function mapRange(value, inMin, inMax, outMin, outMax, clampOut = false) {
|
|
8
|
+
const t = inMax === inMin ? 0 : (value - inMin) / (inMax - inMin);
|
|
9
|
+
const out = outMin + t * (outMax - outMin);
|
|
10
|
+
if (!clampOut)
|
|
11
|
+
return out;
|
|
12
|
+
const lo = Math.min(outMin, outMax);
|
|
13
|
+
const hi = Math.max(outMin, outMax);
|
|
14
|
+
return Math.min(hi, Math.max(lo, out));
|
|
15
|
+
}
|
|
16
|
+
/** Decibels with an explicit sign: "+6.0 dB", "0.0 dB", "-12.5 dB".
|
|
17
|
+
-Infinity (a gain fader on the floor) reads "-inf dB". */
|
|
18
|
+
export function formatDb(db, decimals = 1) {
|
|
19
|
+
if (db === Number.NEGATIVE_INFINITY)
|
|
20
|
+
return "-inf dB";
|
|
21
|
+
const text = db.toFixed(decimals);
|
|
22
|
+
return `${db > 0 && !text.startsWith("+") ? "+" : ""}${text} dB`;
|
|
23
|
+
}
|
|
24
|
+
/** Frequency with automatic kHz: "440 Hz", "1.2 kHz". */
|
|
25
|
+
export function formatHz(hz, decimals = 1) {
|
|
26
|
+
if (Math.abs(hz) >= 1000)
|
|
27
|
+
return `${trim(hz / 1000, Math.max(decimals, 1))} kHz`;
|
|
28
|
+
return `${trim(hz, decimals)} Hz`;
|
|
29
|
+
}
|
|
30
|
+
/** Time with automatic seconds: "350 ms", "1.25 s". */
|
|
31
|
+
export function formatMs(ms, decimals = 0) {
|
|
32
|
+
if (Math.abs(ms) >= 1000)
|
|
33
|
+
return `${trim(ms / 1000, Math.max(decimals, 2))} s`;
|
|
34
|
+
return `${trim(ms, decimals)} ms`;
|
|
35
|
+
}
|
|
36
|
+
/** A 0..1 value as "42%". */
|
|
37
|
+
export function formatPercent(value01, decimals = 0) {
|
|
38
|
+
return `${trim(value01 * 100, decimals)}%`;
|
|
39
|
+
}
|
|
40
|
+
/** Semitones with an explicit sign: "+7 st", "0 st", "-12 st". */
|
|
41
|
+
export function formatSemitones(semitones, decimals = 0) {
|
|
42
|
+
const text = trim(semitones, decimals);
|
|
43
|
+
return `${semitones > 0 && !text.startsWith("+") ? "+" : ""}${text} st`;
|
|
44
|
+
}
|
|
45
|
+
const NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
46
|
+
/** A MIDI note number as scientific pitch: 60 → "C4", 69 → "A4". */
|
|
47
|
+
export function midiNoteName(note) {
|
|
48
|
+
const n = Math.round(note);
|
|
49
|
+
return `${NOTE_NAMES[((n % 12) + 12) % 12]}${Math.floor(n / 12) - 1}`;
|
|
50
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export { NumberBox, Checkbox, RadioGroup, ParamNumberBox, ParamCheckbox, ParamRa
|
|
|
28
28
|
export type { NumberBoxProps, CheckboxProps, RadioGroupProps, ParamNumberBoxProps, ParamCheckboxProps, ParamRadioGroupProps, } from "./fields";
|
|
29
29
|
export { ProgressBar, Spinner } from "./feedback";
|
|
30
30
|
export type { ProgressBarProps, SpinnerProps } from "./feedback";
|
|
31
|
+
export { PianoKeyboard } from "./keyboard";
|
|
32
|
+
export type { PianoKeyboardProps } from "./keyboard";
|
|
33
|
+
export { StepSequencer } from "./sequencer";
|
|
34
|
+
export type { StepSequencerProps } from "./sequencer";
|
|
35
|
+
export { mapRange, formatDb, formatHz, formatMs, formatPercent, formatSemitones, midiNoteName, } from "./format";
|
|
31
36
|
export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
|
|
32
37
|
export type { MacroPadProps, ParamMacroPadProps, HardwareKnobProps, ParamHardwareKnobProps, CrossfaderProps, ParamCrossfaderProps, PulseOrbProps, } from "./specialty";
|
|
33
38
|
export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
|
package/dist/index.js
CHANGED
|
@@ -18,4 +18,7 @@ export { Knob, Slider, Toggle, XYPad, Segmented, Select, GenericEditor, ParamKno
|
|
|
18
18
|
export { Meter, usePeakHold, peakHoldStep } from "./meter";
|
|
19
19
|
export { NumberBox, Checkbox, RadioGroup, ParamNumberBox, ParamCheckbox, ParamRadioGroup, snapToStep, } from "./fields";
|
|
20
20
|
export { ProgressBar, Spinner } from "./feedback";
|
|
21
|
+
export { PianoKeyboard } from "./keyboard";
|
|
22
|
+
export { StepSequencer } from "./sequencer";
|
|
23
|
+
export { mapRange, formatDb, formatHz, formatMs, formatPercent, formatSemitones, midiNoteName, } from "./format";
|
|
21
24
|
export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface PianoKeyboardProps {
|
|
2
|
+
/** MIDI note of the leftmost key (snapped down to a white key).
|
|
3
|
+
Default 48 (C3). */
|
|
4
|
+
startNote?: number;
|
|
5
|
+
/** Whole octaves; the keyboard ends on the top C. Default 2. */
|
|
6
|
+
octaves?: number;
|
|
7
|
+
/** Width of one white key. Default 24. */
|
|
8
|
+
whiteKeyWidth?: number;
|
|
9
|
+
/** White-key height; blacks are 60% of it. Default 96. */
|
|
10
|
+
height?: number;
|
|
11
|
+
/** Externally-held notes (host MIDI in) — painted active. */
|
|
12
|
+
heldNotes?: number[];
|
|
13
|
+
/** Prints "C3" on each C. Default true. */
|
|
14
|
+
showOctaveLabels?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
whiteColor?: string;
|
|
17
|
+
blackColor?: string;
|
|
18
|
+
activeColor?: string;
|
|
19
|
+
borderColor?: string;
|
|
20
|
+
onNoteOn?: (note: number) => void;
|
|
21
|
+
onNoteOff?: (note: number) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function PianoKeyboard({ startNote, octaves, whiteKeyWidth, height, heldNotes, showOctaveLabels, disabled, whiteColor, blackColor, activeColor, borderColor, onNoteOn, onNoteOff, }: PianoKeyboardProps): import("react").JSX.Element;
|
package/dist/keyboard.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The playable piano keyboard — preview a patch without touching the
|
|
3
|
+
// host. Views only (white row + absolutely-positioned blacks); press
|
|
4
|
+
// for note-on, release for note-off, drag across keys for glissando.
|
|
5
|
+
import { useRef, useState } from "react";
|
|
6
|
+
import { View, Text } from "./primitives";
|
|
7
|
+
import { midiNoteName } from "./format";
|
|
8
|
+
const BLACK_SEMIS = new Set([1, 3, 6, 8, 10]);
|
|
9
|
+
const isBlack = (note) => BLACK_SEMIS.has(((note % 12) + 12) % 12);
|
|
10
|
+
export function PianoKeyboard({ startNote = 48, octaves = 2, whiteKeyWidth = 24, height = 96, heldNotes, showOctaveLabels = true, disabled, whiteColor = "#ECF2E8", blackColor = "#17191C", activeColor = "#C6F135", borderColor = "#00000066", onNoteOn, onNoteOff, }) {
|
|
11
|
+
const [pressed, setPressed] = useState(null);
|
|
12
|
+
const active = useRef(null);
|
|
13
|
+
// Snap the start onto a white key so the layout math stays honest.
|
|
14
|
+
let base = Math.round(startNote);
|
|
15
|
+
while (isBlack(base))
|
|
16
|
+
base += 1;
|
|
17
|
+
const whiteCount = Math.max(1, Math.round(octaves)) * 7 + 1;
|
|
18
|
+
const blackWidth = whiteKeyWidth * 0.62;
|
|
19
|
+
const blackHeight = height * 0.6;
|
|
20
|
+
const whites = [];
|
|
21
|
+
const blacks = [];
|
|
22
|
+
for (let note = base; whites.length < whiteCount; note++) {
|
|
23
|
+
if (isBlack(note)) {
|
|
24
|
+
blacks.push({ note, left: whites.length * whiteKeyWidth - blackWidth / 2 });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
whites.push(note);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const press = (note) => {
|
|
31
|
+
if (active.current === note)
|
|
32
|
+
return;
|
|
33
|
+
if (active.current !== null)
|
|
34
|
+
onNoteOff?.(active.current);
|
|
35
|
+
active.current = note;
|
|
36
|
+
setPressed(note);
|
|
37
|
+
onNoteOn?.(note);
|
|
38
|
+
};
|
|
39
|
+
const release = () => {
|
|
40
|
+
if (active.current === null)
|
|
41
|
+
return;
|
|
42
|
+
onNoteOff?.(active.current);
|
|
43
|
+
active.current = null;
|
|
44
|
+
setPressed(null);
|
|
45
|
+
};
|
|
46
|
+
/** The key under an approximate pointer position (glissando). */
|
|
47
|
+
const noteAt = (x, y) => {
|
|
48
|
+
if (y <= blackHeight) {
|
|
49
|
+
for (const black of blacks) {
|
|
50
|
+
if (x >= black.left && x <= black.left + blackWidth)
|
|
51
|
+
return black.note;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const index = Math.min(whiteCount - 1, Math.max(0, Math.floor(x / whiteKeyWidth)));
|
|
55
|
+
return whites[index];
|
|
56
|
+
};
|
|
57
|
+
const isActive = (note) => pressed === note || (heldNotes !== undefined && heldNotes.includes(note));
|
|
58
|
+
const keyHandlers = (note, centerX, centerY) => disabled
|
|
59
|
+
? {}
|
|
60
|
+
: {
|
|
61
|
+
onMouseDown: () => press(note),
|
|
62
|
+
onMouseUp: release,
|
|
63
|
+
onDrag: (e) => press(noteAt(centerX + e.dx, centerY + e.dy)),
|
|
64
|
+
onDragEnd: release,
|
|
65
|
+
};
|
|
66
|
+
return (_jsxs(View, { className: `flex-row relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width: whiteCount * whiteKeyWidth, height }, children: [whites.map((note, i) => (_jsx(View, { className: "border rounded-b-[3] justify-end items-center pb-1", style: {
|
|
67
|
+
width: whiteKeyWidth,
|
|
68
|
+
height,
|
|
69
|
+
backgroundColor: isActive(note) ? activeColor : whiteColor,
|
|
70
|
+
borderColor,
|
|
71
|
+
}, ...keyHandlers(note, (i + 0.5) * whiteKeyWidth, height * 0.8), children: showOctaveLabels && ((note % 12) + 12) % 12 === 0 ? (_jsx(Text, { className: "text-[8] font-bold", style: { color: "#00000088" }, children: midiNoteName(note) })) : null }, note))), blacks.map((black) => (_jsx(View, { className: "absolute border rounded-b-[3]", style: {
|
|
72
|
+
left: black.left,
|
|
73
|
+
top: 0,
|
|
74
|
+
width: blackWidth,
|
|
75
|
+
height: blackHeight,
|
|
76
|
+
backgroundColor: isActive(black.note) ? activeColor : blackColor,
|
|
77
|
+
borderColor,
|
|
78
|
+
}, ...keyHandlers(black.note, black.left + blackWidth / 2, blackHeight / 2) }, black.note)))] }));
|
|
79
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface StepSequencerProps {
|
|
2
|
+
/** rows × steps. `pattern[row][step]` true = lit. */
|
|
3
|
+
pattern: boolean[][];
|
|
4
|
+
/** The column currently playing — painted with a bright ring. */
|
|
5
|
+
playhead?: number;
|
|
6
|
+
/** Printed left of each row ("KICK", "SNARE"…). */
|
|
7
|
+
rowLabels?: string[];
|
|
8
|
+
/** Cell edge length. Default 20. */
|
|
9
|
+
cellSize?: number;
|
|
10
|
+
/** Space between cells. Default 5. */
|
|
11
|
+
gap?: number;
|
|
12
|
+
/** Downbeat tint every N steps. Default 4; 0 disables. */
|
|
13
|
+
groupEvery?: number;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
cellColor?: string;
|
|
16
|
+
/** Downbeat cells (step 0, 4, 8…) when unlit. */
|
|
17
|
+
downbeatColor?: string;
|
|
18
|
+
activeColor?: string;
|
|
19
|
+
playheadColor?: string;
|
|
20
|
+
labelColor?: string;
|
|
21
|
+
onToggle: (row: number, step: number, next: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function StepSequencer({ pattern, playhead, rowLabels, cellSize, gap, groupEvery, disabled, cellColor, downbeatColor, activeColor, playheadColor, labelColor, onToggle, }: StepSequencerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The step sequencer grid — rows of cells you click on and off, with a
|
|
3
|
+
// playhead column the host drives. Patterns live in the app's state;
|
|
4
|
+
// the component is fully controlled.
|
|
5
|
+
import { View, Text } from "./primitives";
|
|
6
|
+
export function StepSequencer({ pattern, playhead, rowLabels, cellSize = 20, gap = 5, groupEvery = 4, disabled, cellColor = "#242922", downbeatColor = "#2E342B", activeColor = "#C6F135", playheadColor = "#ECF2E8", labelColor = "#a1a1aa", onToggle, }) {
|
|
7
|
+
const labelWidth = rowLabels !== undefined && rowLabels.length > 0
|
|
8
|
+
? Math.max(...rowLabels.map((label) => label.length)) * 7 + 10
|
|
9
|
+
: 0;
|
|
10
|
+
return (_jsx(View, { className: disabled ? "opacity-40" : "", style: { rowGap: gap }, children: pattern.map((row, r) => (_jsxs(View, { className: "flex-row items-center", style: { columnGap: gap }, children: [labelWidth > 0 ? (_jsx(Text, { className: "text-[10] font-bold tracking-widest", style: { width: labelWidth, color: labelColor }, children: rowLabels?.[r] ?? "" })) : null, row.map((lit, s) => (_jsx(View, { className: `rounded-[4] border ${disabled ? "" : "cursor-pointer"}`, style: {
|
|
11
|
+
width: cellSize,
|
|
12
|
+
height: cellSize,
|
|
13
|
+
backgroundColor: lit
|
|
14
|
+
? activeColor
|
|
15
|
+
: groupEvery > 0 && s % groupEvery === 0
|
|
16
|
+
? downbeatColor
|
|
17
|
+
: cellColor,
|
|
18
|
+
borderColor: s === playhead ? playheadColor : "#00000055",
|
|
19
|
+
}, onClick: disabled ? undefined : () => onToggle(r, s, !lit) }, s)))] }, r))) }));
|
|
20
|
+
}
|
package/dist/tw.js
CHANGED
|
@@ -217,6 +217,11 @@ function resolveClass(cls) {
|
|
|
217
217
|
case "text": {
|
|
218
218
|
if (rest in textSizes)
|
|
219
219
|
return { fontSize: textSizes[rest] };
|
|
220
|
+
if (rest.startsWith("[") && !rest.startsWith("[#")) {
|
|
221
|
+
const size = parseLength(rest);
|
|
222
|
+
if (typeof size === "number")
|
|
223
|
+
return { fontSize: size };
|
|
224
|
+
}
|
|
220
225
|
const color = resolveColor(rest);
|
|
221
226
|
return color !== undefined ? { color } : undefined;
|
|
222
227
|
}
|
package/package.json
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@vsreact/core",
|
|
3
|
-
"version": "0.0.
|
|
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
|
-
],
|
|
17
|
-
"type": "module",
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"homepage": "https://vsreact.n9records.com",
|
|
20
|
-
"bugs": "https://github.com/N9RecordsTechnologiesIL/VSReacT/issues",
|
|
21
|
-
"repository": {
|
|
22
|
-
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/N9RecordsTechnologiesIL/VSReacT.git",
|
|
24
|
-
"directory": "vsreact/js"
|
|
25
|
-
},
|
|
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
|
-
"./components": {
|
|
36
|
-
"types": "./dist/components.d.ts",
|
|
37
|
-
"bun": "./src/components.ts",
|
|
38
|
-
"import": "./dist/components.js",
|
|
39
|
-
"default": "./dist/components.js"
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"files": [
|
|
43
|
-
"dist",
|
|
44
|
-
"src",
|
|
45
|
-
"build.ts"
|
|
46
|
-
],
|
|
47
|
-
"scripts": {
|
|
48
|
-
"test": "bun test",
|
|
49
|
-
"typecheck": "tsc --noEmit",
|
|
50
|
-
"build": "bun run build.ts",
|
|
51
|
-
"build:dist": "tsc -p tsconfig.build.json",
|
|
52
|
-
"prepublishOnly": "bun run build:dist"
|
|
53
|
-
},
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"@types/react": "^18.3.3",
|
|
56
|
-
"react": "18.3.1",
|
|
57
|
-
"react-reconciler": "0.29.2"
|
|
58
|
-
},
|
|
59
|
-
"devDependencies": {
|
|
60
|
-
"@types/react-reconciler": "0.28.8",
|
|
61
|
-
"bun-types": "^1.3.14",
|
|
62
|
-
"typescript": "^5.5.0"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@vsreact/core",
|
|
3
|
+
"version": "0.0.12",
|
|
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
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://vsreact.n9records.com",
|
|
20
|
+
"bugs": "https://github.com/N9RecordsTechnologiesIL/VSReacT/issues",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/N9RecordsTechnologiesIL/VSReacT.git",
|
|
24
|
+
"directory": "vsreact/js"
|
|
25
|
+
},
|
|
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
|
+
"./components": {
|
|
36
|
+
"types": "./dist/components.d.ts",
|
|
37
|
+
"bun": "./src/components.ts",
|
|
38
|
+
"import": "./dist/components.js",
|
|
39
|
+
"default": "./dist/components.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"src",
|
|
45
|
+
"build.ts"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "bun test",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"build": "bun run build.ts",
|
|
51
|
+
"build:dist": "tsc -p tsconfig.build.json",
|
|
52
|
+
"prepublishOnly": "bun run build:dist"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@types/react": "^18.3.3",
|
|
56
|
+
"react": "18.3.1",
|
|
57
|
+
"react-reconciler": "0.29.2"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/react-reconciler": "0.28.8",
|
|
61
|
+
"bun-types": "^1.3.14",
|
|
62
|
+
"typescript": "^5.5.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/bridge.ts
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
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
|
-
|
|
5
|
-
import { fireTimer } from "./runtime";
|
|
6
|
-
|
|
7
|
-
export type Op = unknown[];
|
|
8
|
-
|
|
9
|
-
let queue: Op[] = [];
|
|
10
|
-
|
|
11
|
-
export function queueOp(op: Op): void {
|
|
12
|
-
queue.push(op);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function flushOps(): void {
|
|
16
|
-
if (queue.length === 0) return;
|
|
17
|
-
const json = JSON.stringify(queue);
|
|
18
|
-
queue = [];
|
|
19
|
-
(globalThis as Record<string, any>).__vsreact_flush?.(json);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type EventHandler = (payload: unknown) => void;
|
|
23
|
-
|
|
24
|
-
const eventHandlers = new Map<number, Map<string, EventHandler>>();
|
|
25
|
-
|
|
26
|
-
export function setHandlers(nodeId: number, handlers: Map<string, EventHandler>): void {
|
|
27
|
-
if (handlers.size === 0) eventHandlers.delete(nodeId);
|
|
28
|
-
else eventHandlers.set(nodeId, handlers);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function removeHandlers(nodeId: number): void {
|
|
32
|
-
eventHandlers.delete(nodeId);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const nativeListeners = new Map<string, Set<EventHandler>>();
|
|
36
|
-
|
|
37
|
-
export function addNativeListener(name: string, cb: EventHandler): () => void {
|
|
38
|
-
let set = nativeListeners.get(name);
|
|
39
|
-
if (!set) nativeListeners.set(name, (set = new Set()));
|
|
40
|
-
set.add(cb);
|
|
41
|
-
return () => {
|
|
42
|
-
set.delete(cb);
|
|
43
|
-
if (set.size === 0) nativeListeners.delete(name);
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface DispatchMessage {
|
|
48
|
-
kind: "event" | "native" | "timer";
|
|
49
|
-
nodeId?: number;
|
|
50
|
-
type?: string;
|
|
51
|
-
name?: string;
|
|
52
|
-
id?: number;
|
|
53
|
-
payload?: unknown;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
(globalThis as Record<string, any>).__vsreact_dispatch = (json: string) => {
|
|
57
|
-
const msg = JSON.parse(json) as DispatchMessage;
|
|
58
|
-
|
|
59
|
-
if (msg.kind === "timer" && msg.id !== undefined) {
|
|
60
|
-
fireTimer(msg.id);
|
|
61
|
-
} else if (msg.kind === "event" && msg.nodeId !== undefined && msg.type) {
|
|
62
|
-
eventHandlers.get(msg.nodeId)?.get(msg.type)?.(msg.payload);
|
|
63
|
-
} else if (msg.kind === "native" && msg.name) {
|
|
64
|
-
nativeListeners.get(msg.name)?.forEach((cb) => cb(msg.payload));
|
|
65
|
-
}
|
|
66
|
-
};
|
|
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
|
+
|
|
5
|
+
import { fireTimer } from "./runtime";
|
|
6
|
+
|
|
7
|
+
export type Op = unknown[];
|
|
8
|
+
|
|
9
|
+
let queue: Op[] = [];
|
|
10
|
+
|
|
11
|
+
export function queueOp(op: Op): void {
|
|
12
|
+
queue.push(op);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function flushOps(): void {
|
|
16
|
+
if (queue.length === 0) return;
|
|
17
|
+
const json = JSON.stringify(queue);
|
|
18
|
+
queue = [];
|
|
19
|
+
(globalThis as Record<string, any>).__vsreact_flush?.(json);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type EventHandler = (payload: unknown) => void;
|
|
23
|
+
|
|
24
|
+
const eventHandlers = new Map<number, Map<string, EventHandler>>();
|
|
25
|
+
|
|
26
|
+
export function setHandlers(nodeId: number, handlers: Map<string, EventHandler>): void {
|
|
27
|
+
if (handlers.size === 0) eventHandlers.delete(nodeId);
|
|
28
|
+
else eventHandlers.set(nodeId, handlers);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function removeHandlers(nodeId: number): void {
|
|
32
|
+
eventHandlers.delete(nodeId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nativeListeners = new Map<string, Set<EventHandler>>();
|
|
36
|
+
|
|
37
|
+
export function addNativeListener(name: string, cb: EventHandler): () => void {
|
|
38
|
+
let set = nativeListeners.get(name);
|
|
39
|
+
if (!set) nativeListeners.set(name, (set = new Set()));
|
|
40
|
+
set.add(cb);
|
|
41
|
+
return () => {
|
|
42
|
+
set.delete(cb);
|
|
43
|
+
if (set.size === 0) nativeListeners.delete(name);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DispatchMessage {
|
|
48
|
+
kind: "event" | "native" | "timer";
|
|
49
|
+
nodeId?: number;
|
|
50
|
+
type?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
id?: number;
|
|
53
|
+
payload?: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
(globalThis as Record<string, any>).__vsreact_dispatch = (json: string) => {
|
|
57
|
+
const msg = JSON.parse(json) as DispatchMessage;
|
|
58
|
+
|
|
59
|
+
if (msg.kind === "timer" && msg.id !== undefined) {
|
|
60
|
+
fireTimer(msg.id);
|
|
61
|
+
} else if (msg.kind === "event" && msg.nodeId !== undefined && msg.type) {
|
|
62
|
+
eventHandlers.get(msg.nodeId)?.get(msg.type)?.(msg.payload);
|
|
63
|
+
} else if (msg.kind === "native" && msg.name) {
|
|
64
|
+
nativeListeners.get(msg.name)?.forEach((cb) => cb(msg.payload));
|
|
65
|
+
}
|
|
66
|
+
};
|
package/src/components.ts
CHANGED
|
@@ -64,6 +64,10 @@ export type {
|
|
|
64
64
|
} from "./fields";
|
|
65
65
|
export { ProgressBar, Spinner } from "./feedback";
|
|
66
66
|
export type { ProgressBarProps, SpinnerProps } from "./feedback";
|
|
67
|
+
export { PianoKeyboard } from "./keyboard";
|
|
68
|
+
export type { PianoKeyboardProps } from "./keyboard";
|
|
69
|
+
export { StepSequencer } from "./sequencer";
|
|
70
|
+
export type { StepSequencerProps } from "./sequencer";
|
|
67
71
|
export {
|
|
68
72
|
MacroPad,
|
|
69
73
|
ParamMacroPad,
|