@hrnec06/react_utils 1.6.0 → 1.7.1
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/README.md +1 -0
- package/package.json +2 -2
- package/src/components/ContextMenu/ContextMenu.tsx +235 -0
- package/src/components/ContextMenu/ContextMenu.types.ts +17 -0
- package/src/components/ContextMenu/ContextMenuCtx.tsx +33 -0
- package/src/components/ContextMenu/ContextMenuItem.tsx +147 -0
- package/src/components/ContextMenu/ContextMenuRoot.tsx +28 -0
- package/src/components/ContextMenu/ContextMenuSection.tsx +28 -0
- package/src/components/Debugger/Debugger.tsx +70 -18
- package/src/components/Debugger/DebuggerTerminal.tsx +3 -3
- package/src/components/Debugger/parser/DebugParser.tsx +117 -14
- package/src/components/Debugger/parser/DebugTerminal.tsx +41 -7
- package/src/components/Debugger/parser/ValueArray.tsx +31 -7
- package/src/components/Debugger/parser/ValueBoolean.tsx +15 -4
- package/src/components/Debugger/parser/ValueConstant.tsx +21 -0
- package/src/components/Debugger/parser/ValueFunction.tsx +17 -5
- package/src/components/Debugger/parser/ValueNumber.tsx +14 -4
- package/src/components/Debugger/parser/ValueObject.tsx +69 -17
- package/src/components/Debugger/parser/ValueString.tsx +13 -4
- package/src/components/ResizeableBox/ResizeableBox.tsx +1 -1
- package/src/hooks/useDebounce.ts +26 -0
- package/src/hooks/useEvent.ts +15 -0
- package/src/hooks/useLatestRef.ts +12 -0
- package/src/hooks/useSyncRef.ts +17 -0
- package/src/hooks/useTransition.ts +2 -1
- package/src/index.ts +12 -3
- package/src/lib/errors/ContextError.ts +11 -0
- package/src/hooks/useUpdatedRef.ts +0 -12
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
A source only React Library for private use.
|
package/package.json
CHANGED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import React, { Children, createContext, forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useState } from "react";
|
|
2
|
+
import CTXM from "./ContextMenu.types";
|
|
3
|
+
import { Optional, Vector2 } from "@hrnec06/util";
|
|
4
|
+
import useContextMenu, { ContextMenuContext, type ContextMenuContext as TContextMenuContext } from "./ContextMenuCtx";
|
|
5
|
+
import useDefaultValue from "../../hooks/useDefaultValue";
|
|
6
|
+
import { v4 as uuidv4} from 'uuid';
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
8
|
+
import useListener from "../../hooks/useListener";
|
|
9
|
+
import clsx from "clsx";
|
|
10
|
+
import ContextMenuSection from "./ContextMenuSection";
|
|
11
|
+
import ContextMenuRoot from "./ContextMenuRoot";
|
|
12
|
+
import useSignal, { Signal } from "../../hooks/useSignal";
|
|
13
|
+
|
|
14
|
+
type InheritCallback = (parent: TContextMenuContext) => (CTXM.ContextMenuRoot | { parent: string, children: CTXM.ContextMenuRoot });
|
|
15
|
+
|
|
16
|
+
interface ContextMenuWrapperProps {
|
|
17
|
+
open: boolean,
|
|
18
|
+
position: Vector2,
|
|
19
|
+
menu: CTXM.ContextMenuRoot
|
|
20
|
+
}
|
|
21
|
+
function ContextMenuWrapper({
|
|
22
|
+
open,
|
|
23
|
+
position,
|
|
24
|
+
menu
|
|
25
|
+
}: ContextMenuWrapperProps)
|
|
26
|
+
{
|
|
27
|
+
if (!open)
|
|
28
|
+
return undefined;
|
|
29
|
+
|
|
30
|
+
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return createPortal((
|
|
36
|
+
<div
|
|
37
|
+
onMouseDownCapture={handleMouseDown}
|
|
38
|
+
onContextMenu={handleMouseDown}
|
|
39
|
+
className={clsx(
|
|
40
|
+
"fixed z-600 select-none"
|
|
41
|
+
)}
|
|
42
|
+
style={{
|
|
43
|
+
left: position[0],
|
|
44
|
+
top: position[1]
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<ContextMenuRoot menu={menu} />
|
|
48
|
+
</div>
|
|
49
|
+
), document.body);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
interface ContextMenuReference {
|
|
54
|
+
isOpen: boolean,
|
|
55
|
+
close: () => void,
|
|
56
|
+
open: (position: Vector2) => void,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ContextMenuProps extends React.HTMLProps<HTMLDivElement> {
|
|
60
|
+
$menu: CTXM.ContextMenuRoot,
|
|
61
|
+
$open?: boolean,
|
|
62
|
+
$id?: string,
|
|
63
|
+
$inherit?: boolean | InheritCallback | Array<string | { id: string, parent: string }>,
|
|
64
|
+
/**
|
|
65
|
+
* If disabled, context event is captured using useCTXMListener()
|
|
66
|
+
*/
|
|
67
|
+
$autoGenerateWrapper?: boolean,
|
|
68
|
+
$onOpen?: () => void,
|
|
69
|
+
$onClose?: () => void,
|
|
70
|
+
$onSelect?: (id: string, owner: unknown) => void,
|
|
71
|
+
}
|
|
72
|
+
const ContextMenu = forwardRef<ContextMenuReference, ContextMenuProps>(({
|
|
73
|
+
$open,
|
|
74
|
+
$menu,
|
|
75
|
+
$id,
|
|
76
|
+
$inherit = false,
|
|
77
|
+
$autoGenerateWrapper = false,
|
|
78
|
+
$onOpen,
|
|
79
|
+
$onClose,
|
|
80
|
+
$onSelect,
|
|
81
|
+
|
|
82
|
+
...props
|
|
83
|
+
}, ref) => {
|
|
84
|
+
const parent = useContextMenu();
|
|
85
|
+
|
|
86
|
+
const open = useSignal(false);
|
|
87
|
+
const [position, setPosition] = useState<Vector2>([0, 0]);
|
|
88
|
+
|
|
89
|
+
const assignedID = useDefaultValue(() => $id ?? uuidv4());
|
|
90
|
+
|
|
91
|
+
const finalMenu = useMemo(() => {
|
|
92
|
+
if ($inherit === false)
|
|
93
|
+
return $menu;
|
|
94
|
+
|
|
95
|
+
const menu: CTXM.ContextMenuRoot = [
|
|
96
|
+
...$menu
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
let current = parent;
|
|
100
|
+
while (current !== undefined)
|
|
101
|
+
{
|
|
102
|
+
if ($inherit === true)
|
|
103
|
+
{
|
|
104
|
+
menu.push(...current.menu);
|
|
105
|
+
}
|
|
106
|
+
else if (Array.isArray($inherit))
|
|
107
|
+
{
|
|
108
|
+
for (const query of $inherit)
|
|
109
|
+
{
|
|
110
|
+
if (query === current.id)
|
|
111
|
+
{
|
|
112
|
+
menu.push(...current.menu);
|
|
113
|
+
}
|
|
114
|
+
else if (typeof query === "object" && query.id === current.id)
|
|
115
|
+
{
|
|
116
|
+
menu.push([
|
|
117
|
+
{
|
|
118
|
+
id: `inherit-${current.id}.${query.id}`,
|
|
119
|
+
label: query.parent,
|
|
120
|
+
children: current.menu
|
|
121
|
+
}
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (typeof $inherit === "function")
|
|
127
|
+
{
|
|
128
|
+
const result = $inherit(current);
|
|
129
|
+
|
|
130
|
+
if (Array.isArray(result))
|
|
131
|
+
{
|
|
132
|
+
menu.push(...result);
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
{
|
|
136
|
+
menu.push([
|
|
137
|
+
{
|
|
138
|
+
id: `inherit-${current.id}.callback`,
|
|
139
|
+
label: result.parent,
|
|
140
|
+
children: result.children
|
|
141
|
+
}
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
current = current?.parent;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return menu;
|
|
150
|
+
}, [$menu, parent, $inherit]);
|
|
151
|
+
|
|
152
|
+
useImperativeHandle(ref, () => ({
|
|
153
|
+
isOpen: false,
|
|
154
|
+
close: () => {},
|
|
155
|
+
open: () => {}
|
|
156
|
+
}), []);
|
|
157
|
+
|
|
158
|
+
useListener(document, 'mousedown', () => {
|
|
159
|
+
if (!open.value)
|
|
160
|
+
return;
|
|
161
|
+
|
|
162
|
+
open.value = false;
|
|
163
|
+
}, [open]);
|
|
164
|
+
|
|
165
|
+
const close = () => {
|
|
166
|
+
open.value = false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const handleContextmenu = (e: React.MouseEvent<HTMLElement>) => {
|
|
170
|
+
if (e.isPropagationStopped())
|
|
171
|
+
return;
|
|
172
|
+
|
|
173
|
+
e.stopPropagation();
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
|
|
176
|
+
open.value = true;
|
|
177
|
+
setPosition([e.clientX, e.clientY]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<ContextMenuContext.Provider
|
|
182
|
+
value={{
|
|
183
|
+
exposed: {
|
|
184
|
+
onContextMenu: handleContextmenu
|
|
185
|
+
},
|
|
186
|
+
parent: parent,
|
|
187
|
+
id: assignedID,
|
|
188
|
+
menu: $menu,
|
|
189
|
+
close: close
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
<ContextCapture
|
|
193
|
+
autoGenerateWrapper={$autoGenerateWrapper}
|
|
194
|
+
handleContextMenu={handleContextmenu}
|
|
195
|
+
|
|
196
|
+
{...props}
|
|
197
|
+
>
|
|
198
|
+
{props.children}
|
|
199
|
+
</ContextCapture>
|
|
200
|
+
|
|
201
|
+
<ContextMenuWrapper
|
|
202
|
+
open={open.value}
|
|
203
|
+
position={position}
|
|
204
|
+
menu={finalMenu}
|
|
205
|
+
/>
|
|
206
|
+
</ContextMenuContext.Provider>
|
|
207
|
+
)
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
interface ContextCaptureProps extends React.HTMLProps<HTMLDivElement> {
|
|
211
|
+
autoGenerateWrapper: boolean,
|
|
212
|
+
handleContextMenu: (event: React.MouseEvent<HTMLElement>) => void,
|
|
213
|
+
}
|
|
214
|
+
function ContextCapture({
|
|
215
|
+
autoGenerateWrapper,
|
|
216
|
+
handleContextMenu,
|
|
217
|
+
...props
|
|
218
|
+
}: ContextCaptureProps)
|
|
219
|
+
{
|
|
220
|
+
if (!autoGenerateWrapper)
|
|
221
|
+
return props.children;
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div
|
|
225
|
+
{...props}
|
|
226
|
+
|
|
227
|
+
onContextMenu={handleContextMenu}
|
|
228
|
+
>
|
|
229
|
+
{props.children}
|
|
230
|
+
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default ContextMenu;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Nullable } from "@hrnec06/util";
|
|
2
|
+
|
|
3
|
+
namespace CTXM {
|
|
4
|
+
export interface ContextMenuItem {
|
|
5
|
+
id: string,
|
|
6
|
+
label: string,
|
|
7
|
+
description?: string,
|
|
8
|
+
children?: ContextMenuRoot,
|
|
9
|
+
onClick?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ContextMenuSection = Array<Nullable<ContextMenuItem>>;
|
|
13
|
+
|
|
14
|
+
export type ContextMenuRoot = Array<Nullable<ContextMenuSection>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default CTXM;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Optional } from "@hrnec06/util";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import ContextError from "../../lib/errors/ContextError";
|
|
4
|
+
import CTXM from "./ContextMenu.types";
|
|
5
|
+
|
|
6
|
+
export interface ContextMenuContextExpose {
|
|
7
|
+
onContextMenu: (event: React.MouseEvent<HTMLElement>) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ContextMenuContext {
|
|
11
|
+
exposed: ContextMenuContextExpose,
|
|
12
|
+
parent?: ContextMenuContext,
|
|
13
|
+
menu: CTXM.ContextMenuRoot,
|
|
14
|
+
id: string,
|
|
15
|
+
close: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ContextMenuContext = createContext<Optional<ContextMenuContext>>(undefined);
|
|
19
|
+
|
|
20
|
+
export function useCTXMListener(): ContextMenuContextExpose['onContextMenu']
|
|
21
|
+
{
|
|
22
|
+
const ctx = useContextMenu();
|
|
23
|
+
|
|
24
|
+
if (!ctx)
|
|
25
|
+
throw new ContextError("CTXMListener");
|
|
26
|
+
|
|
27
|
+
return ctx.exposed.onContextMenu;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function useContextMenu()
|
|
31
|
+
{
|
|
32
|
+
return useContext(ContextMenuContext);
|
|
33
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ChevronRightIcon } from "lucide-react";
|
|
2
|
+
import CTXM from "./ContextMenu.types";
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import ContextMenuRoot from "./ContextMenuRoot";
|
|
5
|
+
import useTransition, { transitionDataAttributes } from "../../hooks/useTransition";
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
import useSyncRef from "../../hooks/useSyncRef";
|
|
8
|
+
import { matchMouseEvent, MouseButton } from "@hrnec06/util";
|
|
9
|
+
import useContextMenu from "./ContextMenuCtx";
|
|
10
|
+
import ContextError from "../../lib/errors/ContextError";
|
|
11
|
+
import useDebounce from "../../hooks/useDebounce";
|
|
12
|
+
|
|
13
|
+
interface ContextMenuItemProps {
|
|
14
|
+
item: CTXM.ContextMenuItem
|
|
15
|
+
}
|
|
16
|
+
export default function ContextMenuItem({
|
|
17
|
+
item
|
|
18
|
+
}: ContextMenuItemProps)
|
|
19
|
+
{
|
|
20
|
+
const menu = useContextMenu();
|
|
21
|
+
if (!menu)
|
|
22
|
+
throw new ContextError("ContextMenu");
|
|
23
|
+
|
|
24
|
+
const [hover, setHover] = useState(false);
|
|
25
|
+
const expanded = useDebounce(hover && !!item.children?.length, !hover ? 200 : 300);
|
|
26
|
+
|
|
27
|
+
const isClickable = item.onClick !== undefined;
|
|
28
|
+
const isEnabled = isClickable || !!item.children?.length;
|
|
29
|
+
|
|
30
|
+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
|
|
33
|
+
if (!isClickable || !matchMouseEvent(e.nativeEvent, MouseButton.Left)) return;
|
|
34
|
+
|
|
35
|
+
item.onClick?.();
|
|
36
|
+
menu.close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleMouseOver = () => {
|
|
40
|
+
setHover(true);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleMouseOut = () => {
|
|
44
|
+
setHover(false);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
onMouseEnter={handleMouseOver}
|
|
50
|
+
onMouseLeave={handleMouseOut}
|
|
51
|
+
role="menu"
|
|
52
|
+
className="relative"
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
onClick={handleClick}
|
|
56
|
+
role="menuitem"
|
|
57
|
+
className={clsx(
|
|
58
|
+
"whitespace-nowrap px-1",
|
|
59
|
+
isEnabled && "cursor-pointer"
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<div
|
|
63
|
+
className={clsx(
|
|
64
|
+
"flex items-center rounded-sm py-0.5",
|
|
65
|
+
hover && isEnabled && "bg-ctxm-item-hover"
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
className={clsx(
|
|
70
|
+
"flex-1 text-sm py-0.5 px-6",
|
|
71
|
+
isEnabled ? clsx(
|
|
72
|
+
"text-ctxm-item-text",
|
|
73
|
+
hover && "text-ctxm-item-text-hover"
|
|
74
|
+
) : "text-ctxm-item-disabled"
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{item.label}
|
|
78
|
+
</div>
|
|
79
|
+
<div
|
|
80
|
+
className="flex-2 pl-8 pr-1"
|
|
81
|
+
>
|
|
82
|
+
{item.description && (
|
|
83
|
+
<div
|
|
84
|
+
className={clsx(
|
|
85
|
+
"text-sm text-right",
|
|
86
|
+
isEnabled ? clsx(
|
|
87
|
+
"text-ctxm-item-description",
|
|
88
|
+
hover && "text-ctxm-item-text-hover"
|
|
89
|
+
) : "text-ctxm-item-disabled",
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
{item.description}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{item.children && (
|
|
97
|
+
<ChevronRightIcon className="size-5 inline-block float-end text-ctxm-item-text-hover" />
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{ item.children && (
|
|
104
|
+
<ContextMenuItemChildren
|
|
105
|
+
open={expanded && isEnabled}
|
|
106
|
+
children={item.children}
|
|
107
|
+
/>
|
|
108
|
+
) }
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface ContextMenuItemChildrenProps {
|
|
114
|
+
children: CTXM.ContextMenuRoot,
|
|
115
|
+
open: boolean
|
|
116
|
+
}
|
|
117
|
+
function ContextMenuItemChildren({ children, open }: ContextMenuItemChildrenProps)
|
|
118
|
+
{
|
|
119
|
+
const [localDivRef, setLocalDivRef] = useState<HTMLElement | null>(null)
|
|
120
|
+
const divRef = useRef<HTMLDivElement>(null);
|
|
121
|
+
|
|
122
|
+
const divRefSync = useSyncRef(
|
|
123
|
+
divRef,
|
|
124
|
+
setLocalDivRef
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const [visible, transition] = useTransition(true, localDivRef, open);
|
|
128
|
+
|
|
129
|
+
if (!visible)
|
|
130
|
+
return undefined;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
ref={divRefSync}
|
|
135
|
+
{...transitionDataAttributes(transition)}
|
|
136
|
+
|
|
137
|
+
className={clsx(
|
|
138
|
+
"absolute left-full -top-[5px] pl-1",
|
|
139
|
+
"transition duration-300 data-leave:duration-200 data-closed:opacity-0 data-leave:sm:scale-95 data-enter:ease-out data-leave:ease-in"
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
<ContextMenuRoot
|
|
143
|
+
menu={children}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import clsx from "clsx"
|
|
2
|
+
import CTXM from "./ContextMenu.types"
|
|
3
|
+
import ContextMenuSection from "./ContextMenuSection"
|
|
4
|
+
|
|
5
|
+
interface ContextMenuRootProps {
|
|
6
|
+
menu: CTXM.ContextMenuRoot
|
|
7
|
+
}
|
|
8
|
+
export default function ContextMenuRoot({
|
|
9
|
+
menu
|
|
10
|
+
}: ContextMenuRootProps)
|
|
11
|
+
{
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className={clsx(
|
|
15
|
+
"bg-ctxm-primary border border-ctxm-border shadow-lg shadow-ctxm-shadow rounded-md"
|
|
16
|
+
)}
|
|
17
|
+
>
|
|
18
|
+
{
|
|
19
|
+
menu.map((section, sectionIx) => (
|
|
20
|
+
section && <ContextMenuSection
|
|
21
|
+
key={sectionIx}
|
|
22
|
+
section={section}
|
|
23
|
+
/>
|
|
24
|
+
))
|
|
25
|
+
}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import CTXM from "./ContextMenu.types";
|
|
3
|
+
import ContextMenuItem from "./ContextMenuItem";
|
|
4
|
+
|
|
5
|
+
interface ContextMenuSectionProps {
|
|
6
|
+
section: CTXM.ContextMenuSection
|
|
7
|
+
}
|
|
8
|
+
export default function ContextMenuSection({
|
|
9
|
+
section
|
|
10
|
+
}: ContextMenuSectionProps)
|
|
11
|
+
{
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className={clsx(
|
|
15
|
+
"border-b border-b-ctxm-border last:border-none py-1"
|
|
16
|
+
)}
|
|
17
|
+
>
|
|
18
|
+
{
|
|
19
|
+
section.map((item) => (
|
|
20
|
+
item && <ContextMenuItem
|
|
21
|
+
key={item.id}
|
|
22
|
+
item={item}
|
|
23
|
+
/>
|
|
24
|
+
))
|
|
25
|
+
}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { minmax, Nullable, Vector2 } from "@hrnec06/util";
|
|
1
|
+
import { matchMouseEvent, minmax, MouseButton, Nullable, Vector2 } from "@hrnec06/util";
|
|
2
2
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import {DebuggerContext} from "./DebuggerContext";
|
|
@@ -6,16 +6,49 @@ import clsx from "clsx";
|
|
|
6
6
|
import ScrollBar from "./DebuggerScrollBar";
|
|
7
7
|
import ParseValue from "./parser/DebugParser";
|
|
8
8
|
import useKeyListener from "../../hooks/useKeyListener";
|
|
9
|
-
import useUpdatedRef from "../../hooks/
|
|
9
|
+
import useUpdatedRef from "../../hooks/useLatestRef";
|
|
10
10
|
import ResizeableBox from "../ResizeableBox/ResizeableBox";
|
|
11
11
|
import useNamespacedId from "../../hooks/useNamespacedId";
|
|
12
12
|
import useLocalStorage from "../../hooks/useLocalStorage";
|
|
13
13
|
import z from "zod";
|
|
14
|
+
import useDefaultValue from "../../hooks/useDefaultValue";
|
|
14
15
|
|
|
15
16
|
const DEBUG_NAMESPACE = "debugger";
|
|
16
17
|
|
|
18
|
+
function useDebuggerID(_customID?: string): string
|
|
19
|
+
{
|
|
20
|
+
const customID = useDefaultValue(_customID);
|
|
21
|
+
|
|
22
|
+
if (customID)
|
|
23
|
+
return customID;
|
|
24
|
+
|
|
25
|
+
return useNamespacedId(DEBUG_NAMESPACE);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface DebuggerLabelProps {
|
|
29
|
+
label: string
|
|
30
|
+
}
|
|
31
|
+
function DebuggerLabel({ label }: DebuggerLabelProps)
|
|
32
|
+
{
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
className={clsx(
|
|
36
|
+
"px-3 py-1",
|
|
37
|
+
"text font-semibold text-white bg-[#181818]"
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
{label}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
interface DebugProps {
|
|
18
46
|
value: unknown,
|
|
47
|
+
/**
|
|
48
|
+
* Assign a persistant ID if saved position breaks because of remounts.
|
|
49
|
+
*/
|
|
50
|
+
id?: string,
|
|
51
|
+
label?: string,
|
|
19
52
|
openPaths?: string[],
|
|
20
53
|
excludePaths?: string[],
|
|
21
54
|
size?: 'normal' | 'big' | 'tiny',
|
|
@@ -25,6 +58,8 @@ interface DebugProps {
|
|
|
25
58
|
}
|
|
26
59
|
function Debug({
|
|
27
60
|
value,
|
|
61
|
+
id: customID,
|
|
62
|
+
label,
|
|
28
63
|
openPaths,
|
|
29
64
|
excludePaths,
|
|
30
65
|
size = 'normal',
|
|
@@ -34,7 +69,7 @@ function Debug({
|
|
|
34
69
|
}: DebugProps) {
|
|
35
70
|
// Config
|
|
36
71
|
|
|
37
|
-
const id =
|
|
72
|
+
const id = useDebuggerID(customID);
|
|
38
73
|
|
|
39
74
|
const { LS_POS_KEY, LS_SIZE_KEY, LS_EXPAND_KEY } = useMemo(() => ({
|
|
40
75
|
LS_POS_KEY: id + ".position",
|
|
@@ -146,6 +181,8 @@ function Debug({
|
|
|
146
181
|
|
|
147
182
|
// Handlers
|
|
148
183
|
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
184
|
+
if (!matchMouseEvent(e.nativeEvent, MouseButton.Left)) return;
|
|
185
|
+
|
|
149
186
|
e.preventDefault();
|
|
150
187
|
|
|
151
188
|
setGrab({
|
|
@@ -213,7 +250,7 @@ function Debug({
|
|
|
213
250
|
}
|
|
214
251
|
}}
|
|
215
252
|
>
|
|
216
|
-
<div className="fixed pointer-events-none w-full h-full left-0 top-0 overflow-hidden z-
|
|
253
|
+
<div className="fixed pointer-events-none w-full h-full left-0 top-0 overflow-hidden z-500">
|
|
217
254
|
<div
|
|
218
255
|
className={clsx(
|
|
219
256
|
"absolute font-jetbrains pointer-events-auto",
|
|
@@ -235,25 +272,40 @@ function Debug({
|
|
|
235
272
|
$onResize={handleResize}
|
|
236
273
|
>
|
|
237
274
|
<div
|
|
238
|
-
|
|
239
|
-
className=
|
|
275
|
+
// onMouseDown={handleMouseDown}
|
|
276
|
+
className={clsx(
|
|
277
|
+
"flex flex-col w-full h-full",
|
|
278
|
+
"bg-[#1f1f1f] border border-[#4b4b4b] shadow-lg rounded-md"
|
|
279
|
+
)}
|
|
240
280
|
>
|
|
281
|
+
{label && (
|
|
282
|
+
<DebuggerLabel
|
|
283
|
+
label={label}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
|
|
241
287
|
<div
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
className="cursor-grab w-full"
|
|
245
|
-
style={{
|
|
246
|
-
transform: `translateY(${-(scrollProgress * scrollHeight)}px)`
|
|
247
|
-
}}
|
|
288
|
+
onWheel={handleWheel}
|
|
289
|
+
className="w-full h-full overflow-hidden flex"
|
|
248
290
|
>
|
|
249
|
-
<div
|
|
250
|
-
|
|
291
|
+
<div
|
|
292
|
+
ref={containerRef}
|
|
293
|
+
onMouseDown={handleMouseDown}
|
|
294
|
+
className="cursor-grab w-full"
|
|
295
|
+
style={{
|
|
296
|
+
transform: `translateY(${-(scrollProgress * scrollHeight)}px)`
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
<div className="p-3 pl-5">
|
|
300
|
+
<ParseValue value={value} />
|
|
301
|
+
</div>
|
|
251
302
|
</div>
|
|
303
|
+
<ScrollBar
|
|
304
|
+
containerHeight={containerHeight}
|
|
305
|
+
scrollHeight={scrollHeight}
|
|
306
|
+
/>
|
|
252
307
|
</div>
|
|
253
|
-
|
|
254
|
-
containerHeight={containerHeight}
|
|
255
|
-
scrollHeight={scrollHeight}
|
|
256
|
-
/>
|
|
308
|
+
|
|
257
309
|
</div>
|
|
258
310
|
</ResizeableBox>
|
|
259
311
|
</div>
|
|
@@ -282,9 +282,9 @@ function useTerminal(
|
|
|
282
282
|
// const history = useState<LogItem[]>(storage);
|
|
283
283
|
const inputHistory = useState<string[]>([]);
|
|
284
284
|
|
|
285
|
-
const terminal = useMemo(() =>
|
|
286
|
-
|
|
287
|
-
|
|
285
|
+
const terminal = useMemo(() =>
|
|
286
|
+
new Terminal(id, name, history, inputHistory, onInput)
|
|
287
|
+
, [name, history, onInput]);
|
|
288
288
|
|
|
289
289
|
// useUpdateEffect(() => {
|
|
290
290
|
// setStorage(history[0]);
|