bitty-tui 0.0.17 → 0.0.18
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/cli.js +2 -1
- package/dist/components/Button.js +21 -15
- package/dist/components/Checkbox.js +9 -2
- package/dist/components/ScrollView.d.ts +2 -1
- package/dist/components/ScrollView.js +3 -1
- package/dist/components/TextInput.js +9 -4
- package/dist/dashboard/DashboardView.js +12 -4
- package/dist/dashboard/components/VaultList.js +15 -1
- package/dist/hooks/use-mouse.d.ts +11 -0
- package/dist/hooks/use-mouse.js +124 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { render } from "ink";
|
|
4
4
|
import App from "./app.js";
|
|
5
5
|
import { StatusMessageProvider } from "./hooks/status-message.js";
|
|
6
|
+
import { MouseProvider } from "./hooks/use-mouse.js";
|
|
6
7
|
import { readPackageUpSync } from "read-package-up";
|
|
7
8
|
import { art } from "./theme/art.js";
|
|
8
9
|
import path from "node:path";
|
|
@@ -26,4 +27,4 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
26
27
|
`);
|
|
27
28
|
process.exit(0);
|
|
28
29
|
}
|
|
29
|
-
render(_jsx(StatusMessageProvider, { children: _jsx(App, {}) }));
|
|
30
|
+
render(_jsx(StatusMessageProvider, { children: _jsx(MouseProvider, { children: _jsx(App, {}) }) }));
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Text, Box, useFocus, useInput } from "ink";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
3
|
+
import { useId, useRef, useState } from "react";
|
|
4
4
|
import { primary } from "../theme/style.js";
|
|
5
|
+
import { useMouseTarget } from "../hooks/use-mouse.js";
|
|
5
6
|
export const Button = ({ isActive = true, doubleConfirm, onClick, children, autoFocus = false, ...props }) => {
|
|
6
|
-
const
|
|
7
|
+
const generatedId = useId();
|
|
8
|
+
const { isFocused } = useFocus({ id: generatedId, autoFocus: autoFocus });
|
|
7
9
|
const [askConfirm, setAskConfirm] = useState(false);
|
|
8
10
|
const timeoutRef = useRef(null);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
if (askConfirm)
|
|
19
|
-
setAskConfirm(false);
|
|
20
|
-
onClick();
|
|
11
|
+
const boxRef = useRef(null);
|
|
12
|
+
const handlePress = () => {
|
|
13
|
+
if (timeoutRef.current)
|
|
14
|
+
clearTimeout(timeoutRef.current);
|
|
15
|
+
if (doubleConfirm && !askConfirm) {
|
|
16
|
+
setAskConfirm(true);
|
|
17
|
+
timeoutRef.current = setTimeout(() => setAskConfirm(false), 1000);
|
|
18
|
+
return;
|
|
21
19
|
}
|
|
20
|
+
if (askConfirm)
|
|
21
|
+
setAskConfirm(false);
|
|
22
|
+
onClick();
|
|
23
|
+
};
|
|
24
|
+
useMouseTarget(generatedId, boxRef, { onClick: handlePress });
|
|
25
|
+
useInput((input, key) => {
|
|
26
|
+
if (key.return)
|
|
27
|
+
handlePress();
|
|
22
28
|
}, { isActive: isFocused && isActive });
|
|
23
|
-
return (_jsx(Box, { borderStyle: "round", borderColor: isFocused && isActive ? primary : "gray", alignItems: "center", justifyContent: "center", ...props, children: _jsx(Text, { color: isFocused && isActive ? (askConfirm ? "yellow" : "white") : "gray", children: askConfirm ? "Confirm?" : children }) }));
|
|
29
|
+
return (_jsx(Box, { ref: boxRef, borderStyle: "round", borderColor: isFocused && isActive ? primary : "gray", alignItems: "center", justifyContent: "center", ...props, children: _jsx(Text, { color: isFocused && isActive ? (askConfirm ? "yellow" : "white") : "gray", children: askConfirm ? "Confirm?" : children }) }));
|
|
24
30
|
};
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Text, Box, useFocus, useInput } from "ink";
|
|
3
|
+
import { useId, useRef } from "react";
|
|
3
4
|
import { primary } from "../theme/style.js";
|
|
5
|
+
import { useMouseTarget } from "../hooks/use-mouse.js";
|
|
4
6
|
export const Checkbox = ({ isActive = true, value, label, onToggle, ...props }) => {
|
|
5
|
-
const
|
|
7
|
+
const generatedId = useId();
|
|
8
|
+
const { isFocused } = useFocus({ id: generatedId });
|
|
9
|
+
const boxRef = useRef(null);
|
|
10
|
+
useMouseTarget(generatedId, boxRef, {
|
|
11
|
+
onClick: () => onToggle(!value),
|
|
12
|
+
});
|
|
6
13
|
useInput((input, key) => {
|
|
7
14
|
if (input === " ") {
|
|
8
15
|
onToggle(!value);
|
|
9
16
|
}
|
|
10
17
|
}, { isActive: isFocused && isActive });
|
|
11
|
-
return (_jsxs(Box, { ...props, children: [_jsx(Box, { width: 5, height: 3, flexShrink: 0, borderStyle: "round", borderColor: isFocused && isActive ? primary : "gray", children: value && (_jsx(Box, { width: 1, height: 1, marginLeft: 1, children: _jsx(Text, { color: isFocused && isActive ? primary : "gray", children: "X" }) })) }), _jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { children: label }) })] }));
|
|
18
|
+
return (_jsxs(Box, { ref: boxRef, ...props, children: [_jsx(Box, { width: 5, height: 3, flexShrink: 0, borderStyle: "round", borderColor: isFocused && isActive ? primary : "gray", children: value && (_jsx(Box, { width: 1, height: 1, marginLeft: 1, children: _jsx(Text, { color: isFocused && isActive ? primary : "gray", children: "X" }) })) }), _jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { children: label }) })] }));
|
|
12
19
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
|
-
export declare const ScrollView: <T>({ count, list, isActive, selectedIndex, onSelect, onSubmit, children, }: {
|
|
2
|
+
export declare const ScrollView: <T>({ count, list, isActive, selectedIndex, onSelect, onSubmit, offsetRef, children, }: {
|
|
3
3
|
count: number;
|
|
4
4
|
list: T[];
|
|
5
5
|
isActive: boolean;
|
|
6
6
|
selectedIndex: number;
|
|
7
7
|
onSelect?: (position: number) => void;
|
|
8
8
|
onSubmit?: (position: number) => void;
|
|
9
|
+
offsetRef?: React.MutableRefObject<number>;
|
|
9
10
|
children: (arg: {
|
|
10
11
|
el: T;
|
|
11
12
|
index: number;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, useInput } from "ink";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
export const ScrollView = ({ count, list, isActive, selectedIndex, onSelect, onSubmit, children, }) => {
|
|
4
|
+
export const ScrollView = ({ count, list, isActive, selectedIndex, onSelect, onSubmit, offsetRef, children, }) => {
|
|
5
5
|
const [offset, setOffset] = useState(0);
|
|
6
|
+
if (offsetRef)
|
|
7
|
+
offsetRef.current = offset;
|
|
6
8
|
useInput((input, key) => {
|
|
7
9
|
if (key.upArrow) {
|
|
8
10
|
if (selectedIndex === offset && offset > 0) {
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Text, Box, useFocus, useInput, useFocusManager } from "ink";
|
|
2
|
+
import { Text, Box, useFocus, useInput, useFocusManager, } from "ink";
|
|
3
3
|
import { primary } from "../theme/style.js";
|
|
4
|
-
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
5
|
import clipboard from "clipboardy";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { useStatusMessage } from "../hooks/status-message.js";
|
|
8
|
+
import { useMouseTarget } from "../hooks/use-mouse.js";
|
|
8
9
|
export const TextInput = ({ id, placeholder, value, isPassword, showPasswordOnFocus, isActive, autoFocus, inline, multiline, maxLines = 1, onChange, onSubmit, onCopy, ...props }) => {
|
|
9
10
|
const [cursor, setCursor] = useState(onChange ? value.length : 0);
|
|
10
11
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
11
|
-
const
|
|
12
|
+
const generatedId = useId();
|
|
13
|
+
const effectiveId = id ?? generatedId;
|
|
14
|
+
const { isFocused } = useFocus({ id: effectiveId, isActive, autoFocus });
|
|
12
15
|
const { showStatusMessage } = useStatusMessage();
|
|
13
16
|
const { focusNext } = useFocusManager();
|
|
17
|
+
const boxRef = useRef(null);
|
|
18
|
+
useMouseTarget(effectiveId, boxRef);
|
|
14
19
|
const displayValue = useMemo(() => {
|
|
15
20
|
let displayValue = value;
|
|
16
21
|
if (isPassword && (showPasswordOnFocus ? !isFocused : true)) {
|
|
@@ -189,5 +194,5 @@ export const TextInput = ({ id, placeholder, value, isPassword, showPasswordOnFo
|
|
|
189
194
|
}
|
|
190
195
|
}
|
|
191
196
|
}, { isActive: isFocused });
|
|
192
|
-
return (_jsx(Box, { borderStyle: "round", borderColor: isFocused ? primary : "gray", borderBottom: !inline, borderTop: !inline, borderLeft: !inline, borderRight: !inline, flexGrow: 1, flexShrink: 0, paddingX: inline ? 0 : 1, overflow: "hidden", minHeight: inline ? 1 : 3, ...props, children: _jsx(Text, { color: value ? "white" : "gray", children: displayValue }) }));
|
|
197
|
+
return (_jsx(Box, { ref: boxRef, borderStyle: "round", borderColor: isFocused ? primary : "gray", borderBottom: !inline, borderTop: !inline, borderLeft: !inline, borderRight: !inline, flexGrow: 1, flexShrink: 0, paddingX: inline ? 0 : 1, overflow: "hidden", minHeight: inline ? 1 : 3, ...props, children: _jsx(Text, { color: value ? "white" : "gray", children: displayValue }) }));
|
|
193
198
|
};
|
|
@@ -8,6 +8,7 @@ import { HelpBar } from "./components/HelpBar.js";
|
|
|
8
8
|
import { primary } from "../theme/style.js";
|
|
9
9
|
import { bwClient, clearConfig, emptyCipher, useBwSync } from "../hooks/bw.js";
|
|
10
10
|
import { useStatusMessage } from "../hooks/status-message.js";
|
|
11
|
+
import { useMouseSubscribe } from "../hooks/use-mouse.js";
|
|
11
12
|
export function DashboardView({ onLogout }) {
|
|
12
13
|
const { sync, error, fetchSync } = useBwSync();
|
|
13
14
|
const [syncState, setSyncState] = useState(sync);
|
|
@@ -46,13 +47,20 @@ export function DashboardView({ onLogout }) {
|
|
|
46
47
|
await clearConfig();
|
|
47
48
|
onLogout();
|
|
48
49
|
};
|
|
50
|
+
useMouseSubscribe((targetId) => {
|
|
51
|
+
if (targetId === "search") {
|
|
52
|
+
setFocusedComponent("search");
|
|
53
|
+
}
|
|
54
|
+
else if (targetId === "list") {
|
|
55
|
+
setFocusedComponent("list");
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setFocusedComponent("detail");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
49
61
|
useEffect(() => {
|
|
50
62
|
setSyncState(sync);
|
|
51
63
|
}, [sync]);
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (focusedComponent === "detail")
|
|
54
|
-
focusNext();
|
|
55
|
-
}, [focusedComponent]);
|
|
56
64
|
useEffect(() => {
|
|
57
65
|
if (error)
|
|
58
66
|
showStatusMessage(error, "error");
|
|
@@ -5,6 +5,8 @@ import { CipherType } from "../../clients/bw.js";
|
|
|
5
5
|
import { ScrollView } from "../../components/ScrollView.js";
|
|
6
6
|
import clipboard from "clipboardy";
|
|
7
7
|
import { useStatusMessage } from "../../hooks/status-message.js";
|
|
8
|
+
import { useRef } from "react";
|
|
9
|
+
import { useMouseTarget } from "../../hooks/use-mouse.js";
|
|
8
10
|
const getTypeIcon = (type) => {
|
|
9
11
|
switch (type) {
|
|
10
12
|
case CipherType.Login:
|
|
@@ -22,6 +24,18 @@ const getTypeIcon = (type) => {
|
|
|
22
24
|
export function VaultList({ filteredCiphers, isFocused, selected, onSelect, }) {
|
|
23
25
|
const { stdout } = useStdout();
|
|
24
26
|
const { showStatusMessage } = useStatusMessage();
|
|
27
|
+
const boxRef = useRef(null);
|
|
28
|
+
const scrollOffsetRef = useRef(0);
|
|
29
|
+
useMouseTarget("list", boxRef, {
|
|
30
|
+
noFocus: true,
|
|
31
|
+
onClick: (_relX, relY) => {
|
|
32
|
+
const visibleIndex = relY - 1; // -1 for border
|
|
33
|
+
const actualIndex = scrollOffsetRef.current + visibleIndex;
|
|
34
|
+
if (actualIndex >= 0 && actualIndex < filteredCiphers.length) {
|
|
35
|
+
onSelect(actualIndex);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
25
39
|
useInput((input, key) => {
|
|
26
40
|
const cipher = selected !== null ? filteredCiphers[selected] : null;
|
|
27
41
|
let field, fldName;
|
|
@@ -64,5 +78,5 @@ export function VaultList({ filteredCiphers, isFocused, selected, onSelect, }) {
|
|
|
64
78
|
showStatusMessage(`📋 Copied ${fldName} to clipboard!`, "success");
|
|
65
79
|
}
|
|
66
80
|
}, { isActive: isFocused });
|
|
67
|
-
return (_jsx(Box, { flexDirection: "column", width: "40%", borderStyle: "round", borderColor: isFocused ? primaryLight : "gray", borderRightColor: "gray", paddingX: 1, overflow: "hidden", children: _jsx(ScrollView, { isActive: isFocused, count: Math.max(stdout.rows - 14, 20), list: filteredCiphers, selectedIndex: selected ?? 0, onSelect: onSelect, children: ({ el: cipher, selected }) => (_jsxs(Box, { justifyContent: "space-between", backgroundColor: selected ? (isFocused ? primary : primaryDark) : "", children: [_jsxs(Box, { children: [_jsxs(Text, { children: [getTypeIcon(cipher.type), " "] }), _jsx(Text, { color: selected && isFocused ? "white" : "default", wrap: "truncate", children: cipher.name })] }), cipher.favorite && _jsx(Text, { color: "yellow", children: "\u2605" })] }, cipher.id)) }) }));
|
|
81
|
+
return (_jsx(Box, { ref: boxRef, flexDirection: "column", width: "40%", borderStyle: "round", borderColor: isFocused ? primaryLight : "gray", borderRightColor: "gray", paddingX: 1, overflow: "hidden", children: _jsx(ScrollView, { isActive: isFocused, count: Math.max(stdout.rows - 14, 20), list: filteredCiphers, selectedIndex: selected ?? 0, onSelect: onSelect, offsetRef: scrollOffsetRef, children: ({ el: cipher, selected }) => (_jsxs(Box, { justifyContent: "space-between", backgroundColor: selected ? (isFocused ? primary : primaryDark) : "", children: [_jsxs(Box, { children: [_jsxs(Text, { children: [getTypeIcon(cipher.type), " "] }), _jsx(Text, { color: selected && isFocused ? "white" : "default", wrap: "truncate", children: cipher.name })] }), cipher.favorite && _jsx(Text, { color: "yellow", children: "\u2605" })] }, cipher.id)) }) }));
|
|
68
82
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type DOMElement } from "ink";
|
|
2
|
+
type TargetOptions = {
|
|
3
|
+
noFocus?: boolean;
|
|
4
|
+
onClick?: (relX: number, relY: number) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare const MouseProvider: ({ children, }: {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare const useMouseTarget: (id: string, ref: React.RefObject<DOMElement | null>, options?: TargetOptions) => void;
|
|
10
|
+
export declare const useMouseSubscribe: (listener: (targetId: string) => void) => void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useCallback, useEffect, useRef, } from "react";
|
|
3
|
+
import { useStdin, useFocusManager, measureElement, } from "ink";
|
|
4
|
+
const MouseContext = createContext({
|
|
5
|
+
registerTarget: () => { },
|
|
6
|
+
unregisterTarget: () => { },
|
|
7
|
+
subscribe: () => () => { },
|
|
8
|
+
});
|
|
9
|
+
const mouseSequenceRe = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
|
|
10
|
+
function getAbsolutePosition(node) {
|
|
11
|
+
let currentNode = node;
|
|
12
|
+
let x = 0;
|
|
13
|
+
let y = 0;
|
|
14
|
+
while (currentNode?.parentNode) {
|
|
15
|
+
if (!currentNode.yogaNode)
|
|
16
|
+
return undefined;
|
|
17
|
+
x += currentNode.yogaNode.getComputedLeft();
|
|
18
|
+
y += currentNode.yogaNode.getComputedTop();
|
|
19
|
+
currentNode = currentNode.parentNode;
|
|
20
|
+
}
|
|
21
|
+
return { x, y };
|
|
22
|
+
}
|
|
23
|
+
export const MouseProvider = ({ children, }) => {
|
|
24
|
+
const { internal_eventEmitter } = useStdin();
|
|
25
|
+
const { focus } = useFocusManager();
|
|
26
|
+
const targetsRef = useRef(new Map());
|
|
27
|
+
const listenersRef = useRef(new Set());
|
|
28
|
+
const focusRef = useRef(focus);
|
|
29
|
+
focusRef.current = focus;
|
|
30
|
+
const registerTarget = useCallback((id, ref, options) => {
|
|
31
|
+
targetsRef.current.set(id, { ref, ...options });
|
|
32
|
+
}, []);
|
|
33
|
+
const unregisterTarget = useCallback((id) => {
|
|
34
|
+
targetsRef.current.delete(id);
|
|
35
|
+
}, []);
|
|
36
|
+
const subscribe = useCallback((listener) => {
|
|
37
|
+
listenersRef.current.add(listener);
|
|
38
|
+
return () => {
|
|
39
|
+
listenersRef.current.delete(listener);
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
const handleClick = useCallback((x, y) => {
|
|
43
|
+
for (const [id, target] of targetsRef.current) {
|
|
44
|
+
const node = target.ref.current;
|
|
45
|
+
if (!node)
|
|
46
|
+
continue;
|
|
47
|
+
const pos = getAbsolutePosition(node);
|
|
48
|
+
if (!pos)
|
|
49
|
+
continue;
|
|
50
|
+
const { width, height } = measureElement(node);
|
|
51
|
+
if (x >= pos.x &&
|
|
52
|
+
x < pos.x + width &&
|
|
53
|
+
y >= pos.y &&
|
|
54
|
+
y < pos.y + height) {
|
|
55
|
+
for (const listener of listenersRef.current) {
|
|
56
|
+
listener(id);
|
|
57
|
+
}
|
|
58
|
+
if (!target.noFocus) {
|
|
59
|
+
focusRef.current(id);
|
|
60
|
+
}
|
|
61
|
+
target.onClick?.(x - pos.x, y - pos.y);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, []);
|
|
66
|
+
const handleClickRef = useRef(handleClick);
|
|
67
|
+
handleClickRef.current = handleClick;
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!internal_eventEmitter)
|
|
70
|
+
return;
|
|
71
|
+
// Enable SGR mouse tracking mode
|
|
72
|
+
process.stdout.write("\x1b[?1000h\x1b[?1006h");
|
|
73
|
+
// Patch event emitter to intercept mouse sequences
|
|
74
|
+
const originalEmit = internal_eventEmitter.emit.bind(internal_eventEmitter);
|
|
75
|
+
internal_eventEmitter.emit = (event, ...args) => {
|
|
76
|
+
if (event === "input") {
|
|
77
|
+
const data = args[0];
|
|
78
|
+
const match = mouseSequenceRe.exec(data);
|
|
79
|
+
if (match) {
|
|
80
|
+
const button = parseInt(match[1], 10);
|
|
81
|
+
const x = parseInt(match[2], 10) - 1;
|
|
82
|
+
const y = parseInt(match[3], 10) - 1;
|
|
83
|
+
const isPress = match[4] === "M";
|
|
84
|
+
if (isPress && (button & 3) === 0) {
|
|
85
|
+
handleClickRef.current(x, y);
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return originalEmit(event, ...args);
|
|
91
|
+
};
|
|
92
|
+
const disableMouse = () => {
|
|
93
|
+
process.stdout.write("\x1b[?1006l\x1b[?1000l");
|
|
94
|
+
};
|
|
95
|
+
process.on("exit", disableMouse);
|
|
96
|
+
return () => {
|
|
97
|
+
internal_eventEmitter.emit = originalEmit;
|
|
98
|
+
disableMouse();
|
|
99
|
+
process.removeListener("exit", disableMouse);
|
|
100
|
+
};
|
|
101
|
+
}, [internal_eventEmitter]);
|
|
102
|
+
return (_jsx(MouseContext.Provider, { value: { registerTarget, unregisterTarget, subscribe }, children: children }));
|
|
103
|
+
};
|
|
104
|
+
export const useMouseTarget = (id, ref, options) => {
|
|
105
|
+
const { registerTarget, unregisterTarget } = useContext(MouseContext);
|
|
106
|
+
const onClickRef = useRef(options?.onClick);
|
|
107
|
+
onClickRef.current = options?.onClick;
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
registerTarget(id, ref, {
|
|
110
|
+
noFocus: options?.noFocus,
|
|
111
|
+
onClick: (x, y) => onClickRef.current?.(x, y),
|
|
112
|
+
});
|
|
113
|
+
return () => unregisterTarget(id);
|
|
114
|
+
}, [id, ref, registerTarget, unregisterTarget, options?.noFocus]);
|
|
115
|
+
};
|
|
116
|
+
export const useMouseSubscribe = (listener) => {
|
|
117
|
+
const { subscribe } = useContext(MouseContext);
|
|
118
|
+
const listenerRef = useRef(listener);
|
|
119
|
+
listenerRef.current = listener;
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const stableListener = (id) => listenerRef.current(id);
|
|
122
|
+
return subscribe(stableListener);
|
|
123
|
+
}, [subscribe]);
|
|
124
|
+
};
|