@vsreact/core 0.0.5 → 0.0.7

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,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
+ }
@@ -18,6 +18,12 @@ export interface LayoutRect {
18
18
  height: number;
19
19
  }
20
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
+
21
27
  export interface CommonProps {
22
28
  className?: string;
23
29
  style?: Style;
@@ -25,6 +31,9 @@ export interface CommonProps {
25
31
  /** Resets the scroll offset of an overflow-y-scroll container. */
26
32
  scrollTop?: number;
27
33
  onClick?: () => void;
34
+ onDoubleClick?: () => void;
35
+ /** Wheel over this node (controls win the wheel over scroll containers). */
36
+ onWheel?: (e: WheelEventPayload) => void;
28
37
  onMouseEnter?: () => void;
29
38
  onMouseLeave?: () => void;
30
39
  onMouseDown?: () => void;
@@ -0,0 +1,148 @@
1
+ // Audio visualizers — bar-based displays fed by arrays of values
2
+ // (typically pushed from C++ via useNativeEvent). For audio-rate scopes,
3
+ // host a juce::Component with <NativeView>; these cover the JS-rate
4
+ // cases: spectrum bars, envelope history, waveform overviews.
5
+
6
+ import { useEffect, useState } from "react";
7
+ import { View, Text } from "./primitives";
8
+
9
+ const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
10
+
11
+ /** Pure rolling-window step (exported for tests): drop the oldest, push
12
+ the newest, pad with zeros to `length`. */
13
+ export function pushRolling(previous: number[], value: number, length: number): number[] {
14
+ const next = previous.slice(Math.max(0, previous.length - length + 1));
15
+ next.push(value);
16
+ while (next.length < length) next.unshift(0);
17
+ return next;
18
+ }
19
+
20
+ /** A rolling window of the last `length` values — turns a live scalar
21
+ (meter level, envelope) into data for <Waveform>/<Bars> history. */
22
+ export function useRollingBuffer(value: number, length = 64): number[] {
23
+ const [buffer, setBuffer] = useState<number[]>(() => Array(length).fill(0));
24
+
25
+ useEffect(() => {
26
+ setBuffer((previous) => pushRolling(previous, value, length));
27
+ }, [value, length]);
28
+
29
+ return buffer;
30
+ }
31
+
32
+ export interface BarsProps {
33
+ /** One bar per entry, 0..1. */
34
+ values: number[];
35
+ width?: number;
36
+ height?: number;
37
+ /** Gap between bars. Default 2. */
38
+ gap?: number;
39
+ /** Bars at or above this turn hot. Default 0.85; 1 disables. */
40
+ hotFrom?: number;
41
+ trackColor?: string;
42
+ color?: string;
43
+ hotColor?: string;
44
+ label?: string;
45
+ }
46
+
47
+ /** Bottom-anchored bars — spectrum analyzers, band meters. */
48
+ export function Bars({
49
+ values,
50
+ width = 160,
51
+ height = 60,
52
+ gap = 2,
53
+ hotFrom = 0.85,
54
+ trackColor = "#141714",
55
+ color = "#C6F135",
56
+ hotColor = "#FF4545",
57
+ label,
58
+ }: BarsProps) {
59
+ const bars = (
60
+ <View
61
+ className="flex-row items-end rounded overflow-hidden px-[2]"
62
+ style={{ width, height, backgroundColor: trackColor, columnGap: gap }}
63
+ >
64
+ {values.map((value, index) => {
65
+ const level = clamp01(value);
66
+ return (
67
+ <View
68
+ key={index}
69
+ className="flex-1 rounded-[1]"
70
+ style={{
71
+ height: Math.max(1, level * (height - 4)),
72
+ backgroundColor: level >= hotFrom ? hotColor : color,
73
+ }}
74
+ />
75
+ );
76
+ })}
77
+ </View>
78
+ );
79
+
80
+ if (label === undefined) return bars;
81
+
82
+ return (
83
+ <View className="items-center gap-2">
84
+ {bars}
85
+ <Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
86
+ </View>
87
+ );
88
+ }
89
+
90
+ export interface WaveformProps {
91
+ /** One bar per entry, −1..1 (or 0..1 — bars mirror around the centre). */
92
+ values: number[];
93
+ width?: number;
94
+ height?: number;
95
+ gap?: number;
96
+ trackColor?: string;
97
+ color?: string;
98
+ /** Draw the centre line. Default true. */
99
+ centerLine?: boolean;
100
+ centerLineColor?: string;
101
+ label?: string;
102
+ }
103
+
104
+ /** Centre-mirrored bars — waveform overviews, envelope history. */
105
+ export function Waveform({
106
+ values,
107
+ width = 160,
108
+ height = 60,
109
+ gap = 2,
110
+ trackColor = "#141714",
111
+ color = "#C6F135",
112
+ centerLine = true,
113
+ centerLineColor = "#FFFFFF22",
114
+ label,
115
+ }: WaveformProps) {
116
+ const wave = (
117
+ <View
118
+ className="relative flex-row items-center rounded overflow-hidden px-[2]"
119
+ style={{ width, height, backgroundColor: trackColor, columnGap: gap }}
120
+ >
121
+ {centerLine ? (
122
+ <View
123
+ className="absolute left-0 right-0 h-[1]"
124
+ style={{ top: height / 2, backgroundColor: centerLineColor }}
125
+ />
126
+ ) : null}
127
+ {values.map((value, index) => (
128
+ <View
129
+ key={index}
130
+ className="flex-1 rounded-[1]"
131
+ style={{
132
+ height: Math.max(1, Math.min(1, Math.abs(value)) * (height - 4)),
133
+ backgroundColor: color,
134
+ }}
135
+ />
136
+ ))}
137
+ </View>
138
+ );
139
+
140
+ if (label === undefined) return wave;
141
+
142
+ return (
143
+ <View className="items-center gap-2">
144
+ {wave}
145
+ <Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
146
+ </View>
147
+ );
148
+ }