@vsreact/core 0.0.9 → 0.0.11
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/components.d.ts +6 -0
- package/dist/components.js +3 -0
- package/dist/controls.d.ts +10 -4
- package/dist/controls.js +20 -14
- package/dist/feedback.d.ts +24 -0
- package/dist/feedback.js +31 -0
- package/dist/fields.d.ts +79 -0
- package/dist/fields.js +73 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/specialty.d.ts +109 -0
- package/dist/specialty.js +186 -0
- package/package.json +1 -1
- package/src/components.ts +37 -0
- package/src/controls.test.tsx +231 -0
- package/src/controls.tsx +100 -25
- package/src/feedback.tsx +99 -0
- package/src/fields.tsx +288 -0
- package/src/index.ts +37 -0
- package/src/specialty.tsx +514 -0
package/src/feedback.tsx
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Feedback components — determinate progress and the indeterminate
|
|
2
|
+
// spinner, both painted natively.
|
|
3
|
+
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { View, Text } from "./primitives";
|
|
6
|
+
import { useInterval } from "./hooks";
|
|
7
|
+
|
|
8
|
+
const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
|
|
9
|
+
|
|
10
|
+
export interface ProgressBarProps {
|
|
11
|
+
/** 0..1. */
|
|
12
|
+
value: number;
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
/** Renders "42%" alongside the bar. Default false. */
|
|
16
|
+
showPercent?: boolean;
|
|
17
|
+
trackColor?: string;
|
|
18
|
+
color?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A determinate progress bar — downloads, renders, analysis passes. */
|
|
23
|
+
export function ProgressBar({
|
|
24
|
+
value,
|
|
25
|
+
width = 200,
|
|
26
|
+
height = 8,
|
|
27
|
+
showPercent = false,
|
|
28
|
+
trackColor = "#2A2F27",
|
|
29
|
+
color = "#C6F135",
|
|
30
|
+
label,
|
|
31
|
+
}: ProgressBarProps) {
|
|
32
|
+
const level = clamp01(value);
|
|
33
|
+
|
|
34
|
+
const bar = (
|
|
35
|
+
<View className="flex-row items-center gap-3">
|
|
36
|
+
<View
|
|
37
|
+
className="rounded-full overflow-hidden"
|
|
38
|
+
style={{ width, height, backgroundColor: trackColor }}
|
|
39
|
+
>
|
|
40
|
+
<View
|
|
41
|
+
className="rounded-full"
|
|
42
|
+
style={{ width: level * width, height, backgroundColor: color }}
|
|
43
|
+
/>
|
|
44
|
+
</View>
|
|
45
|
+
{showPercent ? (
|
|
46
|
+
<Text className="text-faint text-[11] font-bold" style={{ width: 34 }}>
|
|
47
|
+
{`${Math.round(level * 100)}%`}
|
|
48
|
+
</Text>
|
|
49
|
+
) : null}
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (label === undefined) return bar;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<View className="items-center gap-2">
|
|
57
|
+
{bar}
|
|
58
|
+
<Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SpinnerProps {
|
|
64
|
+
/** Diameter. Default 28. */
|
|
65
|
+
size?: number;
|
|
66
|
+
thickness?: number;
|
|
67
|
+
color?: string;
|
|
68
|
+
trackColor?: string;
|
|
69
|
+
/** Degrees per 16ms tick. Default 9 (~ one turn / 0.64s). */
|
|
70
|
+
speed?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An indeterminate spinner — a 100° arc chasing its own tail. */
|
|
74
|
+
export function Spinner({
|
|
75
|
+
size = 28,
|
|
76
|
+
thickness,
|
|
77
|
+
color = "#C6F135",
|
|
78
|
+
trackColor = "#FFFFFF14",
|
|
79
|
+
speed = 9,
|
|
80
|
+
}: SpinnerProps) {
|
|
81
|
+
const [angle, setAngle] = useState(0);
|
|
82
|
+
useInterval(() => setAngle((a) => (a + speed) % 360), 16);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View
|
|
86
|
+
style={{
|
|
87
|
+
width: size,
|
|
88
|
+
height: size,
|
|
89
|
+
arcTrackColor: trackColor,
|
|
90
|
+
arcColor: color,
|
|
91
|
+
arcStart: 0,
|
|
92
|
+
arcEnd: 360,
|
|
93
|
+
arcValueStart: angle,
|
|
94
|
+
arcValueEnd: angle + 100,
|
|
95
|
+
arcThickness: thickness ?? Math.max(2.5, size * 0.11),
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
package/src/fields.tsx
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// Field controls — the number box, checkbox, and radio group. Settings
|
|
2
|
+
// panels and fine-value entry, with the same DAW conventions as the
|
|
3
|
+
// knobs: drag, wheel, double-click reset, automation-safe Param twins.
|
|
4
|
+
|
|
5
|
+
import { useRef } from "react";
|
|
6
|
+
import { View, Text } from "./primitives";
|
|
7
|
+
import { useParameter } from "./parameters";
|
|
8
|
+
|
|
9
|
+
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));
|
|
10
|
+
|
|
11
|
+
/** Snap onto the step grid without float dust. */
|
|
12
|
+
export function snapToStep(value: number, step: number, min: number): number {
|
|
13
|
+
const steps = Math.round((value - min) / step);
|
|
14
|
+
return Number((min + steps * step).toPrecision(12));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NumberBoxProps {
|
|
18
|
+
value: number;
|
|
19
|
+
min?: number;
|
|
20
|
+
max?: number;
|
|
21
|
+
/** Value change per step; drags move ~a step per 4px. Default 1. */
|
|
22
|
+
step?: number;
|
|
23
|
+
/** Renders the value. Default: trimmed to 2 decimals. */
|
|
24
|
+
format?: (value: number) => string;
|
|
25
|
+
label?: string;
|
|
26
|
+
width?: number;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/** Double-click resets to this. */
|
|
29
|
+
defaultValue?: number;
|
|
30
|
+
trackColor?: string;
|
|
31
|
+
textColor?: string;
|
|
32
|
+
onChange: (value: number) => void;
|
|
33
|
+
onBegin?: () => void;
|
|
34
|
+
onEnd?: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The draggable number — BPM, milliseconds, semitones. Drag vertically,
|
|
38
|
+
wheel to step, double-click to reset. */
|
|
39
|
+
export function NumberBox({
|
|
40
|
+
value,
|
|
41
|
+
min = 0,
|
|
42
|
+
max = 100,
|
|
43
|
+
step = 1,
|
|
44
|
+
format = (v) => String(Math.round(v * 100) / 100),
|
|
45
|
+
label,
|
|
46
|
+
width = 84,
|
|
47
|
+
disabled,
|
|
48
|
+
defaultValue,
|
|
49
|
+
trackColor = "#2A2F27",
|
|
50
|
+
textColor = "#ECF2E8",
|
|
51
|
+
onChange,
|
|
52
|
+
onBegin,
|
|
53
|
+
onEnd,
|
|
54
|
+
}: NumberBoxProps) {
|
|
55
|
+
const startValue = useRef(0);
|
|
56
|
+
const clamped = clamp(value, min, max);
|
|
57
|
+
|
|
58
|
+
const commit = (raw: number) => onChange(clamp(snapToStep(raw, step, min), min, max));
|
|
59
|
+
|
|
60
|
+
const nudge = (raw: number) => {
|
|
61
|
+
onBegin?.();
|
|
62
|
+
commit(raw);
|
|
63
|
+
onEnd?.();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<View className="items-center gap-2">
|
|
68
|
+
<View
|
|
69
|
+
className={`items-center rounded-lg border px-3 py-[7] ${disabled ? "opacity-40" : "cursor-pointer"}`}
|
|
70
|
+
style={{ width, backgroundColor: trackColor, borderColor: "#00000055" }}
|
|
71
|
+
onDragStart={
|
|
72
|
+
disabled
|
|
73
|
+
? undefined
|
|
74
|
+
: () => {
|
|
75
|
+
startValue.current = clamped;
|
|
76
|
+
onBegin?.();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
onDrag={disabled ? undefined : (e) => commit(startValue.current - (e.dy * step) / 4)}
|
|
80
|
+
onDragEnd={disabled ? undefined : () => onEnd?.()}
|
|
81
|
+
onDoubleClick={
|
|
82
|
+
disabled || defaultValue === undefined ? undefined : () => nudge(defaultValue)
|
|
83
|
+
}
|
|
84
|
+
onWheel={
|
|
85
|
+
disabled ? undefined : (e) => nudge(clamped + (e.dy > 0 ? step : -step))
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<Text className="text-[13] font-bold" style={{ color: textColor }}>
|
|
89
|
+
{format(clamped)}
|
|
90
|
+
</Text>
|
|
91
|
+
</View>
|
|
92
|
+
{label !== undefined ? (
|
|
93
|
+
<Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
|
|
94
|
+
) : null}
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ParamNumberBoxProps {
|
|
100
|
+
paramId: string;
|
|
101
|
+
label?: string;
|
|
102
|
+
width?: number;
|
|
103
|
+
/** Normalized step per wheel notch / drag unit. Default 0.01. */
|
|
104
|
+
step?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A NumberBox over a host parameter: shows the host's formatted text,
|
|
108
|
+
drags the normalized value, double-click resets to the default. */
|
|
109
|
+
export function ParamNumberBox({ paramId, label, width, step = 0.01 }: ParamNumberBoxProps) {
|
|
110
|
+
const param = useParameter(paramId);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<NumberBox
|
|
114
|
+
value={param.value}
|
|
115
|
+
min={0}
|
|
116
|
+
max={1}
|
|
117
|
+
step={step}
|
|
118
|
+
format={() => param.text}
|
|
119
|
+
label={label ?? param.name.toUpperCase()}
|
|
120
|
+
width={width}
|
|
121
|
+
defaultValue={param.defaultValue}
|
|
122
|
+
onChange={param.set}
|
|
123
|
+
onBegin={param.begin}
|
|
124
|
+
onEnd={param.end}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface CheckboxProps {
|
|
130
|
+
checked: boolean;
|
|
131
|
+
label?: string;
|
|
132
|
+
disabled?: boolean;
|
|
133
|
+
/** Box edge length. Default 16. */
|
|
134
|
+
size?: number;
|
|
135
|
+
accentColor?: string;
|
|
136
|
+
boxColor?: string;
|
|
137
|
+
textColor?: string;
|
|
138
|
+
onChange: (checked: boolean) => void;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A checkbox row — settings panels, option lists. */
|
|
142
|
+
export function Checkbox({
|
|
143
|
+
checked,
|
|
144
|
+
label,
|
|
145
|
+
disabled,
|
|
146
|
+
size = 16,
|
|
147
|
+
accentColor = "#C6F135",
|
|
148
|
+
boxColor = "#2A2F27",
|
|
149
|
+
textColor = "#d4d4d8",
|
|
150
|
+
onChange,
|
|
151
|
+
}: CheckboxProps) {
|
|
152
|
+
return (
|
|
153
|
+
<View
|
|
154
|
+
className={`flex-row items-center gap-2 ${disabled ? "opacity-40" : "cursor-pointer"}`}
|
|
155
|
+
onClick={disabled ? undefined : () => onChange(!checked)}
|
|
156
|
+
>
|
|
157
|
+
<View
|
|
158
|
+
className="items-center justify-center rounded-[4] border"
|
|
159
|
+
style={{
|
|
160
|
+
width: size,
|
|
161
|
+
height: size,
|
|
162
|
+
backgroundColor: checked ? accentColor : boxColor,
|
|
163
|
+
borderColor: "#00000066",
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{checked ? (
|
|
167
|
+
<Text className="font-bold" style={{ fontSize: size * 0.7, color: "#09090b" }}>
|
|
168
|
+
✓
|
|
169
|
+
</Text>
|
|
170
|
+
) : null}
|
|
171
|
+
</View>
|
|
172
|
+
{label !== undefined ? (
|
|
173
|
+
<Text className="text-[12]" style={{ color: textColor }}>
|
|
174
|
+
{label}
|
|
175
|
+
</Text>
|
|
176
|
+
) : null}
|
|
177
|
+
</View>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface ParamCheckboxProps {
|
|
182
|
+
paramId: string;
|
|
183
|
+
label?: string;
|
|
184
|
+
size?: number;
|
|
185
|
+
accentColor?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A Checkbox bound to a bool-style parameter (checked = value ≥ 0.5). */
|
|
189
|
+
export function ParamCheckbox({ paramId, label, ...rest }: ParamCheckboxProps) {
|
|
190
|
+
const param = useParameter(paramId);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<Checkbox
|
|
194
|
+
checked={param.value >= 0.5}
|
|
195
|
+
label={label ?? param.name}
|
|
196
|
+
onChange={(next) => {
|
|
197
|
+
param.begin();
|
|
198
|
+
param.set(next ? 1 : 0);
|
|
199
|
+
param.end();
|
|
200
|
+
}}
|
|
201
|
+
{...rest}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface RadioGroupProps {
|
|
207
|
+
options: string[];
|
|
208
|
+
index: number;
|
|
209
|
+
disabled?: boolean;
|
|
210
|
+
/** Row spacing. Default 8. */
|
|
211
|
+
gap?: number;
|
|
212
|
+
accentColor?: string;
|
|
213
|
+
dotColor?: string;
|
|
214
|
+
textColor?: string;
|
|
215
|
+
activeTextColor?: string;
|
|
216
|
+
onChange: (index: number) => void;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Vertical exclusive options with dots — the settings-panel sibling of
|
|
220
|
+
<Segmented>. */
|
|
221
|
+
export function RadioGroup({
|
|
222
|
+
options,
|
|
223
|
+
index,
|
|
224
|
+
disabled,
|
|
225
|
+
gap = 8,
|
|
226
|
+
accentColor = "#C6F135",
|
|
227
|
+
dotColor = "#2A2F27",
|
|
228
|
+
textColor = "#a1a1aa",
|
|
229
|
+
activeTextColor = "#ECF2E8",
|
|
230
|
+
onChange,
|
|
231
|
+
}: RadioGroupProps) {
|
|
232
|
+
const current = Math.min(options.length - 1, Math.max(0, index));
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<View className={disabled ? "opacity-40" : ""} style={{ rowGap: gap }}>
|
|
236
|
+
{options.map((option, i) => (
|
|
237
|
+
<View
|
|
238
|
+
key={`${option}-${i}`}
|
|
239
|
+
className={`flex-row items-center gap-2 ${disabled ? "" : "cursor-pointer"}`}
|
|
240
|
+
onClick={disabled ? undefined : () => onChange(i)}
|
|
241
|
+
>
|
|
242
|
+
<View
|
|
243
|
+
className="items-center justify-center rounded-full border"
|
|
244
|
+
style={{ width: 15, height: 15, backgroundColor: dotColor, borderColor: "#00000066" }}
|
|
245
|
+
>
|
|
246
|
+
{i === current ? (
|
|
247
|
+
<View
|
|
248
|
+
className="rounded-full"
|
|
249
|
+
style={{ width: 7, height: 7, backgroundColor: accentColor }}
|
|
250
|
+
/>
|
|
251
|
+
) : null}
|
|
252
|
+
</View>
|
|
253
|
+
<Text className="text-[12]" style={{ color: i === current ? activeTextColor : textColor }}>
|
|
254
|
+
{option}
|
|
255
|
+
</Text>
|
|
256
|
+
</View>
|
|
257
|
+
))}
|
|
258
|
+
</View>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface ParamRadioGroupProps {
|
|
263
|
+
paramId: string;
|
|
264
|
+
options: string[];
|
|
265
|
+
gap?: number;
|
|
266
|
+
accentColor?: string;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** A RadioGroup bound to a choice-style parameter (value ↔ index, like
|
|
270
|
+
ParamSegmented). */
|
|
271
|
+
export function ParamRadioGroup({ paramId, options, ...rest }: ParamRadioGroupProps) {
|
|
272
|
+
const param = useParameter(paramId);
|
|
273
|
+
const count = Math.max(1, options.length);
|
|
274
|
+
const index = Math.round(param.value * (count - 1));
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<RadioGroup
|
|
278
|
+
options={options}
|
|
279
|
+
index={index}
|
|
280
|
+
onChange={(i) => {
|
|
281
|
+
param.begin();
|
|
282
|
+
param.set(count <= 1 ? 0 : i / (count - 1));
|
|
283
|
+
param.end();
|
|
284
|
+
}}
|
|
285
|
+
{...rest}
|
|
286
|
+
/>
|
|
287
|
+
);
|
|
288
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -71,4 +71,41 @@ export type {
|
|
|
71
71
|
} from "./controls";
|
|
72
72
|
export { Meter, usePeakHold, peakHoldStep } from "./meter";
|
|
73
73
|
export type { MeterProps, PeakHoldOptions, PeakHoldState } from "./meter";
|
|
74
|
+
export {
|
|
75
|
+
NumberBox,
|
|
76
|
+
Checkbox,
|
|
77
|
+
RadioGroup,
|
|
78
|
+
ParamNumberBox,
|
|
79
|
+
ParamCheckbox,
|
|
80
|
+
ParamRadioGroup,
|
|
81
|
+
snapToStep,
|
|
82
|
+
} from "./fields";
|
|
83
|
+
export type {
|
|
84
|
+
NumberBoxProps,
|
|
85
|
+
CheckboxProps,
|
|
86
|
+
RadioGroupProps,
|
|
87
|
+
ParamNumberBoxProps,
|
|
88
|
+
ParamCheckboxProps,
|
|
89
|
+
ParamRadioGroupProps,
|
|
90
|
+
} from "./fields";
|
|
91
|
+
export { ProgressBar, Spinner } from "./feedback";
|
|
92
|
+
export type { ProgressBarProps, SpinnerProps } from "./feedback";
|
|
93
|
+
export {
|
|
94
|
+
MacroPad,
|
|
95
|
+
ParamMacroPad,
|
|
96
|
+
HardwareKnob,
|
|
97
|
+
ParamHardwareKnob,
|
|
98
|
+
Crossfader,
|
|
99
|
+
ParamCrossfader,
|
|
100
|
+
PulseOrb,
|
|
101
|
+
} from "./specialty";
|
|
102
|
+
export type {
|
|
103
|
+
MacroPadProps,
|
|
104
|
+
ParamMacroPadProps,
|
|
105
|
+
HardwareKnobProps,
|
|
106
|
+
ParamHardwareKnobProps,
|
|
107
|
+
CrossfaderProps,
|
|
108
|
+
ParamCrossfaderProps,
|
|
109
|
+
PulseOrbProps,
|
|
110
|
+
} from "./specialty";
|
|
74
111
|
export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
|