agent-ecosystem/src/renderer/components/team/editor/EditorSelectionMenu.tsx
iliya 5b0c7d13fc feat: add project editor with drag & drop file management
- Backend: ProjectFileService with file CRUD, search, git status, file watcher
- IPC: 12 editor channels with security validation and path containment
- Store: editorSlice with multi-tab management, draft persistence, conflict detection
- UI: CodeMirror 6 editor, file tree with DnD, search-in-files, context menus
- Move: fs.rename with EXDEV fallback, full path remapping across all caches
- Tests: comprehensive coverage for services, IPC handlers, store, and utilities
2026-02-28 23:40:41 +02:00

110 lines
3.7 KiB
TypeScript

/**
* Floating action menu shown near text selection in the editor.
*
* Positioned absolutely relative to the editor content container.
* Uses onMouseDown preventDefault to avoid deselecting text in CM6.
*/
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { ListTodo, MessageSquare } from 'lucide-react';
import type { EditorSelectionInfo } from '@shared/types/editor';
// =============================================================================
// Types
// =============================================================================
interface EditorSelectionMenuProps {
info: EditorSelectionInfo;
/** Bounding rect of the editor content container (for viewport → container conversion) */
containerRect: DOMRect;
onSendMessage: () => void;
onCreateTask: () => void;
}
// =============================================================================
// Constants
// =============================================================================
const MENU_GAP = 8; // px gap between selection end and menu
const MENU_WIDTH = 68; // approximate menu width for clamping
const MENU_HEIGHT = 32; // approximate menu height for clamping
// =============================================================================
// Component
// =============================================================================
export const EditorSelectionMenu = ({
info,
containerRect,
onSendMessage,
onCreateTask,
}: EditorSelectionMenuProps): React.ReactElement | null => {
if (!info.text.trim()) return null;
// Convert viewport coords → container-relative
const rawTop = info.screenRect.top - containerRect.top;
const rawLeft = info.screenRect.right - containerRect.left + MENU_GAP;
// Check if selection is within visible container bounds
const selTopInContainer = info.screenRect.top - containerRect.top;
const selBottomInContainer = info.screenRect.bottom - containerRect.top;
if (selBottomInContainer < 0 || selTopInContainer > containerRect.height) {
return null; // selection is scrolled out of view
}
// Clamp to container bounds
const top = Math.max(0, Math.min(rawTop, containerRect.height - MENU_HEIGHT));
const left =
rawLeft + MENU_WIDTH > containerRect.width
? info.screenRect.right - containerRect.left - MENU_WIDTH - MENU_GAP // flip to left side
: rawLeft;
return (
<div
className="pointer-events-auto absolute z-20 flex items-center gap-0.5 rounded-md border border-border-emphasis bg-surface-overlay p-0.5 shadow-lg animate-in fade-in-0 zoom-in-95"
style={{ top, left: Math.max(0, left) }}
>
<MenuButton
icon={<MessageSquare className="size-3.5" />}
label="Write Teammate"
onClick={onSendMessage}
/>
<MenuButton
icon={<ListTodo className="size-3.5" />}
label="Create Task"
onClick={onCreateTask}
/>
</div>
);
};
// =============================================================================
// Menu button
// =============================================================================
interface MenuButtonProps {
icon: React.ReactNode;
label: string;
onClick: () => void;
}
const MenuButton = ({ icon, label, onClick }: MenuButtonProps): React.ReactElement => (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
tabIndex={-1}
aria-label={label}
onClick={onClick}
onMouseDown={(e) => e.preventDefault()} // prevent CM6 selection loss
className="rounded p-1.5 text-text-secondary transition-colors hover:bg-surface-raised hover:text-text"
>
{icon}
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{label}
</TooltipContent>
</Tooltip>
);