/** * Radix-based context menu for the editor file tree. * * Wraps children via ContextMenu.Trigger asChild. Uses event delegation * with `data-editor-path` / `data-editor-type` attributes on tree items * to determine the right-clicked target. */ import React, { useCallback, useRef, useState } from 'react'; import * as ContextMenu from '@radix-ui/react-context-menu'; import { lastSeparatorIndex } from '@shared/utils/platformPath'; import { ClipboardCopy, FilePlus, FolderOpen, FolderPlus, ListTodo, MessageSquare, Pencil, Trash2, } from 'lucide-react'; // ============================================================================= // Types // ============================================================================= interface TargetEntry { path: string; isDir: boolean; isSensitive: boolean; } interface EditorContextMenuProps { children: React.ReactNode; projectPath: string | null; onNewFile: (parentDir: string) => void; onNewFolder: (parentDir: string) => void; onDelete: (path: string) => void; onRename: (path: string) => void; /** Trigger "Create Task" with a file mention (files only, not directories) */ onCreateTask?: (filePath: string) => void; /** Trigger "Write Teammate" with a file mention (files only, not directories) */ onSendMessage?: (filePath: string) => void; } // ============================================================================= // Component // ============================================================================= export const EditorContextMenu = ({ children, projectPath, onNewFile, onNewFolder, onDelete, onRename, onCreateTask, onSendMessage, }: EditorContextMenuProps): React.ReactElement => { const [target, setTarget] = useState(null); const triggerRef = useRef(null); const handleContextMenu = useCallback((e: React.MouseEvent) => { // Walk up from target to find the nearest element with data-editor-path let el = e.target as HTMLElement | null; while (el && el !== e.currentTarget) { const path = el.getAttribute('data-editor-path'); if (path) { const type = el.getAttribute('data-editor-type'); const sensitive = el.getAttribute('data-editor-sensitive'); setTarget({ path, isDir: type === 'directory', isSensitive: sensitive === 'true', }); return; } el = el.parentElement; } // Clicked on empty area — still show menu but with limited options setTarget(null); }, []); const parentDir = target ? target.isDir ? target.path : target.path.substring(0, lastSeparatorIndex(target.path)) : null; return (
{children}
{parentDir && ( <> onNewFile(parentDir)} > New File onNewFolder(parentDir)} > New Folder )} {target && ( <> onRename(target.path)} > Rename onDelete(target.path)} > Delete )} {target && ( <> void navigator.clipboard.writeText(target.path)} > Copy Path {projectPath && target.path.startsWith(projectPath) && ( { const relative = target.path.slice(projectPath.length + 1); void navigator.clipboard.writeText(relative); }} > Copy Relative Path )} { void window.electronAPI.showInFolder(target.path); }} > Reveal in Finder )} {/* Team actions — file only */} {target && !target.isDir && (onCreateTask || onSendMessage) && ( <> {onSendMessage && ( onSendMessage(target.path)} > Write Teammate )} {onCreateTask && ( onCreateTask(target.path)} > Create Task )} )}
); };