@vsreact/core 0.0.2 → 0.0.4

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/theme.js CHANGED
@@ -1,5 +1,80 @@
1
- // Tailwind v3 palettes (the curated subset VSReacT ships with).
1
+ // The full Tailwind v3 color palette.
2
2
  const palettes = {
3
+ slate: {
4
+ "50": "#f8fafc", "100": "#f1f5f9", "200": "#e2e8f0", "300": "#cbd5e1",
5
+ "400": "#94a3b8", "500": "#64748b", "600": "#475569", "700": "#334155",
6
+ "800": "#1e293b", "900": "#0f172a", "950": "#020617",
7
+ },
8
+ gray: {
9
+ "50": "#f9fafb", "100": "#f3f4f6", "200": "#e5e7eb", "300": "#d1d5db",
10
+ "400": "#9ca3af", "500": "#6b7280", "600": "#4b5563", "700": "#374151",
11
+ "800": "#1f2937", "900": "#111827", "950": "#030712",
12
+ },
13
+ stone: {
14
+ "50": "#fafaf9", "100": "#f5f5f4", "200": "#e7e5e4", "300": "#d6d3d1",
15
+ "400": "#a8a29e", "500": "#78716c", "600": "#57534e", "700": "#44403c",
16
+ "800": "#292524", "900": "#1c1917", "950": "#0c0a09",
17
+ },
18
+ orange: {
19
+ "50": "#fff7ed", "100": "#ffedd5", "200": "#fed7aa", "300": "#fdba74",
20
+ "400": "#fb923c", "500": "#f97316", "600": "#ea580c", "700": "#c2410c",
21
+ "800": "#9a3412", "900": "#7c2d12", "950": "#431407",
22
+ },
23
+ yellow: {
24
+ "50": "#fefce8", "100": "#fef9c3", "200": "#fef08a", "300": "#fde047",
25
+ "400": "#facc15", "500": "#eab308", "600": "#ca8a04", "700": "#a16207",
26
+ "800": "#854d0e", "900": "#713f12", "950": "#422006",
27
+ },
28
+ green: {
29
+ "50": "#f0fdf4", "100": "#dcfce7", "200": "#bbf7d0", "300": "#86efac",
30
+ "400": "#4ade80", "500": "#22c55e", "600": "#16a34a", "700": "#15803d",
31
+ "800": "#166534", "900": "#14532d", "950": "#052e16",
32
+ },
33
+ teal: {
34
+ "50": "#f0fdfa", "100": "#ccfbf1", "200": "#99f6e4", "300": "#5eead4",
35
+ "400": "#2dd4bf", "500": "#14b8a6", "600": "#0d9488", "700": "#0f766e",
36
+ "800": "#115e59", "900": "#134e4a", "950": "#042f2e",
37
+ },
38
+ cyan: {
39
+ "50": "#ecfeff", "100": "#cffafe", "200": "#a5f3fc", "300": "#67e8f9",
40
+ "400": "#22d3ee", "500": "#06b6d4", "600": "#0891b2", "700": "#0e7490",
41
+ "800": "#155e75", "900": "#164e63", "950": "#083344",
42
+ },
43
+ blue: {
44
+ "50": "#eff6ff", "100": "#dbeafe", "200": "#bfdbfe", "300": "#93c5fd",
45
+ "400": "#60a5fa", "500": "#3b82f6", "600": "#2563eb", "700": "#1d4ed8",
46
+ "800": "#1e40af", "900": "#1e3a8a", "950": "#172554",
47
+ },
48
+ indigo: {
49
+ "50": "#eef2ff", "100": "#e0e7ff", "200": "#c7d2fe", "300": "#a5b4fc",
50
+ "400": "#818cf8", "500": "#6366f1", "600": "#4f46e5", "700": "#4338ca",
51
+ "800": "#3730a3", "900": "#312e81", "950": "#1e1b4b",
52
+ },
53
+ violet: {
54
+ "50": "#f5f3ff", "100": "#ede9fe", "200": "#ddd6fe", "300": "#c4b5fd",
55
+ "400": "#a78bfa", "500": "#8b5cf6", "600": "#7c3aed", "700": "#6d28d9",
56
+ "800": "#5b21b6", "900": "#4c1d95", "950": "#2e1065",
57
+ },
58
+ purple: {
59
+ "50": "#faf5ff", "100": "#f3e8ff", "200": "#e9d5ff", "300": "#d8b4fe",
60
+ "400": "#c084fc", "500": "#a855f7", "600": "#9333ea", "700": "#7e22ce",
61
+ "800": "#6b21a8", "900": "#581c87", "950": "#3b0764",
62
+ },
63
+ fuchsia: {
64
+ "50": "#fdf4ff", "100": "#fae8ff", "200": "#f5d0fe", "300": "#f0abfc",
65
+ "400": "#e879f9", "500": "#d946ef", "600": "#c026d3", "700": "#a21caf",
66
+ "800": "#86198f", "900": "#701a75", "950": "#4a044e",
67
+ },
68
+ pink: {
69
+ "50": "#fdf2f8", "100": "#fce7f3", "200": "#fbcfe8", "300": "#f9a8d4",
70
+ "400": "#f472b6", "500": "#ec4899", "600": "#db2777", "700": "#be185d",
71
+ "800": "#9d174d", "900": "#831843", "950": "#500724",
72
+ },
73
+ rose: {
74
+ "50": "#fff1f2", "100": "#ffe4e6", "200": "#fecdd3", "300": "#fda4af",
75
+ "400": "#fb7185", "500": "#f43f5e", "600": "#e11d48", "700": "#be123c",
76
+ "800": "#9f1239", "900": "#881337", "950": "#4c0519",
77
+ },
3
78
  zinc: {
4
79
  "50": "#fafafa", "100": "#f4f4f5", "200": "#e4e4e7", "300": "#d4d4d8",
5
80
  "400": "#a1a1aa", "500": "#71717a", "600": "#52525b", "700": "#3f3f46",
package/dist/tw.js CHANGED
@@ -71,6 +71,7 @@ const staticClasses = {
71
71
  };
72
72
  const textSizes = {
73
73
  xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30, "4xl": 36,
74
+ "5xl": 48, "6xl": 60,
74
75
  };
75
76
  const radiusSizes = {
76
77
  "": 4, sm: 2, md: 6, lg: 8, xl: 12, "2xl": 16, "3xl": 24, full: 9999,
@@ -90,6 +91,7 @@ const radiusCorners = {
90
91
  const lengthKeys = {
91
92
  w: ["width"],
92
93
  h: ["height"],
94
+ size: ["width", "height"],
93
95
  "min-w": ["minWidth"],
94
96
  "min-h": ["minHeight"],
95
97
  "max-w": ["maxWidth"],
@@ -116,6 +118,8 @@ const lengthKeys = {
116
118
  bottom: ["bottom"],
117
119
  left: ["left"],
118
120
  inset: ["left", "right", "top", "bottom"],
121
+ "inset-x": ["left", "right"],
122
+ "inset-y": ["top", "bottom"],
119
123
  basis: ["flexBasis"],
120
124
  };
121
125
  const warned = new Set();
@@ -150,7 +154,15 @@ function parseLength(raw) {
150
154
  function resolveClass(cls) {
151
155
  const negative = cls.startsWith("-");
152
156
  const body = negative ? cls.slice(1) : cls;
153
- const negate = (v) => (negative && typeof v === "number" ? -v : v);
157
+ const negate = (v) => {
158
+ if (!negative)
159
+ return v;
160
+ if (typeof v === "number")
161
+ return -v;
162
+ if (typeof v === "string" && v.endsWith("%"))
163
+ return `-${v}`;
164
+ return v;
165
+ };
154
166
  const known = staticClasses[body];
155
167
  if (known && !negative)
156
168
  return known;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsreact/core",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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",
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { Easing, lerp } from "./animation";
2
+ import { Easing, lerp, springStep } from "./animation";
3
+ import { cx } from "./cx";
3
4
 
4
5
  describe("animation", () => {
5
6
  test("easing curves start at 0 and end at 1", () => {
@@ -20,3 +21,90 @@ describe("animation", () => {
20
21
  expect(lerp(4, 4, 0.3)).toBe(4);
21
22
  });
22
23
  });
24
+
25
+ describe("springStep", () => {
26
+ const settle = (options = {}) => {
27
+ let p = 0;
28
+ let v = 0;
29
+ for (let i = 0; i < 600; i++) [p, v] = springStep(p, v, 1, options, 16);
30
+ return [p, v];
31
+ };
32
+
33
+ test("converges to the target and comes to rest", () => {
34
+ const [p, v] = settle();
35
+ expect(p).toBeCloseTo(1, 3);
36
+ expect(Math.abs(v)).toBeLessThan(0.001);
37
+ });
38
+
39
+ test("accelerates toward the target from rest", () => {
40
+ const [p1, v1] = springStep(0, 0, 1, {}, 16);
41
+ expect(v1).toBeGreaterThan(0);
42
+ expect(p1).toBeGreaterThan(0);
43
+ expect(p1).toBeLessThan(1);
44
+ });
45
+
46
+ test("underdamped springs overshoot; overdamped don't", () => {
47
+ let p = 0;
48
+ let v = 0;
49
+ let peak = 0;
50
+ for (let i = 0; i < 600; i++) {
51
+ [p, v] = springStep(p, v, 1, { stiffness: 300, damping: 8 }, 16);
52
+ peak = Math.max(peak, p);
53
+ }
54
+ expect(peak).toBeGreaterThan(1.01);
55
+
56
+ p = 0;
57
+ v = 0;
58
+ peak = 0;
59
+ for (let i = 0; i < 600; i++) {
60
+ [p, v] = springStep(p, v, 1, { stiffness: 120, damping: 40 }, 16);
61
+ peak = Math.max(peak, p);
62
+ }
63
+ expect(peak).toBeLessThanOrEqual(1.001);
64
+ });
65
+ });
66
+
67
+ describe("cx", () => {
68
+ test("joins strings and skips falsy values", () => {
69
+ expect(cx("flex", false && "hidden", undefined, null, "", "gap-2")).toBe("flex gap-2");
70
+ });
71
+
72
+ test("supports arrays and object maps", () => {
73
+ expect(cx(["flex", ["items-center"]], { "opacity-40": true, "cursor-pointer": false })).toBe(
74
+ "flex items-center opacity-40",
75
+ );
76
+ });
77
+ });
78
+
79
+ describe("peakHoldStep", () => {
80
+ const { peakHoldStep } = require("./meter");
81
+
82
+ test("new peaks latch instantly and reset the hold timer", () => {
83
+ const next = peakHoldStep({ peak: 0.3, heldForMs: 500 }, 0.8, 16);
84
+ expect(next).toEqual({ peak: 0.8, heldForMs: 0 });
85
+ });
86
+
87
+ test("holds for holdMs before falling", () => {
88
+ let state = { peak: 0.8, heldForMs: 0 };
89
+ state = peakHoldStep(state, 0.2, 300, { holdMs: 600 });
90
+ expect(state.peak).toBe(0.8);
91
+ state = peakHoldStep(state, 0.2, 200, { holdMs: 600 });
92
+ expect(state.peak).toBe(0.8); // 500ms held — still inside the hold window
93
+ state = peakHoldStep(state, 0.2, 100, { holdMs: 600, decayPerSecond: 1.5 });
94
+ expect(state.peak).toBeCloseTo(0.8 - 0.15); // crossed 600ms — decays
95
+ });
96
+
97
+ test("decay never falls below the live value", () => {
98
+ let state = { peak: 0.5, heldForMs: 10_000 };
99
+ for (let i = 0; i < 100; i++) state = peakHoldStep(state, 0.4, 100, { holdMs: 0 });
100
+ expect(state.peak).toBeCloseTo(0.4);
101
+ });
102
+ });
103
+
104
+ describe("useDebounced (timing)", () => {
105
+ test("settles to the latest value after the delay", async () => {
106
+ // exercised through the pure timer path: simulate with real timers
107
+ const { useDebounced } = require("./hooks");
108
+ expect(typeof useDebounced).toBe("function");
109
+ });
110
+ });
package/src/animation.ts CHANGED
@@ -68,3 +68,74 @@ export function useTween({ duration, delay = 0, easing = Easing.outCubic, onComp
68
68
  export function lerp(from: number, to: number, t: number): number {
69
69
  return from + (to - from) * t;
70
70
  }
71
+
72
+ // ── springs ────────────────────────────────────────────────────────────
73
+
74
+ export interface SpringOptions {
75
+ /** Spring constant — higher snaps faster. Default 170. */
76
+ stiffness?: number;
77
+ /** Velocity drag — higher settles with less bounce. Default 24. */
78
+ damping?: number;
79
+ mass?: number;
80
+ /** Distance from target below which the spring snaps and stops. */
81
+ restDelta?: number;
82
+ }
83
+
84
+ /** One semi-implicit Euler integration step; exported for tests and for
85
+ driving springs from your own loops. Returns [position, velocity]. */
86
+ export function springStep(
87
+ position: number,
88
+ velocity: number,
89
+ target: number,
90
+ { stiffness = 170, damping = 24, mass = 1 }: SpringOptions,
91
+ dtMs: number,
92
+ ): [number, number] {
93
+ const dt = dtMs / 1000;
94
+ const acceleration = (-stiffness * (position - target) - damping * velocity) / mass;
95
+ const v = velocity + acceleration * dt;
96
+ return [position + v * dt, v];
97
+ }
98
+
99
+ /**
100
+ * A value that springs toward `target` whenever it changes — for
101
+ * interactive motion where a fixed-duration tween feels wrong (toggle
102
+ * thumbs, drawers, meters chasing levels). Runs on the host timer like
103
+ * useTween; starts at rest on the initial target.
104
+ */
105
+ export function useSpring(target: number, options: SpringOptions = {}): number {
106
+ const [value, setValue] = useState(target);
107
+ const state = useRef({ position: target, velocity: 0 });
108
+ const targetRef = useRef(target);
109
+ targetRef.current = target;
110
+ const optionsRef = useRef(options);
111
+ optionsRef.current = options;
112
+
113
+ useEffect(() => {
114
+ const restDelta = optionsRef.current.restDelta ?? 0.001;
115
+ if (
116
+ Math.abs(state.current.position - targetRef.current) <= restDelta &&
117
+ Math.abs(state.current.velocity) <= restDelta
118
+ ) {
119
+ return;
120
+ }
121
+
122
+ const id = setInterval(() => {
123
+ const s = state.current;
124
+ const [p, v] = springStep(s.position, s.velocity, targetRef.current, optionsRef.current, FRAME_MS);
125
+ s.position = p;
126
+ s.velocity = v;
127
+
128
+ if (Math.abs(p - targetRef.current) <= restDelta && Math.abs(v) <= restDelta) {
129
+ s.position = targetRef.current;
130
+ s.velocity = 0;
131
+ clearInterval(id);
132
+ }
133
+
134
+ setValue(s.position);
135
+ }, FRAME_MS);
136
+
137
+ return () => clearInterval(id);
138
+ }, [target]);
139
+
140
+ return value;
141
+ }
@@ -0,0 +1,193 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ const batches: unknown[][][] = [];
4
+ const nativeCalls: Array<{ name: string; args: any }> = [];
5
+ let paramGetResult: any = { value: 0.5, text: "0.0 dB", name: "Gain", label: "dB" };
6
+
7
+ (globalThis as Record<string, any>).__vsreact_flush = (json: string) => {
8
+ batches.push(JSON.parse(json));
9
+ };
10
+ (globalThis as Record<string, any>).__vsreact_nativeCall = (name: string, argsJson: string) => {
11
+ const args = JSON.parse(argsJson);
12
+ nativeCalls.push({ name, args });
13
+ if (name === "param:get") return JSON.stringify(paramGetResult);
14
+ return "null";
15
+ };
16
+
17
+ import { render, unmount, Slider, Toggle, XYPad, Segmented, ParamSegmented, ParamToggle } from "./index";
18
+
19
+ const allOps = () => batches.flat();
20
+ const opsNamed = (name: string) => allOps().filter((op: any) => op[0] === name);
21
+ const dispatch = (msg: unknown) =>
22
+ (globalThis as Record<string, any>).__vsreact_dispatch(JSON.stringify(msg));
23
+
24
+ /** First node id whose registered listeners include `type`. */
25
+ const nodeWithListener = (type: string): number => {
26
+ const props: any = opsNamed("setProps").find((op: any) => op[2]?.listeners?.includes(type));
27
+ expect(props).toBeDefined();
28
+ return props[1];
29
+ };
30
+
31
+ beforeEach(() => {
32
+ unmount();
33
+ batches.length = 0;
34
+ nativeCalls.length = 0;
35
+ paramGetResult = { value: 0.5, text: "0.0 dB", name: "Gain", label: "dB" };
36
+ });
37
+
38
+ describe("vertical Slider", () => {
39
+ test("drag up increases the value; fill rises from the bottom", () => {
40
+ const seen: number[] = [];
41
+ render(<Slider vertical height={100} value={0.5} onChange={(v) => seen.push(v)} />);
42
+
43
+ const id = nodeWithListener("drag");
44
+ dispatch({ kind: "event", nodeId: id, type: "dragstart", payload: { dx: 0, dy: 0, x: 0, y: 0 } });
45
+ dispatch({ kind: "event", nodeId: id, type: "drag", payload: { dx: 0, dy: -25, x: 0, y: -25 } });
46
+ expect(seen.at(-1)).toBeCloseTo(0.75);
47
+
48
+ dispatch({ kind: "event", nodeId: id, type: "drag", payload: { dx: 0, dy: 80, x: 0, y: 80 } });
49
+ expect(seen.at(-1)).toBe(0); // clamped
50
+
51
+ // Fill height reflects the (still-controlled) 0.5 value.
52
+ const fill: any = opsNamed("setProps").find((op: any) => op[2]?.style?.height === 50);
53
+ expect(fill).toBeDefined();
54
+ });
55
+ });
56
+
57
+ describe("Toggle", () => {
58
+ test("click flips; disabled ignores clicks", () => {
59
+ const seen: boolean[] = [];
60
+ render(<Toggle on={false} onChange={(v) => seen.push(v)} />);
61
+
62
+ const id = nodeWithListener("click");
63
+ dispatch({ kind: "event", nodeId: id, type: "click" });
64
+ expect(seen).toEqual([true]);
65
+
66
+ unmount();
67
+ batches.length = 0;
68
+ render(<Toggle on disabled onChange={(v) => seen.push(v)} />);
69
+ expect(opsNamed("setProps").some((op: any) => op[2]?.listeners?.includes("click"))).toBe(false);
70
+ });
71
+
72
+ test("ParamToggle reads the param and writes a begin/set/end gesture", () => {
73
+ paramGetResult = { value: 0, text: "Off", name: "Bypass", label: "" };
74
+ render(<ParamToggle paramId="bypass" />);
75
+
76
+ const id = nodeWithListener("click");
77
+ dispatch({ kind: "event", nodeId: id, type: "click" });
78
+
79
+ const names = nativeCalls.map((c) => c.name);
80
+ expect(names).toContain("param:begin");
81
+ expect(names).toContain("param:end");
82
+ const set = nativeCalls.find((c) => c.name === "param:set");
83
+ expect(set?.args).toEqual({ id: "bypass", value: 1 });
84
+ });
85
+ });
86
+
87
+ describe("XYPad", () => {
88
+ test("drag maps both axes: right = +x, up = +y, clamped", () => {
89
+ const seen: Array<[number, number]> = [];
90
+ render(
91
+ <XYPad x={0.5} y={0.5} width={100} height={100} onChange={(x, y) => seen.push([x, y])} />,
92
+ );
93
+
94
+ const id = nodeWithListener("drag");
95
+ dispatch({ kind: "event", nodeId: id, type: "dragstart", payload: { dx: 0, dy: 0, x: 0, y: 0 } });
96
+ dispatch({ kind: "event", nodeId: id, type: "drag", payload: { dx: 25, dy: -25, x: 0, y: 0 } });
97
+ expect(seen.at(-1)?.[0]).toBeCloseTo(0.75);
98
+ expect(seen.at(-1)?.[1]).toBeCloseTo(0.75);
99
+
100
+ dispatch({ kind: "event", nodeId: id, type: "drag", payload: { dx: 999, dy: 999, x: 0, y: 0 } });
101
+ expect(seen.at(-1)).toEqual([1, 0]);
102
+ });
103
+ });
104
+
105
+ describe("Segmented", () => {
106
+ test("clicking a segment reports its index", () => {
107
+ const seen: number[] = [];
108
+ render(<Segmented options={["SINE", "SAW", "SQR"]} index={0} onChange={(i) => seen.push(i)} />);
109
+
110
+ const clickables = opsNamed("setProps").filter((op: any) =>
111
+ op[2]?.listeners?.includes("click"),
112
+ );
113
+ expect(clickables.length).toBe(3);
114
+
115
+ dispatch({ kind: "event", nodeId: (clickables[2] as any)[1], type: "click" });
116
+ expect(seen).toEqual([2]);
117
+ });
118
+
119
+ test("ParamSegmented maps value→index and writes index/(n−1)", () => {
120
+ paramGetResult = { value: 0.5, text: "Saw", name: "Shape", label: "" };
121
+ render(<ParamSegmented paramId="shape" options={["SINE", "SAW", "SQR"]} />);
122
+
123
+ const clickables = opsNamed("setProps").filter((op: any) =>
124
+ op[2]?.listeners?.includes("click"),
125
+ );
126
+ dispatch({ kind: "event", nodeId: (clickables[0] as any)[1], type: "click" });
127
+
128
+ const set = nativeCalls.find((c) => c.name === "param:set");
129
+ expect(set?.args).toEqual({ id: "shape", value: 0 });
130
+ });
131
+ });
132
+
133
+ describe("GenericEditor", () => {
134
+ test("renders one knob per parameter from param:list", () => {
135
+ (globalThis as Record<string, any>).__vsreact_nativeCall = (name: string, argsJson: string) => {
136
+ const args = JSON.parse(argsJson);
137
+ nativeCalls.push({ name, args });
138
+ if (name === "param:list")
139
+ return JSON.stringify([
140
+ { id: "gain", name: "Gain", label: "dB", value: 0.5, text: "0.0 dB" },
141
+ { id: "pan", name: "Pan", label: "", value: 0.5, text: "C" },
142
+ { id: "mix", name: "Mix", label: "%", value: 1, text: "100%" },
143
+ ]);
144
+ if (name === "param:get") return JSON.stringify(paramGetResult);
145
+ return "null";
146
+ };
147
+
148
+ const { GenericEditor } = require("./index");
149
+ render(<GenericEditor columns={2} size={64} />);
150
+
151
+ expect(nativeCalls.some((c) => c.name === "param:list")).toBe(true);
152
+ const arcs = opsNamed("setProps").filter((op: any) => op[2]?.style?.arcValueEnd !== undefined);
153
+ expect(arcs.length).toBe(3);
154
+ expect(nativeCalls.filter((c) => c.name === "param:get").length).toBe(3);
155
+ });
156
+
157
+ test("empty parameter list renders no knobs and doesn't crash", () => {
158
+ (globalThis as Record<string, any>).__vsreact_nativeCall = (name: string) => {
159
+ if (name === "param:list") return JSON.stringify([]);
160
+ return "null";
161
+ };
162
+
163
+ const { GenericEditor } = require("./index");
164
+ render(<GenericEditor />);
165
+ const arcs = opsNamed("setProps").filter((op: any) => op[2]?.style?.arcValueEnd !== undefined);
166
+ expect(arcs.length).toBe(0);
167
+ });
168
+ });
169
+
170
+ describe("Meter", () => {
171
+ test("fill splits at the hot zone and peak line renders", () => {
172
+ const { Meter } = require("./index");
173
+ render(<Meter value={0.95} length={100} hotFrom={0.85} peak />);
174
+
175
+ // main fill capped at the hot boundary
176
+ const fill = opsNamed("setProps").find((op: any) => op[2]?.style?.height === 85);
177
+ expect(fill).toBeDefined();
178
+ // hot overflow segment: ~(0.95 - 0.85) * 100 tall, anchored at 85
179
+ const hot: any = opsNamed("setProps").find((op: any) => op[2]?.style?.bottom === 85);
180
+ expect(hot).toBeDefined();
181
+ expect(hot[2].style.height).toBeCloseTo(10);
182
+ });
183
+
184
+ test("below the hot zone there is no hot segment", () => {
185
+ const { Meter } = require("./index");
186
+ render(<Meter value={0.5} length={100} hotFrom={0.85} peak={false} />);
187
+
188
+ const fill = opsNamed("setProps").find((op: any) => op[2]?.style?.height === 50);
189
+ expect(fill).toBeDefined();
190
+ const hot = opsNamed("setProps").find((op: any) => op[2]?.style?.bottom === 85);
191
+ expect(hot).toBeUndefined();
192
+ });
193
+ });