/** * MoreMenu - Dropdown menu behind a "..." icon for less-frequent toolbar actions. * * Groups: Search, Export (session-only), Analyze (session-only), Settings. * Closes on outside click or Escape. */ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useStore } from '@renderer/store'; import { triggerDownload } from '@renderer/utils/sessionExporter'; import { formatShortcut } from '@renderer/utils/stringUtils'; import { Activity, Braces, FileText, MoreHorizontal, Search, Settings, Type } from 'lucide-react'; import type { SessionDetail } from '@renderer/types/data'; import type { Tab } from '@renderer/types/tabs'; import type { ExportFormat } from '@renderer/utils/sessionExporter'; interface MoreMenuProps { activeTab: Tab | undefined; activeTabSessionDetail: SessionDetail | null; activeTabId: string | null; } interface MenuItem { id: string; label: string; icon: React.ComponentType<{ className?: string }>; shortcut?: string; onClick: () => void; } export const MoreMenu = ({ activeTab, activeTabSessionDetail, activeTabId, }: Readonly): React.JSX.Element => { const [isOpen, setIsOpen] = useState(false); const [buttonHover, setButtonHover] = useState(false); const [hoveredId, setHoveredId] = useState(null); const containerRef = useRef(null); const openCommandPalette = useStore((s) => s.openCommandPalette); const openSettingsTab = useStore((s) => s.openSettingsTab); const openSessionReport = useStore((s) => s.openSessionReport); // Close on outside click useEffect(() => { if (!isOpen) return; const handleClickOutside = (event: MouseEvent): void => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [isOpen]); // Close on Escape useEffect(() => { if (!isOpen) return; const handleEscape = (event: KeyboardEvent): void => { if (event.key === 'Escape') { setIsOpen(false); } }; document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); }, [isOpen]); const handleExport = useCallback( (format: ExportFormat) => { if (activeTabSessionDetail) { triggerDownload(activeTabSessionDetail, format); } setIsOpen(false); }, [activeTabSessionDetail] ); const isSessionWithData = activeTab?.type === 'session' && activeTabSessionDetail != null; // Build menu sections const topItems: MenuItem[] = [ { id: 'search', label: 'Search', icon: Search, shortcut: formatShortcut('K'), onClick: () => { openCommandPalette(); setIsOpen(false); }, }, ]; const sessionItems: MenuItem[] = isSessionWithData ? [ { id: 'export-md', label: 'Export as Markdown', icon: FileText, shortcut: '.md', onClick: () => handleExport('markdown'), }, { id: 'export-json', label: 'Export as JSON', icon: Braces, shortcut: '.json', onClick: () => handleExport('json'), }, { id: 'export-txt', label: 'Export as Plain Text', icon: Type, shortcut: '.txt', onClick: () => handleExport('plaintext'), }, { id: 'analyze', label: 'Analyze Session', icon: Activity, onClick: () => { if (activeTabId) openSessionReport(activeTabId); setIsOpen(false); }, }, ] : []; const bottomItems: MenuItem[] = [ { id: 'settings', label: 'Settings', icon: Settings, shortcut: formatShortcut(','), onClick: () => { openSettingsTab(); setIsOpen(false); }, }, ]; const renderItem = (item: MenuItem): React.JSX.Element => ( ); const separator = (
); return (
{/* Trigger button */} {/* Dropdown menu */} {isOpen && (
{topItems.map(renderItem)} {sessionItems.length > 0 && ( <> {separator} {sessionItems.map(renderItem)} )} {separator} {bottomItems.map(renderItem)}
)}
); };