@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.
- package/package.json +1 -1
- 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 +86 -84
- package/src/components/Debugger/DebuggerTerminal.tsx +18 -49
- package/src/components/Debugger/parser/DebugParser.tsx +121 -14
- package/src/components/Debugger/parser/DebugTerminal.tsx +24 -7
- package/src/components/Debugger/parser/ValueArray.tsx +22 -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 +61 -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/useDefaultValue.ts +8 -0
- package/src/hooks/useEfficientRef.ts +3 -2
- package/src/hooks/useEvent.ts +15 -0
- package/src/hooks/useLatestRef.ts +12 -0
- package/src/hooks/useLocalStorage.ts +134 -0
- package/src/hooks/useSyncRef.ts +17 -0
- package/src/hooks/useTransition.ts +2 -1
- package/src/index.ts +16 -5
- package/src/lib/errors/ContextError.ts +11 -0
- package/src/hooks/useEfficientState.ts +0 -9
- package/src/hooks/useUpdatedRef.ts +0 -12
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
|
+
}
|