@vsreact/core 0.0.18 → 0.0.19

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/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import "./runtime";
2
2
  import "./bridge";
3
3
  /** The SDK version — stamp it on support dumps and analytics. */
4
- export declare const VERSION = "0.0.18";
4
+ export declare const VERSION = "0.0.19";
5
5
  export { View, Text, Image, TextInput, NativeView } from "./primitives";
6
6
  export type { CommonProps, TextProps, ImageProps, TextInputProps, NativeViewProps, } from "./primitives";
7
7
  export { render, unmount } from "./render";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import "./runtime";
4
4
  import "./bridge";
5
5
  /** The SDK version — stamp it on support dumps and analytics. */
6
- export const VERSION = "0.0.18";
6
+ export const VERSION = "0.0.19";
7
7
  export { View, Text, Image, TextInput, NativeView } from "./primitives";
8
8
  export { render, unmount } from "./render";
9
9
  export { native } from "./native";
@@ -37,10 +37,23 @@ export function useParameter(id) {
37
37
  : { value: 0, text: "", name: id, label: "", defaultValue: 0 };
38
38
  });
39
39
  useEffect(() => native.on("param", (p) => {
40
- if (p?.id === id)
41
- setState((s) => ({ ...s, value: Number(p.value), text: String(p.text ?? "") }));
40
+ if (p?.id !== id)
41
+ return;
42
+ // A malformed event must never poison good state: keep the previous
43
+ // value on non-finite numbers and the previous text when absent.
44
+ setState((s) => {
45
+ const value = Number(p.value);
46
+ return {
47
+ ...s,
48
+ value: Number.isFinite(value) ? value : s.value,
49
+ text: p.text == null ? s.text : String(p.text),
50
+ };
51
+ });
42
52
  }), [id]);
43
53
  const set = useCallback((normalized) => {
54
+ // NaN would round-trip into the APVTS as garbage — drop it here too.
55
+ if (!Number.isFinite(normalized))
56
+ return;
44
57
  const value = Math.min(1, Math.max(0, normalized));
45
58
  setState((s) => ({ ...s, value }));
46
59
  native.call("param:set", { id, value });
package/dist/tw.js CHANGED
@@ -68,6 +68,21 @@ const staticClasses = {
68
68
  "shadow-md": { shadowColor: "#00000066", shadowRadius: 8, shadowOffsetY: 2 },
69
69
  "shadow-lg": { shadowColor: "#00000066", shadowRadius: 12, shadowOffsetY: 4 },
70
70
  "shadow-xl": { shadowColor: "#00000066", shadowRadius: 20, shadowOffsetY: 8 },
71
+ "shadow-inner": { insetShadowColor: "#0000000D", insetShadowRadius: 4, insetShadowOffsetY: 2 },
72
+ "border-t": { borderTopWidth: 1 },
73
+ "border-r": { borderRightWidth: 1 },
74
+ "border-b": { borderBottomWidth: 1 },
75
+ "border-l": { borderLeftWidth: 1 },
76
+ "bg-gradient-to-t": { gradientType: "linear", gradientAngle: 0 },
77
+ "bg-gradient-to-tr": { gradientType: "linear", gradientAngle: 45 },
78
+ "bg-gradient-to-r": { gradientType: "linear", gradientAngle: 90 },
79
+ "bg-gradient-to-br": { gradientType: "linear", gradientAngle: 135 },
80
+ "bg-gradient-to-b": { gradientType: "linear", gradientAngle: 180 },
81
+ "bg-gradient-to-bl": { gradientType: "linear", gradientAngle: 225 },
82
+ "bg-gradient-to-l": { gradientType: "linear", gradientAngle: 270 },
83
+ "bg-gradient-to-tl": { gradientType: "linear", gradientAngle: 315 },
84
+ "bg-gradient-radial": { gradientType: "radial" },
85
+ "bg-gradient-conic": { gradientType: "conic" },
71
86
  };
72
87
  const textSizes = {
73
88
  xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30, "4xl": 36,
@@ -121,6 +136,8 @@ const lengthKeys = {
121
136
  "inset-x": ["left", "right"],
122
137
  "inset-y": ["top", "bottom"],
123
138
  basis: ["flexBasis"],
139
+ "translate-x": ["translateX"],
140
+ "translate-y": ["translateY"],
124
141
  };
125
142
  const warned = new Set();
126
143
  function warnUnknown(cls) {
@@ -214,6 +231,32 @@ function resolveClass(cls) {
214
231
  const color = resolveColor(rest);
215
232
  return color !== undefined ? { backgroundColor: color } : undefined;
216
233
  }
234
+ case "from": {
235
+ const color = resolveColor(rest);
236
+ return color !== undefined ? { gradientFrom: color } : undefined;
237
+ }
238
+ case "via": {
239
+ const color = resolveColor(rest);
240
+ return color !== undefined ? { gradientVia: color } : undefined;
241
+ }
242
+ case "to": {
243
+ const color = resolveColor(rest);
244
+ return color !== undefined ? { gradientTo: color } : undefined;
245
+ }
246
+ case "rotate": {
247
+ // Degrees, literal: rotate-45, -rotate-90, rotate-[10]
248
+ const deg = rest.startsWith("[") ? parseLength(rest) : Number(rest);
249
+ return typeof deg === "number" && Number.isFinite(deg) ? { rotate: negate(deg) } : undefined;
250
+ }
251
+ case "scale": {
252
+ // Percent named scale (scale-95), raw factor arbitrary (scale-[1.25])
253
+ if (rest.startsWith("[")) {
254
+ const factor = parseLength(rest);
255
+ return typeof factor === "number" ? { scale: factor } : undefined;
256
+ }
257
+ const percent = Number(rest);
258
+ return Number.isFinite(percent) ? { scale: percent / 100 } : undefined;
259
+ }
217
260
  case "text": {
218
261
  if (rest in textSizes)
219
262
  return { fontSize: textSizes[rest] };
@@ -229,6 +272,13 @@ function resolveClass(cls) {
229
272
  const n = Number(rest);
230
273
  if (Number.isFinite(n))
231
274
  return { borderWidth: n };
275
+ // Per-side widths (literal px like tw): border-t-2, border-b-[3]
276
+ const side = /^([trbl])-(.+)$/.exec(rest);
277
+ if (side) {
278
+ const key = { t: "borderTopWidth", r: "borderRightWidth", b: "borderBottomWidth", l: "borderLeftWidth" }[side[1]];
279
+ const width = side[2].startsWith("[") ? parseLength(side[2]) : Number(side[2]);
280
+ return typeof width === "number" && Number.isFinite(width) ? { [key]: width } : undefined;
281
+ }
232
282
  const color = resolveColor(rest);
233
283
  return color !== undefined ? { borderColor: color } : undefined;
234
284
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsreact/core",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Write React. Ship native VST. A React renderer for JUCE audio plugins — QuickJS + Yoga + juce::Graphics, no webview.",
5
5
  "keywords": [
6
6
  "react",
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import "./runtime";
4
4
  import "./bridge";
5
5
 
6
6
  /** The SDK version — stamp it on support dumps and analytics. */
7
- export const VERSION = "0.0.18";
7
+ export const VERSION = "0.0.19";
8
8
 
9
9
  export { View, Text, Image, TextInput, NativeView } from "./primitives";
10
10
  export type {
package/src/parameters.ts CHANGED
@@ -68,14 +68,25 @@ export function useParameter(id: string): ParameterHandle {
68
68
  useEffect(
69
69
  () =>
70
70
  native.on("param", (p) => {
71
- if (p?.id === id)
72
- setState((s) => ({ ...s, value: Number(p.value), text: String(p.text ?? "") }));
71
+ if (p?.id !== id) return;
72
+ // A malformed event must never poison good state: keep the previous
73
+ // value on non-finite numbers and the previous text when absent.
74
+ setState((s) => {
75
+ const value = Number(p.value);
76
+ return {
77
+ ...s,
78
+ value: Number.isFinite(value) ? value : s.value,
79
+ text: p.text == null ? s.text : String(p.text),
80
+ };
81
+ });
73
82
  }),
74
83
  [id],
75
84
  );
76
85
 
77
86
  const set = useCallback(
78
87
  (normalized: number) => {
88
+ // NaN would round-trip into the APVTS as garbage — drop it here too.
89
+ if (!Number.isFinite(normalized)) return;
79
90
  const value = Math.min(1, Math.max(0, normalized));
80
91
  setState((s) => ({ ...s, value }));
81
92
  native.call("param:set", { id, value });
@@ -0,0 +1,89 @@
1
+ // 0.0.19 hardening: malformed param traffic must never poison good state.
2
+ import { beforeEach, describe, expect, test } from "bun:test";
3
+
4
+ const batches: unknown[][][] = [];
5
+ const nativeCalls: Array<{ name: string; args: any }> = [];
6
+ let paramGetResult: any = { value: 0.5, text: "320 ms", name: "Time", label: "ms", defaultValue: 0.5 };
7
+
8
+ (globalThis as Record<string, any>).__vsreact_flush = (json: string) => {
9
+ batches.push(JSON.parse(json));
10
+ };
11
+ const defaultNativeCall = (name: string, argsJson: string) => {
12
+ const args = JSON.parse(argsJson);
13
+ nativeCalls.push({ name, args });
14
+ if (name === "param:get") return JSON.stringify(paramGetResult);
15
+ return "null";
16
+ };
17
+ (globalThis as Record<string, any>).__vsreact_nativeCall = defaultNativeCall;
18
+
19
+ import { render, unmount, useParameter, View } from "./index";
20
+ import type { ParameterHandle } from "./index";
21
+
22
+ const dispatch = (msg: unknown) =>
23
+ (globalThis as Record<string, any>).__vsreact_dispatch(JSON.stringify(msg));
24
+
25
+ let handle: ParameterHandle | null = null;
26
+
27
+ function Probe({ id }: { id: string }) {
28
+ handle = useParameter(id);
29
+ return <View />;
30
+ }
31
+
32
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
33
+
34
+ beforeEach(() => {
35
+ unmount();
36
+ batches.length = 0;
37
+ nativeCalls.length = 0;
38
+ handle = null;
39
+ paramGetResult = { value: 0.5, text: "320 ms", name: "Time", label: "ms", defaultValue: 0.5 };
40
+ (globalThis as Record<string, any>).__vsreact_nativeCall = defaultNativeCall;
41
+ });
42
+
43
+ describe("useParameter hardening", () => {
44
+ test("initial state is the synchronous param:get snapshot", () => {
45
+ render(<Probe id="time" />);
46
+ expect(handle!.value).toBe(0.5);
47
+ expect(handle!.text).toBe("320 ms");
48
+ });
49
+
50
+ test("event with missing text keeps the previous text", async () => {
51
+ render(<Probe id="time" />);
52
+ await settle();
53
+
54
+ dispatch({ kind: "native", name: "param", payload: { id: "time", value: 0.75 } });
55
+ expect(handle!.value).toBe(0.75);
56
+ expect(handle!.text).toBe("320 ms");
57
+ });
58
+
59
+ test("event with a non-finite value keeps the previous value", async () => {
60
+ render(<Probe id="time" />);
61
+ await settle();
62
+
63
+ dispatch({ kind: "native", name: "param", payload: { id: "time", value: "wat", text: "junk" } });
64
+ expect(handle!.value).toBe(0.5);
65
+ expect(handle!.text).toBe("junk");
66
+ });
67
+
68
+ test("set(NaN) is dropped before it reaches the native side", async () => {
69
+ render(<Probe id="time" />);
70
+ await settle();
71
+ nativeCalls.length = 0;
72
+
73
+ handle!.set(Number.NaN);
74
+ expect(nativeCalls.filter((c) => c.name === "param:set")).toHaveLength(0);
75
+ expect(handle!.value).toBe(0.5);
76
+
77
+ handle!.set(0.25);
78
+ expect(nativeCalls.filter((c) => c.name === "param:set")).toHaveLength(1);
79
+ });
80
+
81
+ test("healthy events still update both fields", async () => {
82
+ render(<Probe id="time" />);
83
+ await settle();
84
+
85
+ dispatch({ kind: "native", name: "param", payload: { id: "time", value: 0.9, text: "901 ms" } });
86
+ expect(handle!.value).toBe(0.9);
87
+ expect(handle!.text).toBe("901 ms");
88
+ });
89
+ });
package/src/tw.test.ts CHANGED
@@ -167,3 +167,62 @@ describe("arbitrary letter spacing", () => {
167
167
  expect(tw("tracking-widest").style).toEqual({ letterSpacing: 1.6 });
168
168
  });
169
169
  });
170
+
171
+ describe("gradients (0.0.19)", () => {
172
+ test("direction classes set type and angle", () => {
173
+ expect(tw("bg-gradient-to-r").style).toEqual({ gradientType: "linear", gradientAngle: 90 });
174
+ expect(tw("bg-gradient-to-tl").style).toEqual({ gradientType: "linear", gradientAngle: 315 });
175
+ expect(tw("bg-gradient-radial").style).toEqual({ gradientType: "radial" });
176
+ expect(tw("bg-gradient-conic").style).toEqual({ gradientType: "conic" });
177
+ });
178
+
179
+ test("from/via/to resolve palette and arbitrary colors", () => {
180
+ expect(tw("from-zinc-900").style).toEqual({ gradientFrom: "#18181b" });
181
+ expect(tw("via-lime-400").style).toEqual({ gradientVia: "#a3e635" });
182
+ expect(tw("to-[#102030]").style).toEqual({ gradientTo: "#102030" });
183
+ });
184
+
185
+ test("full stack composes", () => {
186
+ expect(tw("bg-gradient-to-b from-[#111111] to-[#222222]").style).toEqual({
187
+ gradientType: "linear",
188
+ gradientAngle: 180,
189
+ gradientFrom: "#111111",
190
+ gradientTo: "#222222",
191
+ });
192
+ });
193
+ });
194
+
195
+ describe("transforms (0.0.19)", () => {
196
+ test("rotate literal degrees + negative + arbitrary", () => {
197
+ expect(tw("rotate-45").style).toEqual({ rotate: 45 });
198
+ expect(tw("-rotate-90").style).toEqual({ rotate: -90 });
199
+ expect(tw("rotate-[10.5]").style).toEqual({ rotate: 10.5 });
200
+ });
201
+
202
+ test("scale percent + arbitrary factor", () => {
203
+ expect(tw("scale-95").style).toEqual({ scale: 0.95 });
204
+ expect(tw("scale-[1.25]").style).toEqual({ scale: 1.25 });
205
+ });
206
+
207
+ test("translate on the spacing scale", () => {
208
+ expect(tw("translate-x-4").style).toEqual({ translateX: 16 });
209
+ expect(tw("-translate-y-2").style).toEqual({ translateY: -8 });
210
+ expect(tw("translate-x-[7]").style).toEqual({ translateX: 7 });
211
+ });
212
+ });
213
+
214
+ describe("per-side borders + inner shadow (0.0.19)", () => {
215
+ test("side widths are literal px", () => {
216
+ expect(tw("border-t").style).toEqual({ borderTopWidth: 1 });
217
+ expect(tw("border-b-2").style).toEqual({ borderBottomWidth: 2 });
218
+ expect(tw("border-l-[3]").style).toEqual({ borderLeftWidth: 3 });
219
+ });
220
+
221
+ test("shadow-inner maps to inset shadow keys", () => {
222
+ expect(tw("shadow-inner").style).toEqual({
223
+ insetShadowColor: "#0000000D",
224
+ insetShadowRadius: 4,
225
+ insetShadowOffsetY: 2,
226
+ });
227
+ });
228
+ });
package/src/tw.ts CHANGED
@@ -79,6 +79,21 @@ const staticClasses: Record<string, Style> = {
79
79
  "shadow-md": { shadowColor: "#00000066", shadowRadius: 8, shadowOffsetY: 2 },
80
80
  "shadow-lg": { shadowColor: "#00000066", shadowRadius: 12, shadowOffsetY: 4 },
81
81
  "shadow-xl": { shadowColor: "#00000066", shadowRadius: 20, shadowOffsetY: 8 },
82
+ "shadow-inner": { insetShadowColor: "#0000000D", insetShadowRadius: 4, insetShadowOffsetY: 2 },
83
+ "border-t": { borderTopWidth: 1 },
84
+ "border-r": { borderRightWidth: 1 },
85
+ "border-b": { borderBottomWidth: 1 },
86
+ "border-l": { borderLeftWidth: 1 },
87
+ "bg-gradient-to-t": { gradientType: "linear", gradientAngle: 0 },
88
+ "bg-gradient-to-tr": { gradientType: "linear", gradientAngle: 45 },
89
+ "bg-gradient-to-r": { gradientType: "linear", gradientAngle: 90 },
90
+ "bg-gradient-to-br": { gradientType: "linear", gradientAngle: 135 },
91
+ "bg-gradient-to-b": { gradientType: "linear", gradientAngle: 180 },
92
+ "bg-gradient-to-bl": { gradientType: "linear", gradientAngle: 225 },
93
+ "bg-gradient-to-l": { gradientType: "linear", gradientAngle: 270 },
94
+ "bg-gradient-to-tl": { gradientType: "linear", gradientAngle: 315 },
95
+ "bg-gradient-radial": { gradientType: "radial" },
96
+ "bg-gradient-conic": { gradientType: "conic" },
82
97
  };
83
98
 
84
99
  const textSizes: Record<string, number> = {
@@ -136,6 +151,8 @@ const lengthKeys: Record<string, string[]> = {
136
151
  "inset-x": ["left", "right"],
137
152
  "inset-y": ["top", "bottom"],
138
153
  basis: ["flexBasis"],
154
+ "translate-x": ["translateX"],
155
+ "translate-y": ["translateY"],
139
156
  };
140
157
 
141
158
  const warned = new Set<string>();
@@ -223,6 +240,32 @@ function resolveClass(cls: string): Style | undefined {
223
240
  const color = resolveColor(rest);
224
241
  return color !== undefined ? { backgroundColor: color } : undefined;
225
242
  }
243
+ case "from": {
244
+ const color = resolveColor(rest);
245
+ return color !== undefined ? { gradientFrom: color } : undefined;
246
+ }
247
+ case "via": {
248
+ const color = resolveColor(rest);
249
+ return color !== undefined ? { gradientVia: color } : undefined;
250
+ }
251
+ case "to": {
252
+ const color = resolveColor(rest);
253
+ return color !== undefined ? { gradientTo: color } : undefined;
254
+ }
255
+ case "rotate": {
256
+ // Degrees, literal: rotate-45, -rotate-90, rotate-[10]
257
+ const deg = rest.startsWith("[") ? parseLength(rest) : Number(rest);
258
+ return typeof deg === "number" && Number.isFinite(deg) ? { rotate: negate(deg) } : undefined;
259
+ }
260
+ case "scale": {
261
+ // Percent named scale (scale-95), raw factor arbitrary (scale-[1.25])
262
+ if (rest.startsWith("[")) {
263
+ const factor = parseLength(rest);
264
+ return typeof factor === "number" ? { scale: factor } : undefined;
265
+ }
266
+ const percent = Number(rest);
267
+ return Number.isFinite(percent) ? { scale: percent / 100 } : undefined;
268
+ }
226
269
  case "text": {
227
270
  if (rest in textSizes) return { fontSize: textSizes[rest] };
228
271
  if (rest.startsWith("[") && !rest.startsWith("[#")) {
@@ -235,6 +278,15 @@ function resolveClass(cls: string): Style | undefined {
235
278
  case "border": {
236
279
  const n = Number(rest);
237
280
  if (Number.isFinite(n)) return { borderWidth: n };
281
+ // Per-side widths (literal px like tw): border-t-2, border-b-[3]
282
+ const side = /^([trbl])-(.+)$/.exec(rest);
283
+ if (side) {
284
+ const key = { t: "borderTopWidth", r: "borderRightWidth", b: "borderBottomWidth", l: "borderLeftWidth" }[
285
+ side[1] as "t" | "r" | "b" | "l"
286
+ ];
287
+ const width = side[2].startsWith("[") ? parseLength(side[2]) : Number(side[2]);
288
+ return typeof width === "number" && Number.isFinite(width) ? { [key]: width } : undefined;
289
+ }
238
290
  const color = resolveColor(rest);
239
291
  return color !== undefined ? { borderColor: color } : undefined;
240
292
  }