@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/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}`);
@@ -12,3 +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";
19
+ export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
20
+ export type { MacroPadProps, ParamMacroPadProps, HardwareKnobProps, ParamHardwareKnobProps, CrossfaderProps, ParamCrossfaderProps, PulseOrbProps, } from "./specialty";
@@ -12,3 +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";
17
+ export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
@@ -40,6 +40,8 @@ export interface SliderProps {
40
40
  height?: number;
41
41
  /** Vertical fader — drag up for more, fill rises from the bottom. */
42
42
  vertical?: boolean;
43
+ /** Flat hardware-style bar thumb instead of the dot. */
44
+ barThumb?: boolean;
43
45
  label?: string;
44
46
  disabled?: boolean;
45
47
  /** Double-click resets to this (DAW convention). */
@@ -52,7 +54,7 @@ export interface SliderProps {
52
54
  onBegin?: () => void;
53
55
  onEnd?: () => void;
54
56
  }
55
- export declare function Slider({ value, width, height, vertical, label, disabled, defaultValue, wheelSensitivity, trackColor, valueColor, onChange, onBegin, onEnd, }: SliderProps): import("react").JSX.Element;
57
+ export declare function Slider({ value, width, height, vertical, barThumb, label, disabled, defaultValue, wheelSensitivity, trackColor, valueColor, onChange, onBegin, onEnd, }: SliderProps): import("react").JSX.Element;
56
58
  export interface ParamSliderProps {
57
59
  paramId: string;
58
60
  label?: string;
@@ -67,6 +69,9 @@ export declare function ParamSlider({ paramId, label, ...rest }: ParamSliderProp
67
69
  export interface ToggleProps {
68
70
  on: boolean;
69
71
  label?: string;
72
+ /** Side captions, hardware style: <Toggle offLabel="OFF" onLabel="ON" />. */
73
+ offLabel?: string;
74
+ onLabel?: string;
70
75
  /** Track height; width is 1.8×. Default 22. */
71
76
  size?: number;
72
77
  disabled?: boolean;
@@ -76,7 +81,7 @@ export interface ToggleProps {
76
81
  onChange: (on: boolean) => void;
77
82
  }
78
83
  /** A switch with a spring-animated thumb — bypass, on/off, A/B. */
79
- export declare function Toggle({ on, label, size, disabled, trackColor, onColor, thumbColor, onChange, }: ToggleProps): import("react").JSX.Element;
84
+ export declare function Toggle({ on, label, offLabel, onLabel, size, disabled, trackColor, onColor, thumbColor, onChange, }: ToggleProps): import("react").JSX.Element;
80
85
  export interface ParamToggleProps {
81
86
  paramId: string;
82
87
  label?: string;
package/dist/controls.js CHANGED
@@ -50,7 +50,7 @@ export function ParamKnob({ paramId, label, ...rest }) {
50
50
  const param = useParameter(paramId);
51
51
  return (_jsx(Knob, { value: param.value, text: param.text, label: label ?? param.name.toUpperCase(), defaultValue: param.defaultValue, onChange: param.set, onBegin: param.begin, onEnd: param.end, ...rest }));
52
52
  }
53
- export function Slider({ value, width = 160, height = 160, vertical, label, disabled, defaultValue, wheelSensitivity = 0.4, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
53
+ export function Slider({ value, width = 160, height = 160, vertical, barThumb, label, disabled, defaultValue, wheelSensitivity = 0.4, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
54
54
  const startValue = useRef(0);
55
55
  const clamped = clamp01(value);
56
56
  const onDragStart = disabled
@@ -70,9 +70,9 @@ export function Slider({ value, width = 160, height = 160, vertical, label, disa
70
70
  ? undefined
71
71
  : (e) => nudge(clamped + e.dy * wheelSensitivity);
72
72
  if (vertical) {
73
- return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: `w-[18] relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { height }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current - e.dy / height)), onDragEnd: onDragEnd, onDoubleClick: onDoubleClick, onWheel: onWheel, children: [_jsx(View, { className: "absolute w-[4] rounded-full left-[7] top-0 bottom-0", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute w-[4] rounded-full left-[7] bottom-0", style: { height: clamped * height, backgroundColor: valueColor } }), _jsx(View, { className: "absolute w-[12] h-[12] rounded-full left-[3]", style: { top: (1 - clamped) * (height - 12), backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
73
+ return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: `w-[18] relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { height }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current - e.dy / height)), onDragEnd: onDragEnd, onDoubleClick: onDoubleClick, onWheel: onWheel, children: [_jsx(View, { className: "absolute w-[4] rounded-full left-[7] top-0 bottom-0", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute w-[4] rounded-full left-[7] bottom-0", style: { height: clamped * height, backgroundColor: valueColor } }), barThumb ? (_jsx(View, { className: "absolute w-[18] h-[5] rounded-[2] left-0", style: { top: (1 - clamped) * (height - 5), backgroundColor: valueColor } })) : (_jsx(View, { className: "absolute w-[12] h-[12] rounded-full left-[3]", style: { top: (1 - clamped) * (height - 12), backgroundColor: valueColor } }))] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
74
74
  }
75
- return (_jsxs(View, { className: "gap-2", children: [_jsxs(View, { className: `h-[18] justify-center relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current + e.dx / width)), onDragEnd: onDragEnd, onDoubleClick: onDoubleClick, onWheel: onWheel, children: [_jsx(View, { className: "h-[4] rounded-full", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute h-[4] rounded-full top-[7]", style: { width: clamped * width, backgroundColor: valueColor } }), _jsx(View, { className: "absolute w-[12] h-[12] rounded-full top-[3]", style: { left: clamped * (width - 12), backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
75
+ return (_jsxs(View, { className: "gap-2", children: [_jsxs(View, { className: `h-[18] justify-center relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current + e.dx / width)), onDragEnd: onDragEnd, onDoubleClick: onDoubleClick, onWheel: onWheel, children: [_jsx(View, { className: "h-[4] rounded-full", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute h-[4] rounded-full top-[7]", style: { width: clamped * width, backgroundColor: valueColor } }), barThumb ? (_jsx(View, { className: "absolute w-[5] h-[18] rounded-[2] top-0", style: { left: clamped * (width - 5), 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] }));
76
76
  }
77
77
  /** A Slider bound to an APVTS parameter — double-click resets to the
78
78
  host's default, the wheel nudges, gestures stay automation-safe. */
@@ -81,18 +81,18 @@ export function ParamSlider({ paramId, label, ...rest }) {
81
81
  return (_jsx(Slider, { value: param.value, label: label ?? param.name.toUpperCase(), defaultValue: param.defaultValue, onChange: param.set, onBegin: param.begin, onEnd: param.end, ...rest }));
82
82
  }
83
83
  /** A switch with a spring-animated thumb — bypass, on/off, A/B. */
84
- export function Toggle({ on, label, size = 22, disabled, trackColor = "#2A2F27", onColor = "#C6F135", thumbColor = "#F4F4F5", onChange, }) {
84
+ export function Toggle({ on, label, offLabel, onLabel, size = 22, disabled, trackColor = "#2A2F27", onColor = "#C6F135", thumbColor = "#F4F4F5", onChange, }) {
85
85
  const t = useSpring(on ? 1 : 0, { stiffness: 300, damping: 28 });
86
86
  const trackWidth = Math.round(size * 1.8);
87
87
  const thumb = size - 6;
88
88
  const travel = trackWidth - thumb - 6;
89
- return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `relative rounded-full ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width: trackWidth, height: size, backgroundColor: on ? onColor : trackColor }, onClick: disabled ? undefined : () => onChange(!on), children: _jsx(View, { className: "absolute rounded-full", style: {
90
- width: thumb,
91
- height: thumb,
92
- top: 3,
93
- left: 3 + clamp01(t) * travel,
94
- backgroundColor: thumbColor,
95
- } }) }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
89
+ return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: "flex-row items-center gap-2", children: [offLabel !== undefined ? (_jsx(Text, { className: "text-[9] font-bold tracking-widest", style: { color: on ? "#6f6e66" : "#ECF2E8" }, children: offLabel })) : null, _jsx(View, { className: `relative rounded-full ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width: trackWidth, height: size, backgroundColor: on ? onColor : trackColor }, onClick: disabled ? undefined : () => onChange(!on), children: _jsx(View, { className: "absolute rounded-full", style: {
90
+ width: thumb,
91
+ height: thumb,
92
+ top: 3,
93
+ left: 3 + clamp01(t) * travel,
94
+ backgroundColor: thumbColor,
95
+ } }) }), onLabel !== undefined ? (_jsx(Text, { className: "text-[9] font-bold tracking-widest", style: { color: on ? "#ECF2E8" : "#6f6e66" }, children: onLabel })) : null] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
96
96
  }
97
97
  /** A Toggle bound to a bool-style APVTS parameter (on = value ≥ 0.5). */
98
98
  export function ParamToggle({ paramId, label, ...rest }) {
@@ -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
- const bar = (_jsxs(View, { className: "flex-row items-center gap-3", children: [_jsx(View, { className: "rounded-full overflow-hidden", style: { width, height, backgroundColor: trackColor }, children: _jsx(View, { className: "rounded-full", style: { width: level * width, height, backgroundColor: color } }) }), showPercent ? (_jsx(Text, { className: "text-faint text-[11] font-bold", style: { width: 34 }, children: `${Math.round(level * 100)}%` })) : null] }));
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 })] }));
@@ -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,4 +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";
36
+ export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
37
+ export type { MacroPadProps, ParamMacroPadProps, HardwareKnobProps, ParamHardwareKnobProps, CrossfaderProps, ParamCrossfaderProps, PulseOrbProps, } from "./specialty";
31
38
  export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
package/dist/index.js CHANGED
@@ -18,3 +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";
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;
@@ -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
+ }
@@ -0,0 +1,109 @@
1
+ export interface MacroPadProps {
2
+ /** Normalized 0..1. */
3
+ x: number;
4
+ /** Normalized 0..1 — 1 is the top. */
5
+ y: number;
6
+ /** Diameter. Default 220. */
7
+ size?: number;
8
+ labelX?: string;
9
+ labelY?: string;
10
+ disabled?: boolean;
11
+ /** Concentric ring count. Default 9. */
12
+ rings?: number;
13
+ /** Freeze the ring motion (reduced-motion, screenshots). */
14
+ animate?: boolean;
15
+ color?: string;
16
+ trackColor?: string;
17
+ onChange: (x: number, y: number) => void;
18
+ onBegin?: () => void;
19
+ onEnd?: () => void;
20
+ }
21
+ /** The centerpiece macro control: a circular 2D pad whose concentric
22
+ rings breathe with the values — x spreads them, y drives their
23
+ intensity. One drag, two parameters, instrument-grade presence. */
24
+ export declare function MacroPad({ x, y, size, labelX, labelY, disabled, rings, animate, color, trackColor, onChange, onBegin, onEnd, }: MacroPadProps): import("react").JSX.Element;
25
+ export interface ParamMacroPadProps {
26
+ paramX: string;
27
+ paramY: string;
28
+ size?: number;
29
+ labelX?: string;
30
+ labelY?: string;
31
+ rings?: number;
32
+ animate?: boolean;
33
+ color?: string;
34
+ trackColor?: string;
35
+ }
36
+ /** A MacroPad driving two host parameters — labels default to their
37
+ names. */
38
+ export declare function ParamMacroPad({ paramX, paramY, labelX, labelY, ...rest }: ParamMacroPadProps): import("react").JSX.Element;
39
+ export interface HardwareKnobProps {
40
+ value: number;
41
+ size?: number;
42
+ label?: string;
43
+ text?: string;
44
+ disabled?: boolean;
45
+ defaultValue?: number;
46
+ wheelSensitivity?: number;
47
+ capColor?: string;
48
+ pointerColor?: string;
49
+ tickColor?: string;
50
+ onChange: (value: number) => void;
51
+ onBegin?: () => void;
52
+ onEnd?: () => void;
53
+ }
54
+ /** The skeuomorphic knob: a dark hardware cap with a glowing pointer
55
+ notch at the rim and a faint tick track — the audio-ui.com look,
56
+ painted natively. */
57
+ export declare function HardwareKnob({ value, size, label, text, disabled, defaultValue, wheelSensitivity, capColor, pointerColor, tickColor, onChange, onBegin, onEnd, }: HardwareKnobProps): import("react").JSX.Element;
58
+ export interface ParamHardwareKnobProps {
59
+ paramId: string;
60
+ size?: number;
61
+ label?: string;
62
+ capColor?: string;
63
+ pointerColor?: string;
64
+ tickColor?: string;
65
+ wheelSensitivity?: number;
66
+ }
67
+ /** A HardwareKnob bound to a host parameter. */
68
+ export declare function ParamHardwareKnob({ paramId, label, ...rest }: ParamHardwareKnobProps): import("react").JSX.Element;
69
+ export interface CrossfaderProps {
70
+ /** 0 = fully start-side (dry), 1 = fully end-side (wet). */
71
+ value: number;
72
+ width?: number;
73
+ height?: number;
74
+ labelStart?: string;
75
+ labelEnd?: string;
76
+ disabled?: boolean;
77
+ trackColor?: string;
78
+ handleColor?: string;
79
+ textColor?: string;
80
+ onChange: (value: number) => void;
81
+ onBegin?: () => void;
82
+ onEnd?: () => void;
83
+ }
84
+ /** The DRY/WET strip: a wide track with a grippy rectangular handle. */
85
+ export declare function Crossfader({ value, width, height, labelStart, labelEnd, disabled, trackColor, handleColor, textColor, onChange, onBegin, onEnd, }: CrossfaderProps): import("react").JSX.Element;
86
+ export interface ParamCrossfaderProps {
87
+ paramId: string;
88
+ width?: number;
89
+ height?: number;
90
+ labelStart?: string;
91
+ labelEnd?: string;
92
+ trackColor?: string;
93
+ handleColor?: string;
94
+ }
95
+ /** A Crossfader bound to a host parameter — the classic mix control. */
96
+ export declare function ParamCrossfader({ paramId, ...rest }: ParamCrossfaderProps): import("react").JSX.Element;
97
+ export interface PulseOrbProps {
98
+ /** Level 0..1 — drives ring intensity and core glow. */
99
+ value: number;
100
+ size?: number;
101
+ color?: string;
102
+ /** Echo ring count. Default 4. */
103
+ rings?: number;
104
+ /** Freeze the motion. */
105
+ animate?: boolean;
106
+ }
107
+ /** A value-reactive orb: a glowing core emitting echo rings — visual
108
+ feedback for levels, activity, or just presence. */
109
+ export declare function PulseOrb({ value, size, color, rings, animate, }: PulseOrbProps): import("react").JSX.Element;