diff --git a/src/renderer/components/common/ExportDropdown.tsx b/src/renderer/components/common/ExportDropdown.tsx new file mode 100644 index 00000000..c35f71d9 --- /dev/null +++ b/src/renderer/components/common/ExportDropdown.tsx @@ -0,0 +1,142 @@ +/** + * ExportDropdown - Download icon button with dropdown for exporting session data. + * + * Supports three formats: Markdown (.md), JSON (.json), Plain Text (.txt). + * Follows the same close-on-outside-click / Escape patterns as RepositoryDropdown. + */ + +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import { triggerDownload } from '@renderer/utils/sessionExporter'; +import { Braces, Download, FileText, Type } from 'lucide-react'; + +import type { SessionDetail } from '@renderer/types/data'; +import type { ExportFormat } from '@renderer/utils/sessionExporter'; + +interface ExportDropdownProps { + sessionDetail: SessionDetail; +} + +interface FormatOption { + format: ExportFormat; + label: string; + icon: React.ComponentType<{ className?: string }>; + ext: string; +} + +const FORMAT_OPTIONS: FormatOption[] = [ + { format: 'markdown', label: 'Markdown', icon: FileText, ext: '.md' }, + { format: 'json', label: 'JSON', icon: Braces, ext: '.json' }, + { format: 'plaintext', label: 'Plain Text', icon: Type, ext: '.txt' }, +]; + +export const ExportDropdown = ({ + sessionDetail, +}: Readonly): React.JSX.Element => { + const [isOpen, setIsOpen] = useState(false); + const [buttonHover, setButtonHover] = useState(false); + const [hoveredFormat, setHoveredFormat] = useState(null); + const containerRef = useRef(null); + + // 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) => { + triggerDownload(sessionDetail, format); + setIsOpen(false); + }, + [sessionDetail] + ); + + return ( +
+ {/* Trigger button */} + + + {/* Dropdown menu */} + {isOpen && ( +
+ {/* Header */} +
+ Export Session +
+ + {/* Format options */} + {FORMAT_OPTIONS.map((option) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx index 6748a0a5..61915d35 100644 --- a/src/renderer/components/layout/TabBar.tsx +++ b/src/renderer/components/layout/TabBar.tsx @@ -17,6 +17,8 @@ import { useStore } from '@renderer/store'; import { Bell, PanelLeft, Plus, RefreshCw, Search, Settings } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; +import { ExportDropdown } from '../common/ExportDropdown'; + import { SortableTab } from './SortableTab'; import { TabContextMenu } from './TabContextMenu'; @@ -50,6 +52,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => { pinnedSessionIds, toggleHideSession, hiddenSessionIds, + tabSessionData, } = useStore( useShallow((s) => ({ pane: s.paneLayout.panes.find((p) => p.id === paneId), @@ -76,6 +79,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => { pinnedSessionIds: s.pinnedSessionIds, toggleHideSession: s.toggleHideSession, hiddenSessionIds: s.hiddenSessionIds, + tabSessionData: s.tabSessionData, })) ); @@ -89,6 +93,11 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => { // Derive stable tab IDs array for SortableContext const tabIds = useMemo(() => openTabs.map((t) => t.id), [openTabs]); + // Derive session detail for the active tab (used by export dropdown) + const activeTabSessionDetail = activeTabId + ? (tabSessionData[activeTabId]?.sessionDetail ?? null) + : null; + // Hover states for buttons const [expandHover, setExpandHover] = useState(false); const [refreshHover, setRefreshHover] = useState(false); @@ -372,6 +381,11 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => { + {/* Export dropdown - show only for session tabs with loaded data */} + {activeTab?.type === 'session' && activeTabSessionDetail && ( + + )} + {/* Notifications bell icon */}