@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.
- package/dist/controls.d.ts +57 -6
- package/dist/controls.js +79 -14
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.js +10 -0
- package/dist/hostConfig.js +3 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -2
- package/dist/overlay.d.ts +9 -0
- package/dist/overlay.js +47 -0
- package/dist/parameters.d.ts +3 -0
- package/dist/parameters.js +3 -1
- package/dist/popover.d.ts +27 -0
- package/dist/popover.js +55 -0
- package/dist/primitives.d.ts +21 -0
- package/dist/render.d.ts +2 -1
- package/dist/render.js +5 -2
- package/package.json +2 -2
- package/src/controls.test.tsx +243 -1
- package/src/controls.tsx +216 -13
- package/src/hooks.ts +12 -0
- package/src/hostConfig.ts +3 -0
- package/src/index.ts +9 -2
- package/src/overlay.tsx +70 -0
- package/src/parameters.ts +6 -1
- package/src/popover.tsx +137 -0
- package/src/primitives.tsx +20 -0
- package/src/render.tsx +12 -2
package/src/overlay.tsx
ADDED
|
@@ -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(
|
package/src/popover.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/primitives.tsx
CHANGED
|
@@ -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(
|
|
28
|
+
reconciler.updateContainer(
|
|
29
|
+
<>
|
|
30
|
+
{element}
|
|
31
|
+
<OverlayLayer />
|
|
32
|
+
</>,
|
|
33
|
+
container,
|
|
34
|
+
null,
|
|
35
|
+
null,
|
|
36
|
+
);
|
|
27
37
|
flushOps();
|
|
28
38
|
}
|
|
29
39
|
|