@hrnec06/react_utils 1.5.1 → 1.7.0

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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/src/components/ContextMenu/ContextMenu.tsx +235 -0
  3. package/src/components/ContextMenu/ContextMenu.types.ts +17 -0
  4. package/src/components/ContextMenu/ContextMenuCtx.tsx +33 -0
  5. package/src/components/ContextMenu/ContextMenuItem.tsx +147 -0
  6. package/src/components/ContextMenu/ContextMenuRoot.tsx +28 -0
  7. package/src/components/ContextMenu/ContextMenuSection.tsx +28 -0
  8. package/src/components/Debugger/Debugger.tsx +86 -84
  9. package/src/components/Debugger/DebuggerTerminal.tsx +18 -49
  10. package/src/components/Debugger/parser/DebugParser.tsx +121 -14
  11. package/src/components/Debugger/parser/DebugTerminal.tsx +24 -7
  12. package/src/components/Debugger/parser/ValueArray.tsx +22 -7
  13. package/src/components/Debugger/parser/ValueBoolean.tsx +15 -4
  14. package/src/components/Debugger/parser/ValueConstant.tsx +21 -0
  15. package/src/components/Debugger/parser/ValueFunction.tsx +17 -5
  16. package/src/components/Debugger/parser/ValueNumber.tsx +14 -4
  17. package/src/components/Debugger/parser/ValueObject.tsx +61 -17
  18. package/src/components/Debugger/parser/ValueString.tsx +13 -4
  19. package/src/components/ResizeableBox/ResizeableBox.tsx +1 -1
  20. package/src/hooks/useDebounce.ts +26 -0
  21. package/src/hooks/useDefaultValue.ts +8 -0
  22. package/src/hooks/useEfficientRef.ts +3 -2
  23. package/src/hooks/useEvent.ts +15 -0
  24. package/src/hooks/useLatestRef.ts +12 -0
  25. package/src/hooks/useLocalStorage.ts +134 -0
  26. package/src/hooks/useSyncRef.ts +17 -0
  27. package/src/hooks/useTransition.ts +2 -1
  28. package/src/index.ts +16 -5
  29. package/src/lib/errors/ContextError.ts +11 -0
  30. package/src/hooks/useEfficientState.ts +0 -9
  31. package/src/hooks/useUpdatedRef.ts +0 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hrnec06/react_utils",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "A debugger component for react.",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -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
+ }