@vsreact/core 0.0.3 → 0.0.5

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.
@@ -0,0 +1,70 @@
1
+ // The overlay layer — content that paints above everything else (menus,
2
+ // tooltips, modals). render() mounts <OverlayLayer/> after your app
3
+ // automatically, so painting order puts overlays on top and hit-testing
4
+ // reaches them first. Position entries absolutely using rects from
5
+ // onLayout / useLayoutRect.
6
+
7
+ import { Fragment, useCallback, useEffect, useRef, useSyncExternalStore, type ReactNode } from "react";
8
+
9
+ interface OverlayEntry {
10
+ key: number;
11
+ node: ReactNode;
12
+ }
13
+
14
+ let entries: OverlayEntry[] = [];
15
+ const subscribers = new Set<() => void>();
16
+ let nextOverlayKey = 1;
17
+
18
+ function emit(): void {
19
+ for (const subscriber of subscribers) subscriber();
20
+ }
21
+
22
+ function setEntry(key: number, node: ReactNode): void {
23
+ entries = [...entries.filter((e) => e.key !== key), { key, node }];
24
+ emit();
25
+ }
26
+
27
+ function clearEntry(key: number): void {
28
+ if (!entries.some((e) => e.key === key)) return;
29
+ entries = entries.filter((e) => e.key !== key);
30
+ emit();
31
+ }
32
+
33
+ /** A per-component slot in the overlay layer. show() replaces the slot's
34
+ content; hide() removes it; unmounting cleans up automatically. */
35
+ export function useOverlay(): {
36
+ show: (node: ReactNode) => void;
37
+ hide: () => void;
38
+ } {
39
+ const key = useRef(0);
40
+ if (key.current === 0) key.current = nextOverlayKey++;
41
+
42
+ useEffect(() => {
43
+ const k = key.current;
44
+ return () => clearEntry(k);
45
+ }, []);
46
+
47
+ return {
48
+ show: useCallback((node: ReactNode) => setEntry(key.current, node), []),
49
+ hide: useCallback(() => clearEntry(key.current), []),
50
+ };
51
+ }
52
+
53
+ /** Mounted automatically by render() as the last sibling of your app. */
54
+ export function OverlayLayer() {
55
+ const list = useSyncExternalStore(
56
+ (onStoreChange) => {
57
+ subscribers.add(onStoreChange);
58
+ return () => subscribers.delete(onStoreChange);
59
+ },
60
+ () => entries,
61
+ );
62
+
63
+ return (
64
+ <>
65
+ {list.map((entry) => (
66
+ <Fragment key={entry.key}>{entry.node}</Fragment>
67
+ ))}
68
+ </>
69
+ );
70
+ }
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 });
@@ -10,6 +10,14 @@ export interface DragEventPayload {
10
10
  y: number;
11
11
  }
12
12
 
13
+ /** A node's laid-out rect in root coordinates (scroll-adjusted). */
14
+ export interface LayoutRect {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+
13
21
  export interface CommonProps {
14
22
  className?: string;
15
23
  style?: Style;
@@ -24,6 +32,9 @@ export interface CommonProps {
24
32
  onDragStart?: (e: DragEventPayload) => void;
25
33
  onDrag?: (e: DragEventPayload) => void;
26
34
  onDragEnd?: (e: DragEventPayload) => void;
35
+ /** Fires after layout whenever this node's root-space rect changes —
36
+ the foundation for popovers, menus, and tooltips. */
37
+ onLayout?: (rect: LayoutRect) => void;
27
38
  }
28
39
 
29
40
  export function View(props: CommonProps) {
package/src/render.tsx CHANGED
@@ -3,12 +3,14 @@ import { LegacyRoot } from "react-reconciler/constants";
3
3
  import type { ReactNode } from "react";
4
4
  import { hostConfig } from "./hostConfig";
5
5
  import { flushOps } from "./bridge";
6
+ import { OverlayLayer } from "./overlay";
6
7
 
7
8
  const reconciler = Reconciler(hostConfig as any);
8
9
 
9
10
  let container: ReturnType<typeof reconciler.createContainer> | null = null;
10
11
 
11
- /** Mounts (or re-renders) the app into the plugin window. */
12
+ /** Mounts (or re-renders) the app into the plugin window. The overlay
13
+ layer (menus, tooltips) mounts after the app so it paints on top. */
12
14
  export function render(element: ReactNode): void {
13
15
  if (!container) {
14
16
  container = reconciler.createContainer(
@@ -23,7 +25,15 @@ export function render(element: ReactNode): void {
23
25
  );
24
26
  }
25
27
 
26
- reconciler.updateContainer(element, container, null, null);
28
+ reconciler.updateContainer(
29
+ <>
30
+ {element}
31
+ <OverlayLayer />
32
+ </>,
33
+ container,
34
+ null,
35
+ null,
36
+ );
27
37
  flushOps();
28
38
  }
29
39
 
package/src/tw.test.ts CHANGED
@@ -137,3 +137,15 @@ describe("tw 0.0.3 additions", () => {
137
137
  expect(tw("text-6xl").style.fontSize).toBe(60);
138
138
  });
139
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
@@ -169,7 +169,12 @@ function resolveClass(cls: string): Style | undefined {
169
169
  const negative = cls.startsWith("-");
170
170
  const body = negative ? cls.slice(1) : cls;
171
171
 
172
- 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
+ };
173
178
 
174
179
  const known = staticClasses[body];
175
180
  if (known && !negative) return known;