@vsreact/core 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation.d.ts +38 -0
- package/dist/animation.js +90 -0
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.js +49 -0
- package/dist/controls.d.ts +126 -0
- package/dist/controls.js +130 -0
- package/dist/cx.d.ts +6 -0
- package/dist/cx.js +26 -0
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.js +15 -0
- package/dist/hostConfig.d.ts +44 -0
- package/dist/hostConfig.js +118 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +13 -0
- package/dist/native.d.ts +6 -0
- package/dist/native.js +16 -0
- package/dist/parameters.d.ts +12 -0
- package/dist/parameters.js +33 -0
- package/dist/primitives.d.ts +70 -0
- package/dist/primitives.js +20 -0
- package/dist/render.d.ts +5 -0
- package/dist/render.js +22 -0
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +68 -0
- package/dist/theme.d.ts +5 -0
- package/dist/theme.js +156 -0
- package/dist/tw.d.ts +13 -0
- package/dist/tw.js +264 -0
- package/package.json +31 -4
- package/src/animation.test.tsx +56 -1
- package/src/animation.ts +71 -0
- package/src/controls.test.tsx +131 -0
- package/src/controls.tsx +352 -19
- package/src/cx.ts +33 -0
- package/src/hooks.ts +18 -0
- package/src/index.ts +50 -24
- package/src/runtime.ts +4 -1
- package/src/theme.ts +76 -1
- package/src/tw.test.ts +26 -0
- package/src/tw.ts +4 -0
package/src/controls.tsx
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// The built-in VST controls — knobs, sliders, toggles, XY pads, segmented
|
|
2
|
+
// switches — drawn by the VSReacT painter and driven by drag gestures.
|
|
3
|
+
// Each has a Param* variant bound to an APVTS parameter.
|
|
3
4
|
|
|
4
5
|
import { useRef } from "react";
|
|
5
6
|
import { View, Text } from "./primitives";
|
|
6
7
|
import { useParameter } from "./parameters";
|
|
8
|
+
import { useSpring } from "./animation";
|
|
9
|
+
|
|
10
|
+
const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
|
|
7
11
|
|
|
8
12
|
/** Vertical-drag-to-value mapping shared by Knob (and tested in isolation). */
|
|
9
13
|
export function dragToValue(startValue: number, dy: number, sensitivity = 0.005): number {
|
|
10
|
-
return
|
|
14
|
+
return clamp01(startValue - dy * sensitivity);
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
const ARC_START = -135;
|
|
@@ -110,7 +114,12 @@ export function ParamKnob({ paramId, label, size, trackColor, valueColor }: Para
|
|
|
110
114
|
|
|
111
115
|
export interface SliderProps {
|
|
112
116
|
value: number; // normalized 0..1
|
|
117
|
+
/** Track length when horizontal (default 160). */
|
|
113
118
|
width?: number;
|
|
119
|
+
/** Track length when vertical (default 160). */
|
|
120
|
+
height?: number;
|
|
121
|
+
/** Vertical fader — drag up for more, fill rises from the bottom. */
|
|
122
|
+
vertical?: boolean;
|
|
114
123
|
label?: string;
|
|
115
124
|
disabled?: boolean;
|
|
116
125
|
trackColor?: string;
|
|
@@ -123,6 +132,8 @@ export interface SliderProps {
|
|
|
123
132
|
export function Slider({
|
|
124
133
|
value,
|
|
125
134
|
width = 160,
|
|
135
|
+
height = 160,
|
|
136
|
+
vertical,
|
|
126
137
|
label,
|
|
127
138
|
disabled,
|
|
128
139
|
trackColor = "#2A2F27",
|
|
@@ -132,27 +143,54 @@ export function Slider({
|
|
|
132
143
|
onEnd,
|
|
133
144
|
}: SliderProps) {
|
|
134
145
|
const startValue = useRef(0);
|
|
135
|
-
const clamped =
|
|
146
|
+
const clamped = clamp01(value);
|
|
147
|
+
|
|
148
|
+
const onDragStart = disabled
|
|
149
|
+
? undefined
|
|
150
|
+
: () => {
|
|
151
|
+
startValue.current = clamped;
|
|
152
|
+
onBegin?.();
|
|
153
|
+
};
|
|
154
|
+
const onDragEnd = disabled ? undefined : () => onEnd?.();
|
|
155
|
+
|
|
156
|
+
if (vertical) {
|
|
157
|
+
return (
|
|
158
|
+
<View className="items-center gap-2">
|
|
159
|
+
<View
|
|
160
|
+
className={`w-[18] relative ${disabled ? "opacity-40" : "cursor-pointer"}`}
|
|
161
|
+
style={{ height }}
|
|
162
|
+
onDragStart={onDragStart}
|
|
163
|
+
onDrag={disabled ? undefined : (e) => onChange(clamp01(startValue.current - e.dy / height))}
|
|
164
|
+
onDragEnd={onDragEnd}
|
|
165
|
+
>
|
|
166
|
+
<View
|
|
167
|
+
className="absolute w-[4] rounded-full left-[7] top-0 bottom-0"
|
|
168
|
+
style={{ backgroundColor: trackColor }}
|
|
169
|
+
/>
|
|
170
|
+
<View
|
|
171
|
+
className="absolute w-[4] rounded-full left-[7] bottom-0"
|
|
172
|
+
style={{ height: clamped * height, backgroundColor: valueColor }}
|
|
173
|
+
/>
|
|
174
|
+
<View
|
|
175
|
+
className="absolute w-[12] h-[12] rounded-full left-[3]"
|
|
176
|
+
style={{ top: (1 - clamped) * (height - 12), backgroundColor: valueColor }}
|
|
177
|
+
/>
|
|
178
|
+
</View>
|
|
179
|
+
{label !== undefined ? (
|
|
180
|
+
<Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
|
|
181
|
+
) : null}
|
|
182
|
+
</View>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
136
185
|
|
|
137
186
|
return (
|
|
138
187
|
<View className="gap-2">
|
|
139
188
|
<View
|
|
140
189
|
className={`h-[18] justify-center relative ${disabled ? "opacity-40" : "cursor-pointer"}`}
|
|
141
190
|
style={{ width }}
|
|
142
|
-
onDragStart={
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: () => {
|
|
146
|
-
startValue.current = clamped;
|
|
147
|
-
onBegin?.();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
onDrag={
|
|
151
|
-
disabled
|
|
152
|
-
? undefined
|
|
153
|
-
: (e) => onChange(Math.min(1, Math.max(0, startValue.current + e.dx / width)))
|
|
154
|
-
}
|
|
155
|
-
onDragEnd={disabled ? undefined : () => onEnd?.()}
|
|
191
|
+
onDragStart={onDragStart}
|
|
192
|
+
onDrag={disabled ? undefined : (e) => onChange(clamp01(startValue.current + e.dx / width))}
|
|
193
|
+
onDragEnd={onDragEnd}
|
|
156
194
|
>
|
|
157
195
|
<View className="h-[4] rounded-full" style={{ backgroundColor: trackColor }} />
|
|
158
196
|
<View
|
|
@@ -175,10 +213,12 @@ export interface ParamSliderProps {
|
|
|
175
213
|
paramId: string;
|
|
176
214
|
label?: string;
|
|
177
215
|
width?: number;
|
|
216
|
+
height?: number;
|
|
217
|
+
vertical?: boolean;
|
|
178
218
|
}
|
|
179
219
|
|
|
180
220
|
/** A Slider bound to an APVTS parameter (via ParameterBridge). */
|
|
181
|
-
export function ParamSlider({ paramId, label, width }: ParamSliderProps) {
|
|
221
|
+
export function ParamSlider({ paramId, label, width, height, vertical }: ParamSliderProps) {
|
|
182
222
|
const param = useParameter(paramId);
|
|
183
223
|
|
|
184
224
|
return (
|
|
@@ -186,9 +226,302 @@ export function ParamSlider({ paramId, label, width }: ParamSliderProps) {
|
|
|
186
226
|
value={param.value}
|
|
187
227
|
label={label ?? param.name.toUpperCase()}
|
|
188
228
|
width={width}
|
|
229
|
+
height={height}
|
|
230
|
+
vertical={vertical}
|
|
189
231
|
onChange={param.set}
|
|
190
232
|
onBegin={param.begin}
|
|
191
233
|
onEnd={param.end}
|
|
192
234
|
/>
|
|
193
235
|
);
|
|
194
236
|
}
|
|
237
|
+
|
|
238
|
+
// ── Toggle ─────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
export interface ToggleProps {
|
|
241
|
+
on: boolean;
|
|
242
|
+
label?: string;
|
|
243
|
+
/** Track height; width is 1.8×. Default 22. */
|
|
244
|
+
size?: number;
|
|
245
|
+
disabled?: boolean;
|
|
246
|
+
trackColor?: string;
|
|
247
|
+
onColor?: string;
|
|
248
|
+
thumbColor?: string;
|
|
249
|
+
onChange: (on: boolean) => void;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** A switch with a spring-animated thumb — bypass, on/off, A/B. */
|
|
253
|
+
export function Toggle({
|
|
254
|
+
on,
|
|
255
|
+
label,
|
|
256
|
+
size = 22,
|
|
257
|
+
disabled,
|
|
258
|
+
trackColor = "#2A2F27",
|
|
259
|
+
onColor = "#C6F135",
|
|
260
|
+
thumbColor = "#F4F4F5",
|
|
261
|
+
onChange,
|
|
262
|
+
}: ToggleProps) {
|
|
263
|
+
const t = useSpring(on ? 1 : 0, { stiffness: 300, damping: 28 });
|
|
264
|
+
const trackWidth = Math.round(size * 1.8);
|
|
265
|
+
const thumb = size - 6;
|
|
266
|
+
const travel = trackWidth - thumb - 6;
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<View className="items-center gap-2">
|
|
270
|
+
<View
|
|
271
|
+
className={`relative rounded-full ${disabled ? "opacity-40" : "cursor-pointer"}`}
|
|
272
|
+
style={{ width: trackWidth, height: size, backgroundColor: on ? onColor : trackColor }}
|
|
273
|
+
onClick={disabled ? undefined : () => onChange(!on)}
|
|
274
|
+
>
|
|
275
|
+
<View
|
|
276
|
+
className="absolute rounded-full"
|
|
277
|
+
style={{
|
|
278
|
+
width: thumb,
|
|
279
|
+
height: thumb,
|
|
280
|
+
top: 3,
|
|
281
|
+
left: 3 + clamp01(t) * travel,
|
|
282
|
+
backgroundColor: thumbColor,
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
</View>
|
|
286
|
+
{label !== undefined ? (
|
|
287
|
+
<Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
|
|
288
|
+
) : null}
|
|
289
|
+
</View>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface ParamToggleProps {
|
|
294
|
+
paramId: string;
|
|
295
|
+
label?: string;
|
|
296
|
+
size?: number;
|
|
297
|
+
trackColor?: string;
|
|
298
|
+
onColor?: string;
|
|
299
|
+
thumbColor?: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** A Toggle bound to a bool-style APVTS parameter (on = value ≥ 0.5). */
|
|
303
|
+
export function ParamToggle({ paramId, label, ...rest }: ParamToggleProps) {
|
|
304
|
+
const param = useParameter(paramId);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<Toggle
|
|
308
|
+
on={param.value >= 0.5}
|
|
309
|
+
label={label ?? param.name.toUpperCase()}
|
|
310
|
+
onChange={(next) => {
|
|
311
|
+
param.begin();
|
|
312
|
+
param.set(next ? 1 : 0);
|
|
313
|
+
param.end();
|
|
314
|
+
}}
|
|
315
|
+
{...rest}
|
|
316
|
+
/>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── XYPad ──────────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
export interface XYPadProps {
|
|
323
|
+
/** Normalized 0..1. */
|
|
324
|
+
x: number;
|
|
325
|
+
/** Normalized 0..1 — 1 is the TOP of the pad (drag up for more). */
|
|
326
|
+
y: number;
|
|
327
|
+
width?: number;
|
|
328
|
+
height?: number;
|
|
329
|
+
label?: string;
|
|
330
|
+
disabled?: boolean;
|
|
331
|
+
trackColor?: string;
|
|
332
|
+
valueColor?: string;
|
|
333
|
+
onChange: (x: number, y: number) => void;
|
|
334
|
+
onBegin?: () => void;
|
|
335
|
+
onEnd?: () => void;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** A 2D drag pad with crosshair — filter cutoff/resonance, pan/depth… */
|
|
339
|
+
export function XYPad({
|
|
340
|
+
x,
|
|
341
|
+
y,
|
|
342
|
+
width = 160,
|
|
343
|
+
height = 160,
|
|
344
|
+
label,
|
|
345
|
+
disabled,
|
|
346
|
+
trackColor = "#161B17",
|
|
347
|
+
valueColor = "#C6F135",
|
|
348
|
+
onChange,
|
|
349
|
+
onBegin,
|
|
350
|
+
onEnd,
|
|
351
|
+
}: XYPadProps) {
|
|
352
|
+
const start = useRef({ x: 0, y: 0 });
|
|
353
|
+
const cxv = clamp01(x);
|
|
354
|
+
const cyv = clamp01(y);
|
|
355
|
+
const thumb = 14;
|
|
356
|
+
const tx = cxv * (width - thumb);
|
|
357
|
+
const ty = (1 - cyv) * (height - thumb);
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<View className="items-center gap-2">
|
|
361
|
+
<View
|
|
362
|
+
className={`relative rounded-lg overflow-hidden border ${disabled ? "opacity-40" : "cursor-pointer"}`}
|
|
363
|
+
style={{ width, height, backgroundColor: trackColor, borderColor: "#00000055" }}
|
|
364
|
+
onDragStart={
|
|
365
|
+
disabled
|
|
366
|
+
? undefined
|
|
367
|
+
: () => {
|
|
368
|
+
start.current = { x: cxv, y: cyv };
|
|
369
|
+
onBegin?.();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
onDrag={
|
|
373
|
+
disabled
|
|
374
|
+
? undefined
|
|
375
|
+
: (e) =>
|
|
376
|
+
onChange(
|
|
377
|
+
clamp01(start.current.x + e.dx / width),
|
|
378
|
+
clamp01(start.current.y - e.dy / height),
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
onDragEnd={disabled ? undefined : () => onEnd?.()}
|
|
382
|
+
>
|
|
383
|
+
<View
|
|
384
|
+
className="absolute left-0 right-0 h-[1]"
|
|
385
|
+
style={{ top: ty + thumb / 2, backgroundColor: valueColor, opacity: 0.35 }}
|
|
386
|
+
/>
|
|
387
|
+
<View
|
|
388
|
+
className="absolute top-0 bottom-0 w-[1]"
|
|
389
|
+
style={{ left: tx + thumb / 2, backgroundColor: valueColor, opacity: 0.35 }}
|
|
390
|
+
/>
|
|
391
|
+
<View
|
|
392
|
+
className="absolute rounded-full"
|
|
393
|
+
style={{ width: thumb, height: thumb, left: tx, top: ty, backgroundColor: valueColor }}
|
|
394
|
+
/>
|
|
395
|
+
</View>
|
|
396
|
+
{label !== undefined ? (
|
|
397
|
+
<Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
|
|
398
|
+
) : null}
|
|
399
|
+
</View>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export interface ParamXYPadProps {
|
|
404
|
+
paramX: string;
|
|
405
|
+
paramY: string;
|
|
406
|
+
width?: number;
|
|
407
|
+
height?: number;
|
|
408
|
+
label?: string;
|
|
409
|
+
trackColor?: string;
|
|
410
|
+
valueColor?: string;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** An XYPad driving two APVTS parameters at once. */
|
|
414
|
+
export function ParamXYPad({ paramX, paramY, label, ...rest }: ParamXYPadProps) {
|
|
415
|
+
const px = useParameter(paramX);
|
|
416
|
+
const py = useParameter(paramY);
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<XYPad
|
|
420
|
+
x={px.value}
|
|
421
|
+
y={py.value}
|
|
422
|
+
label={label ?? `${px.name} / ${py.name}`.toUpperCase()}
|
|
423
|
+
onChange={(nx, ny) => {
|
|
424
|
+
px.set(nx);
|
|
425
|
+
py.set(ny);
|
|
426
|
+
}}
|
|
427
|
+
onBegin={() => {
|
|
428
|
+
px.begin();
|
|
429
|
+
py.begin();
|
|
430
|
+
}}
|
|
431
|
+
onEnd={() => {
|
|
432
|
+
px.end();
|
|
433
|
+
py.end();
|
|
434
|
+
}}
|
|
435
|
+
{...rest}
|
|
436
|
+
/>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Segmented ──────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
export interface SegmentedProps {
|
|
443
|
+
options: string[];
|
|
444
|
+
index: number;
|
|
445
|
+
label?: string;
|
|
446
|
+
disabled?: boolean;
|
|
447
|
+
trackColor?: string;
|
|
448
|
+
activeColor?: string;
|
|
449
|
+
textColor?: string;
|
|
450
|
+
activeTextColor?: string;
|
|
451
|
+
onChange: (index: number) => void;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** A row of exclusive options — oscillator shapes, filter modes, A/B/C. */
|
|
455
|
+
export function Segmented({
|
|
456
|
+
options,
|
|
457
|
+
index,
|
|
458
|
+
label,
|
|
459
|
+
disabled,
|
|
460
|
+
trackColor = "#2A2F27",
|
|
461
|
+
activeColor = "#C6F135",
|
|
462
|
+
textColor = "#a1a1aa",
|
|
463
|
+
activeTextColor = "#09090b",
|
|
464
|
+
onChange,
|
|
465
|
+
}: SegmentedProps) {
|
|
466
|
+
const current = Math.min(options.length - 1, Math.max(0, index));
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<View className="items-center gap-2">
|
|
470
|
+
<View
|
|
471
|
+
className={`flex-row rounded-lg p-[3] gap-[3] ${disabled ? "opacity-40" : ""}`}
|
|
472
|
+
style={{ backgroundColor: trackColor }}
|
|
473
|
+
>
|
|
474
|
+
{options.map((option, i) => (
|
|
475
|
+
<View
|
|
476
|
+
key={`${option}-${i}`}
|
|
477
|
+
className={`px-3 py-[6] rounded-md ${disabled ? "" : "cursor-pointer"}`}
|
|
478
|
+
style={{ backgroundColor: i === current ? activeColor : "#00000000" }}
|
|
479
|
+
onClick={disabled ? undefined : () => onChange(i)}
|
|
480
|
+
>
|
|
481
|
+
<Text
|
|
482
|
+
className="text-[11] font-bold tracking-wide"
|
|
483
|
+
style={{ color: i === current ? activeTextColor : textColor }}
|
|
484
|
+
>
|
|
485
|
+
{option}
|
|
486
|
+
</Text>
|
|
487
|
+
</View>
|
|
488
|
+
))}
|
|
489
|
+
</View>
|
|
490
|
+
{label !== undefined ? (
|
|
491
|
+
<Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
|
|
492
|
+
) : null}
|
|
493
|
+
</View>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export interface ParamSegmentedProps {
|
|
498
|
+
paramId: string;
|
|
499
|
+
options: string[];
|
|
500
|
+
label?: string;
|
|
501
|
+
trackColor?: string;
|
|
502
|
+
activeColor?: string;
|
|
503
|
+
textColor?: string;
|
|
504
|
+
activeTextColor?: string;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** A Segmented bound to a choice-style APVTS parameter: the normalized
|
|
508
|
+
value maps to an option index (index / (count − 1)). */
|
|
509
|
+
export function ParamSegmented({ paramId, options, label, ...rest }: ParamSegmentedProps) {
|
|
510
|
+
const param = useParameter(paramId);
|
|
511
|
+
const count = Math.max(1, options.length);
|
|
512
|
+
const index = Math.round(param.value * (count - 1));
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
<Segmented
|
|
516
|
+
options={options}
|
|
517
|
+
index={index}
|
|
518
|
+
label={label ?? param.name.toUpperCase()}
|
|
519
|
+
onChange={(i) => {
|
|
520
|
+
param.begin();
|
|
521
|
+
param.set(count <= 1 ? 0 : i / (count - 1));
|
|
522
|
+
param.end();
|
|
523
|
+
}}
|
|
524
|
+
{...rest}
|
|
525
|
+
/>
|
|
526
|
+
);
|
|
527
|
+
}
|
package/src/cx.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// cx — compose conditional className strings (a tiny clsx).
|
|
2
|
+
|
|
3
|
+
export type ClassValue =
|
|
4
|
+
| string
|
|
5
|
+
| number
|
|
6
|
+
| null
|
|
7
|
+
| false
|
|
8
|
+
| undefined
|
|
9
|
+
| ClassValue[]
|
|
10
|
+
| Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `cx("flex", active && "bg-red-500", { "opacity-40": disabled })`
|
|
14
|
+
* → `"flex bg-red-500 opacity-40"`.
|
|
15
|
+
*/
|
|
16
|
+
export function cx(...inputs: ClassValue[]): string {
|
|
17
|
+
const out: string[] = [];
|
|
18
|
+
|
|
19
|
+
for (const input of inputs) {
|
|
20
|
+
if (!input && input !== 0) continue;
|
|
21
|
+
|
|
22
|
+
if (typeof input === "string" || typeof input === "number") {
|
|
23
|
+
out.push(String(input));
|
|
24
|
+
} else if (Array.isArray(input)) {
|
|
25
|
+
const nested = cx(...input);
|
|
26
|
+
if (nested) out.push(nested);
|
|
27
|
+
} else if (typeof input === "object") {
|
|
28
|
+
for (const [key, value] of Object.entries(input)) if (value) out.push(key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return out.join(" ");
|
|
33
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Convenience hooks over the native bridge.
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { native } from "./native";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Subscribes to a C++ event (RootView::sendNativeEvent) for the lifetime
|
|
8
|
+
* of the component. The handler can close over fresh state — it is kept
|
|
9
|
+
* current without resubscribing.
|
|
10
|
+
*
|
|
11
|
+
* useNativeEvent("download:progress", (p) => setProgress(p.ratio));
|
|
12
|
+
*/
|
|
13
|
+
export function useNativeEvent(name: string, handler: (payload: any) => void): void {
|
|
14
|
+
const handlerRef = useRef(handler);
|
|
15
|
+
handlerRef.current = handler;
|
|
16
|
+
|
|
17
|
+
useEffect(() => native.on(name, (payload) => handlerRef.current(payload)), [name]);
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
1
|
-
// vsreact public API. runtime must load first — it installs the
|
|
2
|
-
// timer/console/microtask shims react depends on inside QuickJS.
|
|
3
|
-
import "./runtime";
|
|
4
|
-
import "./bridge";
|
|
5
|
-
|
|
6
|
-
export { View, Text, Image, TextInput, NativeView } from "./primitives";
|
|
7
|
-
export type {
|
|
8
|
-
CommonProps,
|
|
9
|
-
TextProps,
|
|
10
|
-
ImageProps,
|
|
11
|
-
TextInputProps,
|
|
12
|
-
NativeViewProps,
|
|
13
|
-
} from "./primitives";
|
|
14
|
-
export { render, unmount } from "./render";
|
|
15
|
-
export { native } from "./native";
|
|
16
|
-
export {
|
|
17
|
-
export
|
|
18
|
-
export {
|
|
19
|
-
export
|
|
20
|
-
export {
|
|
21
|
-
export
|
|
22
|
-
export {
|
|
23
|
-
export
|
|
24
|
-
export type {
|
|
1
|
+
// @vsreact/core public API. runtime must load first — it installs the
|
|
2
|
+
// timer/console/microtask shims react depends on inside QuickJS.
|
|
3
|
+
import "./runtime";
|
|
4
|
+
import "./bridge";
|
|
5
|
+
|
|
6
|
+
export { View, Text, Image, TextInput, NativeView } from "./primitives";
|
|
7
|
+
export type {
|
|
8
|
+
CommonProps,
|
|
9
|
+
TextProps,
|
|
10
|
+
ImageProps,
|
|
11
|
+
TextInputProps,
|
|
12
|
+
NativeViewProps,
|
|
13
|
+
} from "./primitives";
|
|
14
|
+
export { render, unmount } from "./render";
|
|
15
|
+
export { native } from "./native";
|
|
16
|
+
export { useNativeEvent } from "./hooks";
|
|
17
|
+
export { configureTheme, tw } from "./tw";
|
|
18
|
+
export type { Style, ResolvedClasses } from "./tw";
|
|
19
|
+
export { cx } from "./cx";
|
|
20
|
+
export type { ClassValue } from "./cx";
|
|
21
|
+
export { useTween, useSpring, springStep, lerp, Easing } from "./animation";
|
|
22
|
+
export type { TweenOptions, SpringOptions, EasingFn } from "./animation";
|
|
23
|
+
export { useParameter } from "./parameters";
|
|
24
|
+
export type { ParameterState, ParameterHandle } from "./parameters";
|
|
25
|
+
export {
|
|
26
|
+
Knob,
|
|
27
|
+
Slider,
|
|
28
|
+
Toggle,
|
|
29
|
+
XYPad,
|
|
30
|
+
Segmented,
|
|
31
|
+
ParamKnob,
|
|
32
|
+
ParamSlider,
|
|
33
|
+
ParamToggle,
|
|
34
|
+
ParamXYPad,
|
|
35
|
+
ParamSegmented,
|
|
36
|
+
dragToValue,
|
|
37
|
+
} from "./controls";
|
|
38
|
+
export type {
|
|
39
|
+
KnobProps,
|
|
40
|
+
SliderProps,
|
|
41
|
+
ToggleProps,
|
|
42
|
+
XYPadProps,
|
|
43
|
+
SegmentedProps,
|
|
44
|
+
ParamKnobProps,
|
|
45
|
+
ParamSliderProps,
|
|
46
|
+
ParamToggleProps,
|
|
47
|
+
ParamXYPadProps,
|
|
48
|
+
ParamSegmentedProps,
|
|
49
|
+
} from "./controls";
|
|
50
|
+
export type { DragEventPayload } from "./primitives";
|
package/src/runtime.ts
CHANGED
|
@@ -83,7 +83,10 @@ if (isHosted) {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
g.performance ??= { now: () => Date.now() } as Performance;
|
|
86
|
-
|
|
86
|
+
// Typed loosely on purpose: the dist build has no Node/Bun types, and the
|
|
87
|
+
// shim only exists for libraries that probe NODE_ENV.
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
g.process ??= { env: { NODE_ENV: "production" } } as any;
|
|
87
90
|
g.self ??= g as unknown as Window & typeof globalThis;
|
|
88
91
|
g.global ??= g;
|
|
89
92
|
}
|
package/src/theme.ts
CHANGED
|
@@ -1,7 +1,82 @@
|
|
|
1
1
|
export type ThemeColors = Record<string, string>;
|
|
2
2
|
|
|
3
|
-
// Tailwind v3
|
|
3
|
+
// The full Tailwind v3 color palette.
|
|
4
4
|
const palettes: Record<string, Record<string, string>> = {
|
|
5
|
+
slate: {
|
|
6
|
+
"50": "#f8fafc", "100": "#f1f5f9", "200": "#e2e8f0", "300": "#cbd5e1",
|
|
7
|
+
"400": "#94a3b8", "500": "#64748b", "600": "#475569", "700": "#334155",
|
|
8
|
+
"800": "#1e293b", "900": "#0f172a", "950": "#020617",
|
|
9
|
+
},
|
|
10
|
+
gray: {
|
|
11
|
+
"50": "#f9fafb", "100": "#f3f4f6", "200": "#e5e7eb", "300": "#d1d5db",
|
|
12
|
+
"400": "#9ca3af", "500": "#6b7280", "600": "#4b5563", "700": "#374151",
|
|
13
|
+
"800": "#1f2937", "900": "#111827", "950": "#030712",
|
|
14
|
+
},
|
|
15
|
+
stone: {
|
|
16
|
+
"50": "#fafaf9", "100": "#f5f5f4", "200": "#e7e5e4", "300": "#d6d3d1",
|
|
17
|
+
"400": "#a8a29e", "500": "#78716c", "600": "#57534e", "700": "#44403c",
|
|
18
|
+
"800": "#292524", "900": "#1c1917", "950": "#0c0a09",
|
|
19
|
+
},
|
|
20
|
+
orange: {
|
|
21
|
+
"50": "#fff7ed", "100": "#ffedd5", "200": "#fed7aa", "300": "#fdba74",
|
|
22
|
+
"400": "#fb923c", "500": "#f97316", "600": "#ea580c", "700": "#c2410c",
|
|
23
|
+
"800": "#9a3412", "900": "#7c2d12", "950": "#431407",
|
|
24
|
+
},
|
|
25
|
+
yellow: {
|
|
26
|
+
"50": "#fefce8", "100": "#fef9c3", "200": "#fef08a", "300": "#fde047",
|
|
27
|
+
"400": "#facc15", "500": "#eab308", "600": "#ca8a04", "700": "#a16207",
|
|
28
|
+
"800": "#854d0e", "900": "#713f12", "950": "#422006",
|
|
29
|
+
},
|
|
30
|
+
green: {
|
|
31
|
+
"50": "#f0fdf4", "100": "#dcfce7", "200": "#bbf7d0", "300": "#86efac",
|
|
32
|
+
"400": "#4ade80", "500": "#22c55e", "600": "#16a34a", "700": "#15803d",
|
|
33
|
+
"800": "#166534", "900": "#14532d", "950": "#052e16",
|
|
34
|
+
},
|
|
35
|
+
teal: {
|
|
36
|
+
"50": "#f0fdfa", "100": "#ccfbf1", "200": "#99f6e4", "300": "#5eead4",
|
|
37
|
+
"400": "#2dd4bf", "500": "#14b8a6", "600": "#0d9488", "700": "#0f766e",
|
|
38
|
+
"800": "#115e59", "900": "#134e4a", "950": "#042f2e",
|
|
39
|
+
},
|
|
40
|
+
cyan: {
|
|
41
|
+
"50": "#ecfeff", "100": "#cffafe", "200": "#a5f3fc", "300": "#67e8f9",
|
|
42
|
+
"400": "#22d3ee", "500": "#06b6d4", "600": "#0891b2", "700": "#0e7490",
|
|
43
|
+
"800": "#155e75", "900": "#164e63", "950": "#083344",
|
|
44
|
+
},
|
|
45
|
+
blue: {
|
|
46
|
+
"50": "#eff6ff", "100": "#dbeafe", "200": "#bfdbfe", "300": "#93c5fd",
|
|
47
|
+
"400": "#60a5fa", "500": "#3b82f6", "600": "#2563eb", "700": "#1d4ed8",
|
|
48
|
+
"800": "#1e40af", "900": "#1e3a8a", "950": "#172554",
|
|
49
|
+
},
|
|
50
|
+
indigo: {
|
|
51
|
+
"50": "#eef2ff", "100": "#e0e7ff", "200": "#c7d2fe", "300": "#a5b4fc",
|
|
52
|
+
"400": "#818cf8", "500": "#6366f1", "600": "#4f46e5", "700": "#4338ca",
|
|
53
|
+
"800": "#3730a3", "900": "#312e81", "950": "#1e1b4b",
|
|
54
|
+
},
|
|
55
|
+
violet: {
|
|
56
|
+
"50": "#f5f3ff", "100": "#ede9fe", "200": "#ddd6fe", "300": "#c4b5fd",
|
|
57
|
+
"400": "#a78bfa", "500": "#8b5cf6", "600": "#7c3aed", "700": "#6d28d9",
|
|
58
|
+
"800": "#5b21b6", "900": "#4c1d95", "950": "#2e1065",
|
|
59
|
+
},
|
|
60
|
+
purple: {
|
|
61
|
+
"50": "#faf5ff", "100": "#f3e8ff", "200": "#e9d5ff", "300": "#d8b4fe",
|
|
62
|
+
"400": "#c084fc", "500": "#a855f7", "600": "#9333ea", "700": "#7e22ce",
|
|
63
|
+
"800": "#6b21a8", "900": "#581c87", "950": "#3b0764",
|
|
64
|
+
},
|
|
65
|
+
fuchsia: {
|
|
66
|
+
"50": "#fdf4ff", "100": "#fae8ff", "200": "#f5d0fe", "300": "#f0abfc",
|
|
67
|
+
"400": "#e879f9", "500": "#d946ef", "600": "#c026d3", "700": "#a21caf",
|
|
68
|
+
"800": "#86198f", "900": "#701a75", "950": "#4a044e",
|
|
69
|
+
},
|
|
70
|
+
pink: {
|
|
71
|
+
"50": "#fdf2f8", "100": "#fce7f3", "200": "#fbcfe8", "300": "#f9a8d4",
|
|
72
|
+
"400": "#f472b6", "500": "#ec4899", "600": "#db2777", "700": "#be185d",
|
|
73
|
+
"800": "#9d174d", "900": "#831843", "950": "#500724",
|
|
74
|
+
},
|
|
75
|
+
rose: {
|
|
76
|
+
"50": "#fff1f2", "100": "#ffe4e6", "200": "#fecdd3", "300": "#fda4af",
|
|
77
|
+
"400": "#fb7185", "500": "#f43f5e", "600": "#e11d48", "700": "#be123c",
|
|
78
|
+
"800": "#9f1239", "900": "#881337", "950": "#4c0519",
|
|
79
|
+
},
|
|
5
80
|
zinc: {
|
|
6
81
|
"50": "#fafafa", "100": "#f4f4f5", "200": "#e4e4e7", "300": "#d4d4d8",
|
|
7
82
|
"400": "#a1a1aa", "500": "#71717a", "600": "#52525b", "700": "#3f3f46",
|
package/src/tw.test.ts
CHANGED
|
@@ -111,3 +111,29 @@ describe("tw resolver", () => {
|
|
|
111
111
|
expect(r.style.backgroundColor).toBe("#27272a");
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("tw 0.0.3 additions", () => {
|
|
116
|
+
test("full Tailwind palette resolves", () => {
|
|
117
|
+
expect(tw("bg-blue-500").style.backgroundColor).toBe("#3b82f6");
|
|
118
|
+
expect(tw("text-rose-400").style.color).toBe("#fb7185");
|
|
119
|
+
expect(tw("border-slate-700").style.borderColor).toBe("#334155");
|
|
120
|
+
expect(tw("bg-violet-950").style.backgroundColor).toBe("#2e1065");
|
|
121
|
+
expect(tw("bg-teal-500/50").style.backgroundColor).toBe("#14b8a680");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("size-* sets width and height together", () => {
|
|
125
|
+
expect(tw("size-10").style).toEqual({ width: 40, height: 40 });
|
|
126
|
+
expect(tw("size-[13]").style).toEqual({ width: 13, height: 13 });
|
|
127
|
+
expect(tw("size-full").style).toEqual({ width: "100%", height: "100%" });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("inset-x / inset-y", () => {
|
|
131
|
+
expect(tw("inset-x-2").style).toEqual({ left: 8, right: 8 });
|
|
132
|
+
expect(tw("inset-y-0").style).toEqual({ top: 0, bottom: 0 });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("larger text sizes", () => {
|
|
136
|
+
expect(tw("text-5xl").style.fontSize).toBe(48);
|
|
137
|
+
expect(tw("text-6xl").style.fontSize).toBe(60);
|
|
138
|
+
});
|
|
139
|
+
});
|