@vsreact/core 0.0.4 → 0.0.6

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
@@ -9,6 +9,8 @@ export interface ParameterState {
9
9
  text: string;
10
10
  name: string;
11
11
  label: string;
12
+ /** The host's normalized default — double-click-reset target. */
13
+ defaultValue: number;
12
14
  }
13
15
 
14
16
  export interface ParameterHandle extends ParameterState {
@@ -24,6 +26,7 @@ export interface ParameterInfo {
24
26
  /** Normalized 0..1 snapshot at mount — use useParameter(id) for live values. */
25
27
  value: number;
26
28
  text: string;
29
+ defaultValue: number;
27
30
  }
28
31
 
29
32
  /**
@@ -41,6 +44,7 @@ export function useParameterList(): ParameterInfo[] {
41
44
  label: String(entry?.label ?? ""),
42
45
  value: Number(entry?.value ?? 0),
43
46
  text: String(entry?.text ?? ""),
47
+ defaultValue: Number(entry?.defaultValue ?? 0),
44
48
  }));
45
49
  });
46
50
 
@@ -56,8 +60,9 @@ export function useParameter(id: string): ParameterHandle {
56
60
  text: String(initial.text ?? ""),
57
61
  name: String(initial.name ?? id),
58
62
  label: String(initial.label ?? ""),
63
+ defaultValue: Number(initial.defaultValue ?? 0),
59
64
  }
60
- : { value: 0, text: "", name: id, label: "" };
65
+ : { value: 0, text: "", name: id, label: "", defaultValue: 0 };
61
66
  });
62
67
 
63
68
  useEffect(
@@ -0,0 +1,137 @@
1
+ // Tooltip + Modal — the dialog kit, built on the overlay layer and
2
+ // onLayout. Both paint above everything via useOverlay.
3
+
4
+ import { useEffect, useRef, useState, type ReactNode } from "react";
5
+ import { View, Text } from "./primitives";
6
+ import { useLayoutRect } from "./hooks";
7
+ import { useOverlay } from "./overlay";
8
+
9
+ export interface TooltipProps {
10
+ /** The tip text. */
11
+ label: string;
12
+ /** Hover dwell before showing. Default 450ms. */
13
+ delayMs?: number;
14
+ /** Gap between the child and the tip. Default 6. */
15
+ offset?: number;
16
+ backgroundColor?: string;
17
+ color?: string;
18
+ children: ReactNode;
19
+ }
20
+
21
+ /** Wraps its child; hovering it long enough shows a tip below. */
22
+ export function Tooltip({
23
+ label,
24
+ delayMs = 450,
25
+ offset = 6,
26
+ backgroundColor = "#20241F",
27
+ color = "#d4d4d8",
28
+ children,
29
+ }: TooltipProps) {
30
+ const [rect, onLayout] = useLayoutRect();
31
+ const [visible, setVisible] = useState(false);
32
+ const overlay = useOverlay();
33
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
34
+
35
+ useEffect(() => {
36
+ if (!visible || rect === null) {
37
+ overlay.hide();
38
+ return;
39
+ }
40
+
41
+ overlay.show(
42
+ <View
43
+ className="absolute rounded-md border px-2 py-[4]"
44
+ style={{
45
+ left: rect.x,
46
+ top: rect.y + rect.height + offset,
47
+ backgroundColor,
48
+ borderColor: "#00000066",
49
+ }}
50
+ >
51
+ <Text className="text-[11]" style={{ color }}>
52
+ {label}
53
+ </Text>
54
+ </View>,
55
+ );
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [visible, rect, label, offset, backgroundColor, color]);
58
+
59
+ useEffect(
60
+ () => () => {
61
+ if (timer.current !== null) clearTimeout(timer.current);
62
+ },
63
+ [],
64
+ );
65
+
66
+ return (
67
+ <View
68
+ onLayout={onLayout}
69
+ onMouseEnter={() => {
70
+ if (timer.current !== null) clearTimeout(timer.current);
71
+ timer.current = setTimeout(() => setVisible(true), delayMs);
72
+ }}
73
+ onMouseLeave={() => {
74
+ if (timer.current !== null) clearTimeout(timer.current);
75
+ timer.current = null;
76
+ setVisible(false);
77
+ }}
78
+ >
79
+ {children}
80
+ </View>
81
+ );
82
+ }
83
+
84
+ export interface ModalProps {
85
+ open: boolean;
86
+ /** Backdrop click (and nothing else) calls this. */
87
+ onClose: () => void;
88
+ title?: string;
89
+ width?: number;
90
+ backgroundColor?: string;
91
+ backdropColor?: string;
92
+ children: ReactNode;
93
+ }
94
+
95
+ /** A centered dialog over a click-away backdrop — confirms, settings,
96
+ about boxes. Content is yours; the chrome is minimal. */
97
+ export function Modal({
98
+ open,
99
+ onClose,
100
+ title,
101
+ width = 320,
102
+ backgroundColor = "#1C201B",
103
+ backdropColor = "#000000B0",
104
+ children,
105
+ }: ModalProps) {
106
+ const overlay = useOverlay();
107
+
108
+ useEffect(() => {
109
+ if (!open) {
110
+ overlay.hide();
111
+ return;
112
+ }
113
+
114
+ overlay.show(
115
+ <View
116
+ className="absolute inset-0 items-center justify-center"
117
+ style={{ backgroundColor: backdropColor }}
118
+ onClick={onClose}
119
+ >
120
+ {/* swallow clicks inside the panel so they don't reach the backdrop */}
121
+ <View
122
+ className="rounded-xl border p-5 gap-3 shadow-xl"
123
+ style={{ width, backgroundColor, borderColor: "#00000066" }}
124
+ onClick={() => {}}
125
+ >
126
+ {title !== undefined ? (
127
+ <Text className="text-[13] font-bold tracking-wide">{title}</Text>
128
+ ) : null}
129
+ {children}
130
+ </View>
131
+ </View>,
132
+ );
133
+ // eslint-disable-next-line react-hooks/exhaustive-deps
134
+ }, [open, title, width, backgroundColor, backdropColor, children]);
135
+
136
+ return null;
137
+ }
@@ -10,6 +10,20 @@ 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
+
21
+ /** Mouse-wheel payload. dy is JUCE's notch fraction (~0.1 per notch,
22
+ positive = wheel up). */
23
+ export interface WheelEventPayload {
24
+ dy: number;
25
+ }
26
+
13
27
  export interface CommonProps {
14
28
  className?: string;
15
29
  style?: Style;
@@ -17,6 +31,9 @@ export interface CommonProps {
17
31
  /** Resets the scroll offset of an overflow-y-scroll container. */
18
32
  scrollTop?: number;
19
33
  onClick?: () => void;
34
+ onDoubleClick?: () => void;
35
+ /** Wheel over this node (controls win the wheel over scroll containers). */
36
+ onWheel?: (e: WheelEventPayload) => void;
20
37
  onMouseEnter?: () => void;
21
38
  onMouseLeave?: () => void;
22
39
  onMouseDown?: () => void;
@@ -24,6 +41,9 @@ export interface CommonProps {
24
41
  onDragStart?: (e: DragEventPayload) => void;
25
42
  onDrag?: (e: DragEventPayload) => void;
26
43
  onDragEnd?: (e: DragEventPayload) => void;
44
+ /** Fires after layout whenever this node's root-space rect changes —
45
+ the foundation for popovers, menus, and tooltips. */
46
+ onLayout?: (rect: LayoutRect) => void;
27
47
  }
28
48
 
29
49
  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