@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/src/meter.tsx ADDED
@@ -0,0 +1,159 @@
1
+ // Meter — the level meter every plugin needs: painted bar with a hot zone
2
+ // and a peak-hold line that holds, then falls. Feed it any 0..1 value
3
+ // (typically pushed from C++ via useNativeEvent).
4
+
5
+ import { useEffect, useRef, useState } from "react";
6
+ import { View, Text } from "./primitives";
7
+
8
+ const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
9
+
10
+ export interface PeakHoldOptions {
11
+ /** How long a peak is held before it starts falling. Default 600ms. */
12
+ holdMs?: number;
13
+ /** Fall rate once the hold expires, in value/second. Default 1.5. */
14
+ decayPerSecond?: number;
15
+ }
16
+
17
+ export interface PeakHoldState {
18
+ peak: number;
19
+ heldForMs: number;
20
+ }
21
+
22
+ /** One peak-hold step (pure, testable): new peaks latch instantly and
23
+ reset the hold timer; after holdMs the peak decays toward the value. */
24
+ export function peakHoldStep(
25
+ state: PeakHoldState,
26
+ value: number,
27
+ dtMs: number,
28
+ { holdMs = 600, decayPerSecond = 1.5 }: PeakHoldOptions = {},
29
+ ): PeakHoldState {
30
+ if (value >= state.peak) return { peak: value, heldForMs: 0 };
31
+
32
+ const heldForMs = state.heldForMs + dtMs;
33
+ if (heldForMs < holdMs) return { peak: state.peak, heldForMs };
34
+
35
+ return { peak: Math.max(value, state.peak - decayPerSecond * (dtMs / 1000)), heldForMs };
36
+ }
37
+
38
+ const FRAME_MS = 33; // meters read fine at ~30fps
39
+
40
+ /** The held peak for a live value — drives the Meter's peak line. */
41
+ export function usePeakHold(value: number, options: PeakHoldOptions = {}): number {
42
+ const [peak, setPeak] = useState(value);
43
+ const state = useRef<PeakHoldState>({ peak: value, heldForMs: 0 });
44
+ const valueRef = useRef(value);
45
+ valueRef.current = value;
46
+ const optionsRef = useRef(options);
47
+ optionsRef.current = options;
48
+
49
+ useEffect(() => {
50
+ const id = setInterval(() => {
51
+ state.current = peakHoldStep(state.current, valueRef.current, FRAME_MS, optionsRef.current);
52
+ setPeak(state.current.peak);
53
+ }, FRAME_MS);
54
+
55
+ return () => clearInterval(id);
56
+ }, []);
57
+
58
+ return Math.max(peak, value);
59
+ }
60
+
61
+ export interface MeterProps {
62
+ /** Level 0..1. */
63
+ value: number;
64
+ /** Long-axis length. Default 120. */
65
+ length?: number;
66
+ /** Short-axis thickness. Default 10. */
67
+ thickness?: number;
68
+ /** Horizontal bar instead of the vertical default. */
69
+ horizontal?: boolean;
70
+ /** Show the peak-hold line. Default true. */
71
+ peak?: boolean;
72
+ holdMs?: number;
73
+ decayPerSecond?: number;
74
+ /** Where the fill turns hot, 0..1. Default 0.85. */
75
+ hotFrom?: number;
76
+ trackColor?: string;
77
+ color?: string;
78
+ hotColor?: string;
79
+ label?: string;
80
+ }
81
+
82
+ /** A natively painted level meter with hot zone + peak hold. */
83
+ export function Meter({
84
+ value,
85
+ length = 120,
86
+ thickness = 10,
87
+ horizontal,
88
+ peak = true,
89
+ holdMs,
90
+ decayPerSecond,
91
+ hotFrom = 0.85,
92
+ trackColor = "#141714",
93
+ color = "#C6F135",
94
+ hotColor = "#FF4545",
95
+ label,
96
+ }: MeterProps) {
97
+ const level = clamp01(value);
98
+ const held = usePeakHold(level, { holdMs, decayPerSecond });
99
+ const hot = clamp01(hotFrom);
100
+
101
+ const fill = Math.min(level, hot) * length;
102
+ const hotFill = level > hot ? (level - hot) * length : 0;
103
+ const peakAt = clamp01(held) * length;
104
+
105
+ const bar = horizontal ? (
106
+ <View
107
+ className="relative rounded overflow-hidden"
108
+ style={{ width: length, height: thickness, backgroundColor: trackColor }}
109
+ >
110
+ <View
111
+ className="absolute left-0 top-0 bottom-0"
112
+ style={{ width: fill, backgroundColor: color }}
113
+ />
114
+ {hotFill > 0 ? (
115
+ <View
116
+ className="absolute top-0 bottom-0"
117
+ style={{ left: hot * length, width: hotFill, backgroundColor: hotColor }}
118
+ />
119
+ ) : null}
120
+ {peak && peakAt > 2 ? (
121
+ <View
122
+ className="absolute top-0 bottom-0 w-[2]"
123
+ style={{ left: peakAt - 2, backgroundColor: held >= hot ? hotColor : color }}
124
+ />
125
+ ) : null}
126
+ </View>
127
+ ) : (
128
+ <View
129
+ className="relative rounded overflow-hidden"
130
+ style={{ width: thickness, height: length, backgroundColor: trackColor }}
131
+ >
132
+ <View
133
+ className="absolute bottom-0 left-0 right-0"
134
+ style={{ height: fill, backgroundColor: color }}
135
+ />
136
+ {hotFill > 0 ? (
137
+ <View
138
+ className="absolute left-0 right-0"
139
+ style={{ bottom: hot * length, height: hotFill, backgroundColor: hotColor }}
140
+ />
141
+ ) : null}
142
+ {peak && peakAt > 2 ? (
143
+ <View
144
+ className="absolute left-0 right-0 h-[2]"
145
+ style={{ bottom: peakAt - 2, backgroundColor: held >= hot ? hotColor : color }}
146
+ />
147
+ ) : null}
148
+ </View>
149
+ );
150
+
151
+ if (label === undefined) return bar;
152
+
153
+ return (
154
+ <View className="items-center gap-2">
155
+ {bar}
156
+ <Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
157
+ </View>
158
+ );
159
+ }
package/src/parameters.ts CHANGED
@@ -17,6 +17,36 @@ export interface ParameterHandle extends ParameterState {
17
17
  end: () => void;
18
18
  }
19
19
 
20
+ export interface ParameterInfo {
21
+ id: string;
22
+ name: string;
23
+ label: string;
24
+ /** Normalized 0..1 snapshot at mount — use useParameter(id) for live values. */
25
+ value: number;
26
+ text: string;
27
+ }
28
+
29
+ /**
30
+ * Enumerates every APVTS parameter (via param:list) once at mount — the
31
+ * host's parameter set is fixed for the plugin's lifetime. Powers
32
+ * <GenericEditor/> and any auto-generated UI.
33
+ */
34
+ export function useParameterList(): ParameterInfo[] {
35
+ const [list] = useState<ParameterInfo[]>(() => {
36
+ const result = native.call("param:list");
37
+ if (!Array.isArray(result)) return [];
38
+ return result.map((entry) => ({
39
+ id: String(entry?.id ?? ""),
40
+ name: String(entry?.name ?? entry?.id ?? ""),
41
+ label: String(entry?.label ?? ""),
42
+ value: Number(entry?.value ?? 0),
43
+ text: String(entry?.text ?? ""),
44
+ }));
45
+ });
46
+
47
+ return list;
48
+ }
49
+
20
50
  export function useParameter(id: string): ParameterHandle {
21
51
  const [state, setState] = useState<ParameterState>(() => {
22
52
  const initial = native.call("param:get", { id });
package/src/theme.ts CHANGED
@@ -1,7 +1,82 @@
1
1
  export type ThemeColors = Record<string, string>;
2
2
 
3
- // Tailwind v3 palettes (the curated subset VSReacT ships with).
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,41 @@ 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
+ });
140
+
141
+ describe("negative spacing", () => {
142
+ test("negative margins and offsets", () => {
143
+ expect(tw("-mt-2").style).toEqual({ marginTop: -8 });
144
+ expect(tw("-mx-[10]").style).toEqual({ marginLeft: -10, marginRight: -10 });
145
+ expect(tw("-top-4").style).toEqual({ top: -16 });
146
+ });
147
+
148
+ test("negative percentages", () => {
149
+ expect(tw("-left-1/2").style).toEqual({ left: "-50%" });
150
+ });
151
+ });
package/src/tw.ts CHANGED
@@ -83,6 +83,7 @@ const staticClasses: Record<string, Style> = {
83
83
 
84
84
  const textSizes: Record<string, number> = {
85
85
  xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30, "4xl": 36,
86
+ "5xl": 48, "6xl": 60,
86
87
  };
87
88
 
88
89
  const radiusSizes: Record<string, number> = {
@@ -105,6 +106,7 @@ const radiusCorners: Record<string, string[]> = {
105
106
  const lengthKeys: Record<string, string[]> = {
106
107
  w: ["width"],
107
108
  h: ["height"],
109
+ size: ["width", "height"],
108
110
  "min-w": ["minWidth"],
109
111
  "min-h": ["minHeight"],
110
112
  "max-w": ["maxWidth"],
@@ -131,6 +133,8 @@ const lengthKeys: Record<string, string[]> = {
131
133
  bottom: ["bottom"],
132
134
  left: ["left"],
133
135
  inset: ["left", "right", "top", "bottom"],
136
+ "inset-x": ["left", "right"],
137
+ "inset-y": ["top", "bottom"],
134
138
  basis: ["flexBasis"],
135
139
  };
136
140
 
@@ -165,7 +169,12 @@ function resolveClass(cls: string): Style | undefined {
165
169
  const negative = cls.startsWith("-");
166
170
  const body = negative ? cls.slice(1) : cls;
167
171
 
168
- const negate = (v: StyleValue): StyleValue => (negative && typeof v === "number" ? -v : v);
172
+ const negate = (v: StyleValue): StyleValue => {
173
+ if (!negative) return v;
174
+ if (typeof v === "number") return -v;
175
+ if (typeof v === "string" && v.endsWith("%")) return `-${v}`;
176
+ return v;
177
+ };
169
178
 
170
179
  const known = staticClasses[body];
171
180
  if (known && !negative) return known;