返回 DeepSeek-Reasonix
TabAddMenu.tsx
根目录 / desktop / frontend / src / components / TabContainer / TabAddMenu.tsx
1 // TabAddMenu is the dropdown opened by the + button in the dock's tab bar.
2 // It lists the addable tab types (files / changed / overview / instructions);
3 // selecting one appends that tab and activates it. Every type can be added
4 // repeatedly, so the list shows no current-selection checkmark. Other panel
5 // types (terminal, browser, remote) are not exposed yet.
6
7 import { useEffect, useLayoutEffect, useRef } from "react";
8 import type { RefObject } from "react";
9 import { useT } from "../../lib/i18n";
10 import { Activity, Compass, FileDiff, FileText } from "lucide-react";
11 import type { ComponentType } from "react";
12 import { desktopHost } from "../../lib/desktopHost";
13 import type { TabType } from "../../store/activityBar";
14
15 interface AddableTab {
16 type: TabType;
17 labelKey: string;
18 icon: ComponentType<{ size?: number | string; className?: string }>;
19 }
20
21 const ADDABLE_TABS: AddableTab[] = [
22 { type: "file", labelKey: "workspace.filesTab", icon: FileText },
23 { type: "changed", labelKey: "workspace.changedTab", icon: FileDiff },
24 { type: "context", labelKey: "rightDock.overview", icon: Activity },
25 ];
26
27 const BROWSER_TAB: AddableTab = { type: "browser", labelKey: "rightDock.browser", icon: Compass };
28
29 interface TabAddMenuProps {
30 onPick: (type: TabType, label: string) => void;
31 onClose: () => void;
32 /** The + button this menu anchors to (opens under it, left-aligned). */
33 anchorRef: RefObject<HTMLButtonElement | null>;
34 }
35
36 export function TabAddMenu({ onPick, onClose, anchorRef }: TabAddMenuProps) {
37 const t = useT();
38 const menuRef = useRef<HTMLDivElement>(null);
39 // Anchor the menu under the + button, its left edge aligned to the button's.
40 // Coordinates are relative to the dock; when the menu would overflow the
41 // dock (narrow dock / window near the screen edge), flip the alignment
42 // inward so it stays fully visible.
43 useLayoutEffect(() => {
44 const el = menuRef.current;
45 const anchor = anchorRef.current;
46 const dock = el?.closest(".tab-container");
47 if (!el || !anchor || !dock) return;
48 const menuRect = el.getBoundingClientRect();
49 const anchorRect = anchor.getBoundingClientRect();
50 const dockRect = dock.getBoundingClientRect();
51 const gap = 4;
52 let left = anchorRect.left - dockRect.left;
53 let top = anchorRect.bottom - dockRect.top + gap;
54 if (left + menuRect.width > dockRect.width - gap) {
55 left = dockRect.width - menuRect.width - gap;
56 }
57 if (top + menuRect.height > dockRect.height - gap) {
58 top = Math.max(gap, anchorRect.top - dockRect.top - menuRect.height - gap);
59 }
60 el.style.left = `${left}px`;
61 el.style.top = `${top}px`;
62 }, [anchorRef]);
63
64 // Close on outside click (anywhere except the menu itself or the + button)
65 // or Escape. The menu is only mounted while open, so listeners die with it.
66 useEffect(() => {
67 const onPointerDown = (event: PointerEvent) => {
68 const target = event.target;
69 if (!(target instanceof Node)) return;
70 if (menuRef.current?.contains(target)) return;
71 if (anchorRef.current?.contains(target)) return;
72 onClose();
73 };
74 const onKeyDown = (event: KeyboardEvent) => {
75 if (event.key === "Escape") onClose();
76 };
77 window.addEventListener("pointerdown", onPointerDown);
78 window.addEventListener("keydown", onKeyDown);
79 return () => {
80 window.removeEventListener("pointerdown", onPointerDown);
81 window.removeEventListener("keydown", onKeyDown);
82 };
83 }, [anchorRef, onClose]);
84
85 return (
86 <div className="tab-add-menu" ref={menuRef} role="menu" aria-label={t("rightDock.addTab")}>
87 {(desktopHost().browser !== undefined ? [...ADDABLE_TABS, BROWSER_TAB] : ADDABLE_TABS).map((item) => (
88 <button
89 key={item.type}
90 type="button"
91 role="menuitem"
92 className="tab-add-menu__item"
93 onClick={() => {
94 onPick(item.type, t(item.labelKey as never));
95 onClose();
96 }}
97 >
98 <item.icon size={14} />
99 <span>{t(item.labelKey as never)}</span>
100 </button>
101 ))}
102 </div>
103 );
104 }
105
105 lines Plain Text