/** * MoreMenu - Dropdown menu behind a "..." icon for less-frequent toolbar actions. * * Groups: Teams, Settings, Extensions, Search, Schedules, Export (session-only), Analyze (session-only). * 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, Calendar, FileText, MoreHorizontal, Puzzle, Search, Settings, Type, Users, } 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 openExtensionsTab = useStore((s) => s.openExtensionsTab); const openSessionReport = useStore((s) => s.openSessionReport); const openSchedulesTab = useStore((s) => s.openSchedulesTab); const openSettingsTab = useStore((s) => s.openSettingsTab); const openTeamsTab = useStore((s) => s.openTeamsTab); // 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: 'teams', label: 'Teams', icon: Users, onClick: () => { openTeamsTab(); setIsOpen(false); }, }, { id: 'settings', label: 'Settings', icon: Settings, onClick: () => { openSettingsTab(); setIsOpen(false); }, }, { id: 'extensions', label: 'Extensions', icon: Puzzle, onClick: () => { openExtensionsTab(); setIsOpen(false); }, }, { id: 'search', label: 'Search', icon: Search, shortcut: formatShortcut('K'), onClick: () => { openCommandPalette(); setIsOpen(false); }, }, { id: 'schedules', label: 'Schedules', icon: Calendar, onClick: () => { openSchedulesTab(); 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 renderItem = (item: MenuItem): React.JSX.Element => ( ); const separator = (
); return (
{/* Trigger button */} {/* Dropdown menu */} {isOpen && (
{topItems.map(renderItem)} {sessionItems.length > 0 && ( <> {separator} {sessionItems.map(renderItem)} )}
)}
); };