返回 DeepSeek-Reasonix
FloatingMenu.tsx
根目录 / desktop / frontend / src / components / FloatingMenu.tsx
1 import type { MouseEvent as ReactMouseEvent, ReactNode } from "react";
2 import { useMemo } from "react";
3 import { createPortal } from "react-dom";
4
5 const FLOATING_MENU_MARGIN = 8;
6
7 export interface FloatingMenuItem {
8 icon?: ReactNode;
9 label: ReactNode;
10 onSelect: () => void;
11 disabled?: boolean;
12 }
13
14 function clampFloatingMenuPosition(x: number, y: number, width: number, height: number): { left: number; top: number } {
15 if (typeof window === "undefined") return { left: x, top: y };
16 const maxLeft = Math.max(FLOATING_MENU_MARGIN, window.innerWidth - width - FLOATING_MENU_MARGIN);
17 const maxTop = Math.max(FLOATING_MENU_MARGIN, window.innerHeight - height - FLOATING_MENU_MARGIN);
18 return {
19 left: Math.min(maxLeft, Math.max(FLOATING_MENU_MARGIN, x)),
20 top: Math.min(maxTop, Math.max(FLOATING_MENU_MARGIN, y)),
21 };
22 }
23
24 export function FloatingMenu({
25 x,
26 y,
27 width = 240,
28 estimatedHeight,
29 className = "",
30 children,
31 }: {
32 x: number;
33 y: number;
34 width?: number;
35 estimatedHeight: number;
36 className?: string;
37 children: ReactNode;
38 }) {
39 const pos = useMemo(() => clampFloatingMenuPosition(x, y, width, estimatedHeight), [estimatedHeight, width, x, y]);
40 if (typeof document === "undefined") return null;
41 // Portal to <body>: the menu is position:fixed, but a transformed ancestor
42 // (e.g. .workspace-preview carries a residual GSAP transform) would otherwise
43 // become its containing block and push the fixed coordinates off-screen.
44 // Rendering at the body root keeps fixed positioning relative to the viewport.
45 return createPortal(
46 <div
47 className={`floating-menu${className ? ` ${className}` : ""}`}
48 style={{ left: pos.left, top: pos.top }}
49 onMouseDown={(e) => {
50 e.preventDefault();
51 e.stopPropagation();
52 }}
53 onClick={(e) => e.stopPropagation()}
54 >
55 {children}
56 </div>,
57 document.body,
58 );
59 }
60
61 export function FloatingMenuItems({ items }: { items: FloatingMenuItem[] }) {
62 return (
63 <>
64 {items.map((item, index) => (
65 <button
66 key={index}
67 type="button"
68 disabled={item.disabled}
69 onClick={(event: ReactMouseEvent<HTMLButtonElement>) => {
70 event.stopPropagation();
71 if (!item.disabled) item.onSelect();
72 }}
73 >
74 {item.icon}
75 <span>{item.label}</span>
76 </button>
77 ))}
78 </>
79 );
80 }
81
81 lines Plain Text