@alpaca-editor/core 1.0.4109 → 1.0.4111
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/components/ui/context-menu.d.ts +45 -17
- package/dist/components/ui/context-menu.js +197 -32
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/components/ui/dialog.d.ts +2 -1
- package/dist/components/ui/dialog.js +2 -2
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/editor/ContextMenu.js +6 -3
- package/dist/editor/ContextMenu.js.map +1 -1
- package/dist/editor/MainLayout.js +1 -1
- package/dist/editor/MainLayout.js.map +1 -1
- package/dist/editor/client/hooks/useGlobalEditorEvents.js +12 -2
- package/dist/editor/client/hooks/useGlobalEditorEvents.js.map +1 -1
- package/dist/editor/commands/itemCommands.js +5 -10
- package/dist/editor/commands/itemCommands.js.map +1 -1
- package/dist/editor/context-menu/InsertMenu.js +24 -3
- package/dist/editor/context-menu/InsertMenu.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +4 -1
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/sidebar/SidebarView.js +1 -1
- package/dist/editor/sidebar/SidebarView.js.map +1 -1
- package/dist/editor/ui/ItemNameDialogNew.js +1 -7
- package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
- package/dist/editor/ui/SimpleMenu.js.map +1 -1
- package/dist/editor/ui/Splitter.js +16 -14
- package/dist/editor/ui/Splitter.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +12 -6
- package/package.json +1 -1
- package/src/components/ui/context-menu.tsx +387 -144
- package/src/components/ui/dialog.tsx +3 -1
- package/src/editor/ContextMenu.tsx +11 -1
- package/src/editor/MainLayout.tsx +9 -13
- package/src/editor/client/hooks/useGlobalEditorEvents.ts +14 -2
- package/src/editor/commands/itemCommands.tsx +11 -12
- package/src/editor/context-menu/InsertMenu.tsx +33 -3
- package/src/editor/page-viewer/PageViewerFrame.tsx +6 -1
- package/src/editor/sidebar/SidebarView.tsx +6 -9
- package/src/editor/ui/ItemNameDialogNew.tsx +2 -12
- package/src/editor/ui/SimpleMenu.tsx +0 -1
- package/src/editor/ui/Splitter.tsx +16 -17
- package/src/revision.ts +2 -2
|
@@ -1,164 +1,246 @@
|
|
|
1
|
-
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
|
2
|
-
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
|
3
1
|
import * as React from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
type Position = { x: number; y: number } | null;
|
|
8
|
+
|
|
9
|
+
interface ContextMenuState {
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
position: Position;
|
|
12
|
+
openAt: (position: { x: number; y: number }) => void;
|
|
13
|
+
close: () => void;
|
|
14
|
+
registerContainer: (el: HTMLElement | null) => void;
|
|
15
|
+
unregisterContainer: (el: HTMLElement | null) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ContextMenuRootContext = React.createContext<ContextMenuState | null>(
|
|
19
|
+
null,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
function useContextMenuRoot(): ContextMenuState {
|
|
23
|
+
const ctx = React.useContext(ContextMenuRootContext);
|
|
24
|
+
if (!ctx)
|
|
25
|
+
throw new Error("ContextMenu components must be used within ContextMenu");
|
|
26
|
+
return ctx;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ContextMenu({ children }: { children: React.ReactNode }) {
|
|
30
|
+
const [position, setPosition] = React.useState<Position>(null);
|
|
31
|
+
const containersRef = React.useRef(new Set<HTMLElement>());
|
|
32
|
+
|
|
33
|
+
const isOpen = position !== null;
|
|
34
|
+
|
|
35
|
+
const openAt = React.useCallback((pos: { x: number; y: number }) => {
|
|
36
|
+
setPosition(pos);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const close = React.useCallback(() => {
|
|
40
|
+
setPosition(null);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const registerContainer = React.useCallback((el: HTMLElement | null) => {
|
|
44
|
+
if (el) containersRef.current.add(el);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const unregisterContainer = React.useCallback((el: HTMLElement | null) => {
|
|
48
|
+
if (el) containersRef.current.delete(el);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (!isOpen) return;
|
|
53
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
54
|
+
if (e.key === "Escape") close();
|
|
55
|
+
};
|
|
56
|
+
const handlePointerDown = (e: MouseEvent | PointerEvent) => {
|
|
57
|
+
const target = e.target as Node | null;
|
|
58
|
+
let inside = false;
|
|
59
|
+
containersRef.current.forEach((el) => {
|
|
60
|
+
if (el.contains(target as Node)) inside = true;
|
|
61
|
+
});
|
|
62
|
+
if (!inside) close();
|
|
63
|
+
};
|
|
64
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
65
|
+
// Use bubble phase so capture-phase handlers inside the menu can stop propagation
|
|
66
|
+
window.addEventListener("pointerdown", handlePointerDown as any);
|
|
67
|
+
window.addEventListener("contextmenu", handlePointerDown as any);
|
|
68
|
+
return () => {
|
|
69
|
+
window.removeEventListener("keydown", handleKeyDown, true);
|
|
70
|
+
window.removeEventListener("pointerdown", handlePointerDown as any);
|
|
71
|
+
window.removeEventListener("contextmenu", handlePointerDown as any);
|
|
72
|
+
};
|
|
73
|
+
}, [isOpen, close]);
|
|
74
|
+
|
|
75
|
+
const value = React.useMemo<ContextMenuState>(
|
|
76
|
+
() => ({
|
|
77
|
+
isOpen,
|
|
78
|
+
position,
|
|
79
|
+
openAt,
|
|
80
|
+
close,
|
|
81
|
+
registerContainer,
|
|
82
|
+
unregisterContainer,
|
|
83
|
+
}),
|
|
84
|
+
[isOpen, position, openAt, close, registerContainer, unregisterContainer],
|
|
85
|
+
);
|
|
86
|
+
|
|
10
87
|
return (
|
|
11
|
-
<
|
|
12
|
-
data-slot="context-menu"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
88
|
+
<ContextMenuRootContext.Provider value={value}>
|
|
89
|
+
<div data-slot="context-menu" data-testid="context-menu">
|
|
90
|
+
{children}
|
|
91
|
+
</div>
|
|
92
|
+
</ContextMenuRootContext.Provider>
|
|
16
93
|
);
|
|
17
94
|
}
|
|
18
95
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
96
|
+
type TriggerProps = {
|
|
97
|
+
asChild?: boolean;
|
|
98
|
+
children: React.ReactElement;
|
|
99
|
+
} & React.HTMLAttributes<HTMLElement>;
|
|
100
|
+
|
|
101
|
+
function ContextMenuTrigger({ asChild, children, ...rest }: TriggerProps) {
|
|
102
|
+
const { openAt } = useContextMenuRoot();
|
|
103
|
+
|
|
104
|
+
const onContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
|
105
|
+
if (rest.onContextMenu) rest.onContextMenu(e);
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
openAt({ x: e.clientX, y: e.clientY });
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (asChild && React.isValidElement(children)) {
|
|
112
|
+
const childOnContextMenu = (children.props as any)?.onContextMenu as
|
|
113
|
+
| ((ev: React.MouseEvent<any>) => void)
|
|
114
|
+
| undefined;
|
|
115
|
+
const composedOnContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
|
116
|
+
if (childOnContextMenu) (childOnContextMenu as any)(e);
|
|
117
|
+
onContextMenu(e);
|
|
118
|
+
};
|
|
119
|
+
return React.cloneElement(
|
|
120
|
+
children as any,
|
|
121
|
+
{
|
|
122
|
+
onContextMenu: composedOnContextMenu,
|
|
123
|
+
} as any,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
22
127
|
return (
|
|
23
|
-
<
|
|
128
|
+
<span
|
|
24
129
|
data-slot="context-menu-trigger"
|
|
25
130
|
data-testid="context-menu-trigger"
|
|
26
|
-
{
|
|
27
|
-
|
|
131
|
+
onContextMenu={onContextMenu}
|
|
132
|
+
{...rest}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</span>
|
|
28
136
|
);
|
|
29
137
|
}
|
|
30
138
|
|
|
31
139
|
function ContextMenuGroup({
|
|
32
|
-
|
|
33
|
-
|
|
140
|
+
children,
|
|
141
|
+
className,
|
|
142
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
34
143
|
return (
|
|
35
|
-
<
|
|
144
|
+
<div
|
|
36
145
|
data-slot="context-menu-group"
|
|
37
146
|
data-testid="context-menu-group"
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function ContextMenuPortal({
|
|
44
|
-
...props
|
|
45
|
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
|
46
|
-
return (
|
|
47
|
-
<ContextMenuPrimitive.Portal
|
|
48
|
-
data-slot="context-menu-portal"
|
|
49
|
-
data-testid="context-menu-portal"
|
|
50
|
-
{...props}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function ContextMenuSub({
|
|
56
|
-
...props
|
|
57
|
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
|
58
|
-
return (
|
|
59
|
-
<ContextMenuPrimitive.Sub
|
|
60
|
-
data-slot="context-menu-sub"
|
|
61
|
-
data-testid="context-menu-sub"
|
|
62
|
-
{...props}
|
|
63
|
-
/>
|
|
147
|
+
className={className}
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
</div>
|
|
64
151
|
);
|
|
65
152
|
}
|
|
66
153
|
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
|
70
|
-
return (
|
|
71
|
-
<ContextMenuPrimitive.RadioGroup
|
|
72
|
-
data-slot="context-menu-radio-group"
|
|
73
|
-
data-testid="context-menu-radio-group"
|
|
74
|
-
{...props}
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
154
|
+
function ContextMenuPortal({ children }: { children: React.ReactNode }) {
|
|
155
|
+
return <>{children}</>;
|
|
77
156
|
}
|
|
78
157
|
|
|
79
|
-
function
|
|
158
|
+
function ContextMenuContent({
|
|
80
159
|
className,
|
|
81
|
-
inset,
|
|
82
160
|
children,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
161
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
162
|
+
const { isOpen, position, close, registerContainer, unregisterContainer } =
|
|
163
|
+
useContextMenuRoot();
|
|
164
|
+
const contentRef = React.useRef<HTMLDivElement | null>(null);
|
|
165
|
+
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
const el = contentRef.current;
|
|
168
|
+
registerContainer(el);
|
|
169
|
+
return () => unregisterContainer(el);
|
|
170
|
+
}, [registerContainer, unregisterContainer]);
|
|
171
|
+
|
|
172
|
+
if (!isOpen || !position) return null;
|
|
173
|
+
|
|
174
|
+
const OFFSET = 1;
|
|
175
|
+
const left = position.x + OFFSET;
|
|
176
|
+
const top = position.y + OFFSET;
|
|
177
|
+
|
|
178
|
+
const node = (
|
|
179
|
+
<div
|
|
180
|
+
ref={contentRef}
|
|
181
|
+
data-slot="context-menu-content"
|
|
182
|
+
data-testid="context-menu-content"
|
|
92
183
|
className={cn(
|
|
93
|
-
"
|
|
184
|
+
"bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-visible rounded-md border font-light shadow-md",
|
|
94
185
|
className,
|
|
95
186
|
)}
|
|
96
|
-
{
|
|
187
|
+
style={{ position: "fixed", left, top }}
|
|
188
|
+
onKeyDownCapture={(e) => {
|
|
189
|
+
if (e.key === "Escape") {
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
close();
|
|
192
|
+
}
|
|
193
|
+
}}
|
|
194
|
+
onContextMenu={(e) => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
}}
|
|
97
198
|
>
|
|
98
199
|
{children}
|
|
99
|
-
|
|
100
|
-
</ContextMenuPrimitive.SubTrigger>
|
|
200
|
+
</div>
|
|
101
201
|
);
|
|
102
|
-
}
|
|
103
202
|
|
|
104
|
-
|
|
105
|
-
className,
|
|
106
|
-
...props
|
|
107
|
-
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
|
108
|
-
return (
|
|
109
|
-
<ContextMenuPrimitive.Portal>
|
|
110
|
-
<ContextMenuPrimitive.SubContent
|
|
111
|
-
data-slot="context-menu-sub-content"
|
|
112
|
-
data-testid="context-menu-sub-content"
|
|
113
|
-
className={cn(
|
|
114
|
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 font-light shadow-lg",
|
|
115
|
-
className,
|
|
116
|
-
)}
|
|
117
|
-
{...props}
|
|
118
|
-
/>
|
|
119
|
-
</ContextMenuPrimitive.Portal>
|
|
120
|
-
);
|
|
203
|
+
return createPortal(node, document.body);
|
|
121
204
|
}
|
|
122
205
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<ContextMenuPrimitive.Content
|
|
130
|
-
data-slot="context-menu-content"
|
|
131
|
-
data-testid="context-menu-content"
|
|
132
|
-
className={cn(
|
|
133
|
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-visible rounded-md border font-light shadow-md",
|
|
134
|
-
className,
|
|
135
|
-
)}
|
|
136
|
-
{...props}
|
|
137
|
-
/>
|
|
138
|
-
</ContextMenuPrimitive.Portal>
|
|
139
|
-
);
|
|
140
|
-
}
|
|
206
|
+
type ItemCommonProps = {
|
|
207
|
+
inset?: boolean;
|
|
208
|
+
variant?: "default" | "destructive";
|
|
209
|
+
onSelect?: (e: React.MouseEvent | React.KeyboardEvent) => void;
|
|
210
|
+
disabled?: boolean;
|
|
211
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
141
212
|
|
|
142
213
|
function ContextMenuItem({
|
|
143
214
|
className,
|
|
144
215
|
inset,
|
|
145
216
|
variant = "default",
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
})
|
|
217
|
+
onClick,
|
|
218
|
+
onSelect,
|
|
219
|
+
...rest
|
|
220
|
+
}: ItemCommonProps) {
|
|
221
|
+
const { close } = useContextMenuRoot();
|
|
222
|
+
const disabled = rest.disabled ?? false;
|
|
223
|
+
|
|
224
|
+
const handle = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
225
|
+
if (disabled) return;
|
|
226
|
+
if (onClick) onClick(e);
|
|
227
|
+
if (onSelect) onSelect(e);
|
|
228
|
+
close();
|
|
229
|
+
};
|
|
151
230
|
return (
|
|
152
|
-
<
|
|
231
|
+
<div
|
|
232
|
+
role="menuitem"
|
|
153
233
|
data-slot="context-menu-item"
|
|
154
234
|
data-testid="context-menu-item"
|
|
155
235
|
data-inset={inset}
|
|
156
236
|
data-variant={variant}
|
|
237
|
+
data-disabled={disabled ? "" : undefined}
|
|
157
238
|
className={cn(
|
|
158
|
-
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive
|
|
239
|
+
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
159
240
|
className,
|
|
160
241
|
)}
|
|
161
|
-
{
|
|
242
|
+
onClick={handle}
|
|
243
|
+
{...rest}
|
|
162
244
|
/>
|
|
163
245
|
);
|
|
164
246
|
}
|
|
@@ -167,63 +249,99 @@ function ContextMenuCheckboxItem({
|
|
|
167
249
|
className,
|
|
168
250
|
children,
|
|
169
251
|
checked,
|
|
170
|
-
|
|
171
|
-
|
|
252
|
+
onClick,
|
|
253
|
+
...rest
|
|
254
|
+
}: {
|
|
255
|
+
className?: string;
|
|
256
|
+
children?: React.ReactNode;
|
|
257
|
+
checked?: boolean;
|
|
258
|
+
disabled?: boolean;
|
|
259
|
+
} & React.HTMLAttributes<HTMLDivElement>) {
|
|
260
|
+
const { close } = useContextMenuRoot();
|
|
261
|
+
const handle = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
262
|
+
if ((rest as any).disabled) return;
|
|
263
|
+
if (onClick) onClick(e);
|
|
264
|
+
close();
|
|
265
|
+
};
|
|
172
266
|
return (
|
|
173
|
-
<
|
|
267
|
+
<div
|
|
174
268
|
data-slot="context-menu-checkbox-item"
|
|
175
269
|
data-testid="context-menu-checkbox-item"
|
|
270
|
+
data-disabled={(rest as any).disabled ? "" : undefined}
|
|
176
271
|
className={cn(
|
|
177
|
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-
|
|
272
|
+
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-xs outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
178
273
|
className,
|
|
179
274
|
)}
|
|
180
|
-
|
|
181
|
-
{...
|
|
275
|
+
onClick={handle}
|
|
276
|
+
{...rest}
|
|
182
277
|
>
|
|
183
278
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
184
|
-
<
|
|
185
|
-
<CheckIcon className="size-4" />
|
|
186
|
-
</ContextMenuPrimitive.ItemIndicator>
|
|
279
|
+
{checked ? <CheckIcon className="size-4" /> : null}
|
|
187
280
|
</span>
|
|
188
281
|
{children}
|
|
189
|
-
</
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function ContextMenuRadioGroup({ children }: { children: React.ReactNode }) {
|
|
287
|
+
return (
|
|
288
|
+
<div
|
|
289
|
+
data-slot="context-menu-radio-group"
|
|
290
|
+
data-testid="context-menu-radio-group"
|
|
291
|
+
>
|
|
292
|
+
{children}
|
|
293
|
+
</div>
|
|
190
294
|
);
|
|
191
295
|
}
|
|
192
296
|
|
|
193
297
|
function ContextMenuRadioItem({
|
|
194
298
|
className,
|
|
195
299
|
children,
|
|
196
|
-
|
|
197
|
-
|
|
300
|
+
checked,
|
|
301
|
+
onClick,
|
|
302
|
+
...rest
|
|
303
|
+
}: {
|
|
304
|
+
className?: string;
|
|
305
|
+
children?: React.ReactNode;
|
|
306
|
+
checked?: boolean;
|
|
307
|
+
disabled?: boolean;
|
|
308
|
+
} & React.HTMLAttributes<HTMLDivElement>) {
|
|
309
|
+
const { close } = useContextMenuRoot();
|
|
310
|
+
const handle = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
311
|
+
if ((rest as any).disabled) return;
|
|
312
|
+
if (onClick) onClick(e);
|
|
313
|
+
close();
|
|
314
|
+
};
|
|
198
315
|
return (
|
|
199
|
-
<
|
|
316
|
+
<div
|
|
200
317
|
data-slot="context-menu-radio-item"
|
|
201
318
|
data-testid="context-menu-radio-item"
|
|
319
|
+
data-disabled={(rest as any).disabled ? "" : undefined}
|
|
202
320
|
className={cn(
|
|
203
|
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-
|
|
321
|
+
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-xs outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
204
322
|
className,
|
|
205
323
|
)}
|
|
206
|
-
{
|
|
324
|
+
onClick={handle}
|
|
325
|
+
{...rest}
|
|
207
326
|
>
|
|
208
327
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
209
|
-
<
|
|
210
|
-
<CircleIcon className="size-2 fill-current" />
|
|
211
|
-
</ContextMenuPrimitive.ItemIndicator>
|
|
328
|
+
{checked ? <CircleIcon className="size-2 fill-current" /> : null}
|
|
212
329
|
</span>
|
|
213
330
|
{children}
|
|
214
|
-
</
|
|
331
|
+
</div>
|
|
215
332
|
);
|
|
216
333
|
}
|
|
217
334
|
|
|
218
335
|
function ContextMenuLabel({
|
|
219
336
|
className,
|
|
220
337
|
inset,
|
|
221
|
-
...
|
|
222
|
-
}:
|
|
338
|
+
...rest
|
|
339
|
+
}: {
|
|
340
|
+
className?: string;
|
|
223
341
|
inset?: boolean;
|
|
224
|
-
}) {
|
|
342
|
+
} & React.HTMLAttributes<HTMLDivElement>) {
|
|
225
343
|
return (
|
|
226
|
-
<
|
|
344
|
+
<div
|
|
227
345
|
data-slot="context-menu-label"
|
|
228
346
|
data-testid="context-menu-label"
|
|
229
347
|
data-inset={inset}
|
|
@@ -231,28 +349,29 @@ function ContextMenuLabel({
|
|
|
231
349
|
"text-foreground px-2 py-1.5 text-xs font-light data-[inset]:pl-8",
|
|
232
350
|
className,
|
|
233
351
|
)}
|
|
234
|
-
{...
|
|
352
|
+
{...rest}
|
|
235
353
|
/>
|
|
236
354
|
);
|
|
237
355
|
}
|
|
238
356
|
|
|
239
357
|
function ContextMenuSeparator({
|
|
240
358
|
className,
|
|
241
|
-
...
|
|
242
|
-
}: React.
|
|
359
|
+
...rest
|
|
360
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
243
361
|
return (
|
|
244
|
-
<
|
|
362
|
+
<div
|
|
363
|
+
role="separator"
|
|
245
364
|
data-slot="context-menu-separator"
|
|
246
365
|
data-testid="context-menu-separator"
|
|
247
366
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
248
|
-
{...
|
|
367
|
+
{...rest}
|
|
249
368
|
/>
|
|
250
369
|
);
|
|
251
370
|
}
|
|
252
371
|
|
|
253
372
|
function ContextMenuShortcut({
|
|
254
373
|
className,
|
|
255
|
-
...
|
|
374
|
+
...rest
|
|
256
375
|
}: React.ComponentProps<"span">) {
|
|
257
376
|
return (
|
|
258
377
|
<span
|
|
@@ -262,11 +381,135 @@ function ContextMenuShortcut({
|
|
|
262
381
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
263
382
|
className,
|
|
264
383
|
)}
|
|
265
|
-
{...
|
|
384
|
+
{...rest}
|
|
266
385
|
/>
|
|
267
386
|
);
|
|
268
387
|
}
|
|
269
388
|
|
|
389
|
+
// Submenu
|
|
390
|
+
interface SubmenuContextState {
|
|
391
|
+
isOpen: boolean;
|
|
392
|
+
setOpen: (open: boolean) => void;
|
|
393
|
+
triggerRef: React.RefObject<HTMLDivElement | null>;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const SubmenuContext = React.createContext<SubmenuContextState | null>(null);
|
|
397
|
+
|
|
398
|
+
function ContextMenuSub({ children }: { children: React.ReactNode }) {
|
|
399
|
+
const [open, setOpen] = React.useState(false);
|
|
400
|
+
const triggerRef = React.useRef<HTMLDivElement>(null);
|
|
401
|
+
const value = React.useMemo(
|
|
402
|
+
() => ({ isOpen: open, setOpen, triggerRef }),
|
|
403
|
+
[open],
|
|
404
|
+
);
|
|
405
|
+
return (
|
|
406
|
+
<SubmenuContext.Provider value={value}>
|
|
407
|
+
<div data-slot="context-menu-sub" data-testid="context-menu-sub">
|
|
408
|
+
{children}
|
|
409
|
+
</div>
|
|
410
|
+
</SubmenuContext.Provider>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function useSubmenu(): SubmenuContextState {
|
|
415
|
+
const ctx = React.useContext(SubmenuContext);
|
|
416
|
+
if (!ctx)
|
|
417
|
+
throw new Error(
|
|
418
|
+
"ContextMenuSub components must be used within ContextMenuSub",
|
|
419
|
+
);
|
|
420
|
+
return ctx;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function ContextMenuSubTrigger({
|
|
424
|
+
className,
|
|
425
|
+
inset,
|
|
426
|
+
children,
|
|
427
|
+
disabled,
|
|
428
|
+
...rest
|
|
429
|
+
}: {
|
|
430
|
+
className?: string;
|
|
431
|
+
inset?: boolean;
|
|
432
|
+
children?: React.ReactNode;
|
|
433
|
+
disabled?: boolean;
|
|
434
|
+
} & React.HTMLAttributes<HTMLDivElement>) {
|
|
435
|
+
const { setOpen, triggerRef } = useSubmenu();
|
|
436
|
+
|
|
437
|
+
const openSub = (e: React.SyntheticEvent) => {
|
|
438
|
+
e.stopPropagation();
|
|
439
|
+
if (!disabled) setOpen(true);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<div
|
|
444
|
+
ref={triggerRef}
|
|
445
|
+
data-slot="context-menu-sub-trigger"
|
|
446
|
+
data-testid="context-menu-sub-trigger"
|
|
447
|
+
data-inset={inset}
|
|
448
|
+
data-disabled={disabled ? "" : undefined}
|
|
449
|
+
className={cn(
|
|
450
|
+
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
451
|
+
className,
|
|
452
|
+
)}
|
|
453
|
+
onMouseEnter={openSub}
|
|
454
|
+
onClick={openSub}
|
|
455
|
+
{...rest}
|
|
456
|
+
>
|
|
457
|
+
{children}
|
|
458
|
+
<ChevronRightIcon className="ml-auto" />
|
|
459
|
+
</div>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function ContextMenuSubContent({
|
|
464
|
+
className,
|
|
465
|
+
children,
|
|
466
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
467
|
+
const { isOpen, setOpen, triggerRef } = useSubmenu();
|
|
468
|
+
const { registerContainer, unregisterContainer } = useContextMenuRoot();
|
|
469
|
+
const ref = React.useRef<HTMLDivElement | null>(null);
|
|
470
|
+
|
|
471
|
+
const [coords, setCoords] = React.useState<{ left: number; top: number }>({
|
|
472
|
+
left: 0,
|
|
473
|
+
top: 0,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
React.useEffect(() => {
|
|
477
|
+
const trig = triggerRef.current;
|
|
478
|
+
if (!trig) return;
|
|
479
|
+
const rect = trig.getBoundingClientRect();
|
|
480
|
+
setCoords({ left: rect.right, top: rect.top });
|
|
481
|
+
}, [triggerRef.current, isOpen]);
|
|
482
|
+
|
|
483
|
+
React.useEffect(() => {
|
|
484
|
+
const el = ref.current;
|
|
485
|
+
registerContainer(el);
|
|
486
|
+
return () => unregisterContainer(el);
|
|
487
|
+
}, [registerContainer, unregisterContainer]);
|
|
488
|
+
|
|
489
|
+
if (!isOpen) return null;
|
|
490
|
+
|
|
491
|
+
const node = (
|
|
492
|
+
<div
|
|
493
|
+
ref={ref}
|
|
494
|
+
data-slot="context-menu-sub-content"
|
|
495
|
+
data-testid="context-menu-sub-content"
|
|
496
|
+
className={cn(
|
|
497
|
+
"bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 font-light shadow-lg",
|
|
498
|
+
className,
|
|
499
|
+
)}
|
|
500
|
+
style={{ position: "fixed", left: coords.left, top: coords.top }}
|
|
501
|
+
onMouseLeave={() => setOpen(false)}
|
|
502
|
+
onMouseEnter={() => setOpen(true)}
|
|
503
|
+
onMouseDownCapture={(e) => e.stopPropagation()}
|
|
504
|
+
onClick={(e) => e.stopPropagation()}
|
|
505
|
+
>
|
|
506
|
+
{children}
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
return createPortal(node, document.body);
|
|
511
|
+
}
|
|
512
|
+
|
|
270
513
|
export {
|
|
271
514
|
ContextMenu,
|
|
272
515
|
ContextMenuCheckboxItem,
|
|
@@ -50,13 +50,15 @@ function DialogContent({
|
|
|
50
50
|
className,
|
|
51
51
|
children,
|
|
52
52
|
showCloseButton = true,
|
|
53
|
+
overlayClassName,
|
|
53
54
|
...props
|
|
54
55
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
55
56
|
showCloseButton?: boolean;
|
|
57
|
+
overlayClassName?: string;
|
|
56
58
|
}) {
|
|
57
59
|
return (
|
|
58
60
|
<DialogPortal data-slot="dialog-portal">
|
|
59
|
-
<DialogOverlay />
|
|
61
|
+
<DialogOverlay className={overlayClassName} />
|
|
60
62
|
<DialogPrimitive.Content
|
|
61
63
|
data-slot="dialog-content"
|
|
62
64
|
className={cn(
|