@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.
Files changed (42) hide show
  1. package/dist/components/ui/context-menu.d.ts +45 -17
  2. package/dist/components/ui/context-menu.js +197 -32
  3. package/dist/components/ui/context-menu.js.map +1 -1
  4. package/dist/components/ui/dialog.d.ts +2 -1
  5. package/dist/components/ui/dialog.js +2 -2
  6. package/dist/components/ui/dialog.js.map +1 -1
  7. package/dist/editor/ContextMenu.js +6 -3
  8. package/dist/editor/ContextMenu.js.map +1 -1
  9. package/dist/editor/MainLayout.js +1 -1
  10. package/dist/editor/MainLayout.js.map +1 -1
  11. package/dist/editor/client/hooks/useGlobalEditorEvents.js +12 -2
  12. package/dist/editor/client/hooks/useGlobalEditorEvents.js.map +1 -1
  13. package/dist/editor/commands/itemCommands.js +5 -10
  14. package/dist/editor/commands/itemCommands.js.map +1 -1
  15. package/dist/editor/context-menu/InsertMenu.js +24 -3
  16. package/dist/editor/context-menu/InsertMenu.js.map +1 -1
  17. package/dist/editor/page-viewer/PageViewerFrame.js +4 -1
  18. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  19. package/dist/editor/sidebar/SidebarView.js +1 -1
  20. package/dist/editor/sidebar/SidebarView.js.map +1 -1
  21. package/dist/editor/ui/ItemNameDialogNew.js +1 -7
  22. package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
  23. package/dist/editor/ui/SimpleMenu.js.map +1 -1
  24. package/dist/editor/ui/Splitter.js +16 -14
  25. package/dist/editor/ui/Splitter.js.map +1 -1
  26. package/dist/revision.d.ts +2 -2
  27. package/dist/revision.js +2 -2
  28. package/dist/styles.css +12 -6
  29. package/package.json +1 -1
  30. package/src/components/ui/context-menu.tsx +387 -144
  31. package/src/components/ui/dialog.tsx +3 -1
  32. package/src/editor/ContextMenu.tsx +11 -1
  33. package/src/editor/MainLayout.tsx +9 -13
  34. package/src/editor/client/hooks/useGlobalEditorEvents.ts +14 -2
  35. package/src/editor/commands/itemCommands.tsx +11 -12
  36. package/src/editor/context-menu/InsertMenu.tsx +33 -3
  37. package/src/editor/page-viewer/PageViewerFrame.tsx +6 -1
  38. package/src/editor/sidebar/SidebarView.tsx +6 -9
  39. package/src/editor/ui/ItemNameDialogNew.tsx +2 -12
  40. package/src/editor/ui/SimpleMenu.tsx +0 -1
  41. package/src/editor/ui/Splitter.tsx +16 -17
  42. 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
- function ContextMenu({
8
- ...props
9
- }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
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
- <ContextMenuPrimitive.Root
12
- data-slot="context-menu"
13
- data-testid="context-menu"
14
- {...props}
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
- function ContextMenuTrigger({
20
- ...props
21
- }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
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
- <ContextMenuPrimitive.Trigger
128
+ <span
24
129
  data-slot="context-menu-trigger"
25
130
  data-testid="context-menu-trigger"
26
- {...props}
27
- />
131
+ onContextMenu={onContextMenu}
132
+ {...rest}
133
+ >
134
+ {children}
135
+ </span>
28
136
  );
29
137
  }
30
138
 
31
139
  function ContextMenuGroup({
32
- ...props
33
- }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
140
+ children,
141
+ className,
142
+ }: React.HTMLAttributes<HTMLDivElement>) {
34
143
  return (
35
- <ContextMenuPrimitive.Group
144
+ <div
36
145
  data-slot="context-menu-group"
37
146
  data-testid="context-menu-group"
38
- {...props}
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 ContextMenuRadioGroup({
68
- ...props
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 ContextMenuSubTrigger({
158
+ function ContextMenuContent({
80
159
  className,
81
- inset,
82
160
  children,
83
- ...props
84
- }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
85
- inset?: boolean;
86
- }) {
87
- return (
88
- <ContextMenuPrimitive.SubTrigger
89
- data-slot="context-menu-sub-trigger"
90
- data-testid="context-menu-sub-trigger"
91
- data-inset={inset}
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
- "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default 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]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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
- {...props}
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
- <ChevronRightIcon className="ml-auto" />
100
- </ContextMenuPrimitive.SubTrigger>
200
+ </div>
101
201
  );
102
- }
103
202
 
104
- function ContextMenuSubContent({
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
- function ContextMenuContent({
124
- className,
125
- ...props
126
- }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
127
- return (
128
- <ContextMenuPrimitive.Portal>
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
- ...props
147
- }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
148
- inset?: boolean;
149
- variant?: "default" | "destructive";
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
- <ContextMenuPrimitive.Item
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 data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default 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]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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
- {...props}
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
- ...props
171
- }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
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
- <ContextMenuPrimitive.CheckboxItem
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-default 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]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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
- checked={checked}
181
- {...props}
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
- <ContextMenuPrimitive.ItemIndicator>
185
- <CheckIcon className="size-4" />
186
- </ContextMenuPrimitive.ItemIndicator>
279
+ {checked ? <CheckIcon className="size-4" /> : null}
187
280
  </span>
188
281
  {children}
189
- </ContextMenuPrimitive.CheckboxItem>
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
- ...props
197
- }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
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
- <ContextMenuPrimitive.RadioItem
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-default 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]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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
- {...props}
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
- <ContextMenuPrimitive.ItemIndicator>
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
- </ContextMenuPrimitive.RadioItem>
331
+ </div>
215
332
  );
216
333
  }
217
334
 
218
335
  function ContextMenuLabel({
219
336
  className,
220
337
  inset,
221
- ...props
222
- }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
338
+ ...rest
339
+ }: {
340
+ className?: string;
223
341
  inset?: boolean;
224
- }) {
342
+ } & React.HTMLAttributes<HTMLDivElement>) {
225
343
  return (
226
- <ContextMenuPrimitive.Label
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
- {...props}
352
+ {...rest}
235
353
  />
236
354
  );
237
355
  }
238
356
 
239
357
  function ContextMenuSeparator({
240
358
  className,
241
- ...props
242
- }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
359
+ ...rest
360
+ }: React.HTMLAttributes<HTMLDivElement>) {
243
361
  return (
244
- <ContextMenuPrimitive.Separator
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
- {...props}
367
+ {...rest}
249
368
  />
250
369
  );
251
370
  }
252
371
 
253
372
  function ContextMenuShortcut({
254
373
  className,
255
- ...props
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
- {...props}
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(