import React, { useCallback, useEffect, useMemo, useRef, useState, useId } from 'react'; import { ChevronIcon, TrashIcon, EditIcon, FolderIcon, ChevronsLeftIcon, LogoutIcon, ChevronDownIcon, SettingsIcon, CheckIcon, PlusIcon, FolderPlusIcon, RestoreIcon, SunIcon, MoonIcon, DesktopIcon, } from '../ui/icons'; import PanelHeader from '../ui/PanelHeader'; import { getTagColorStyle } from '../utils/colors'; const FolderNode = ({ node, depth, isSelected, onToggle, onSelect, onDrop, onDragOver, onDragLeave, onDelete, onRename, renderChildren, onFolderDragStart, onFolderDragEnd, draggingFolderId, }) => { const isRoot = node.id === 'root'; const hasChildren = Boolean(node.hasChildren); const canToggle = hasChildren || !node.loaded; const showChevron = canToggle; const icon = showChevron ? : null; const canDrag = !isRoot; const isDragging = draggingFolderId === node.id; const isExpanded = Boolean(node.expanded); const rowClasses = ['folder-row']; if (isSelected) { rowClasses.push('active'); } const handleToggleClick = (event) => { event.stopPropagation(); if (canToggle) { onToggle(node.id); } }; return (
  • onSelect(node.id)} onDragOver={(event) => onDragOver(event, node.id)} onDragLeave={onDragLeave} onDrop={(event) => onDrop(event, node.id)} onDragStart={(event) => { if (!canDrag || !onFolderDragStart) return; onFolderDragStart(event, node.id); }} onDragEnd={(event) => { if (onFolderDragEnd) { onFolderDragEnd(event); } }} > {icon} {node.name} {node.id !== 'root' && (
    )}
    {isExpanded && node.children.length > 0 && ( )}
  • ); }; const THEME_MODES = ['system', 'light', 'dark']; const THEME_MODE_LABELS = { system: 'System', light: 'Light', dark: 'Dark', }; const Sidebar = ({ folderNodes, onToggle, onSelect, onDrop, onDragOver, onDragLeave, onDeleteFolder, onRenameFolder, selectedFolder, onFolderDragStart, onFolderDragEnd, draggedFolderId, onCreateFolder, creatingFolder = false, onNeutralHueChange, neutralHue, themeMode = 'system', onThemeModeChange, tags = [], activeTagIds = [], onToggleTagFilter, correspondents = [], activeCorrespondentIds = [], onToggleCorrespondentFilter, onManageTags, onManageCorrespondents, onCreateTag, onCreateCorrespondent, searchQuery = '', onSearchChange, onSearchSubmit, onSearchClear, isFilterActive, onLogout, status, onCollapse, tenantName, tenants = [], activeTenantId = null, onSelectTenant, onOpenSettings, }) => { const neutralHueInputId = useId(); const sortedCorrespondents = useMemo(() => { if (!Array.isArray(correspondents)) { return []; } return correspondents .filter((entry) => entry?.name) .slice() .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); }, [correspondents]); const activeCorrespondentSet = useMemo( () => new Set(activeCorrespondentIds || []), [activeCorrespondentIds], ); const handleToggleTag = onToggleTagFilter || (() => {}); const activeTagSet = new Set(activeTagIds); const handleManageTags = onManageTags || (() => {}); const handleManageCorrespondents = onManageCorrespondents || (() => {}); const handleCreateTag = useCallback(async () => { const input = window.prompt('New tag name'); if (!input) { return; } const trimmed = input.trim(); if (!trimmed) { return; } try { await onCreateTag?.(trimmed); } catch (error) { console.error('[sidebar] failed to create tag', error); } }, [onCreateTag]); const handleCreateCorrespondent = useCallback(async () => { const input = window.prompt('New correspondent name'); if (!input) { return; } const trimmed = input.trim(); if (!trimmed) { return; } try { await onCreateCorrespondent?.(trimmed); } catch (error) { console.error('[sidebar] failed to create correspondent', error); } }, [onCreateCorrespondent]); const handleCreateFolder = useCallback(() => { if (creatingFolder) { return; } onCreateFolder?.(); }, [creatingFolder, onCreateFolder]); const handleNeutralHueReset = useCallback(() => { onNeutralHueChange?.(''); }, [onNeutralHueChange]); const themeModeIndex = THEME_MODES.indexOf(themeMode); const safeThemeModeIndex = themeModeIndex === -1 ? 0 : themeModeIndex; const resolvedThemeMode = THEME_MODES[safeThemeModeIndex]; const nextThemeMode = THEME_MODES[(safeThemeModeIndex + 1) % THEME_MODES.length]; const themeModeLabel = THEME_MODE_LABELS[resolvedThemeMode]; const nextThemeLabel = THEME_MODE_LABELS[nextThemeMode]; const themeModeIcon = resolvedThemeMode === 'dark' ? : resolvedThemeMode === 'light' ? : ; const handleThemeModeToggle = useCallback(() => { if (!onThemeModeChange) { return; } const currentIndex = THEME_MODES.indexOf(themeMode); const safeIndex = currentIndex === -1 ? 0 : currentIndex; const nextMode = THEME_MODES[(safeIndex + 1) % THEME_MODES.length]; onThemeModeChange(nextMode); }, [onThemeModeChange, themeMode]); const handleSearchInputChange = useCallback( (event) => { onSearchChange?.(event.target.value); }, [onSearchChange], ); const handleSearchFormSubmit = useCallback( (event) => { event.preventDefault(); onSearchSubmit?.(); }, [onSearchSubmit], ); const handleSearchClear = useCallback(() => { onSearchClear?.(); }, [onSearchClear]); const tenantButtonRef = useRef(null); const tenantMenuRef = useRef(null); const [tenantMenuOpen, setTenantMenuOpen] = useState(false); const showTenantList = tenants.length > 1; const toggleTenantMenu = useCallback(() => { const next = !tenantMenuOpen; setTenantMenuOpen(next); if (next && tenants.length === 0 && onSelectTenant) { onSelectTenant(null, { refreshOnly: true }); } }, [tenantMenuOpen, tenants.length, onSelectTenant]); useEffect(() => { if (!tenantMenuOpen) { return undefined; } const handlePointer = (event) => { const menuNode = tenantMenuRef.current; const buttonNode = tenantButtonRef.current; if (!menuNode) return; if (menuNode.contains(event.target)) return; if (buttonNode && buttonNode.contains(event.target)) return; setTenantMenuOpen(false); }; const handleKeyDown = (event) => { if (event.key === 'Escape') { setTenantMenuOpen(false); } }; document.addEventListener('mousedown', handlePointer); document.addEventListener('touchstart', handlePointer); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handlePointer); document.removeEventListener('touchstart', handlePointer); document.removeEventListener('keydown', handleKeyDown); }; }, [tenantMenuOpen]); const handleTenantSelect = useCallback( (tenant) => { const targetId = tenant?.id || null; if (!targetId) { return; } setTenantMenuOpen(false); onSelectTenant?.(tenant); }, [onSelectTenant], ); const handleLogoutFromMenu = useCallback(() => { setTenantMenuOpen(false); onLogout?.(); }, [onLogout]); const handleSettingsFromMenu = useCallback(() => { setTenantMenuOpen(false); onOpenSettings?.(); }, [onOpenSettings]); const renderNodes = useCallback( (ids, depth) => ids.map((id) => { const node = folderNodes.get(id); if (!node) return null; return ( onToggle(id)} onSelect={onSelect} onDrop={onDrop} onDragOver={onDragOver} onDragLeave={onDragLeave} onDelete={onDeleteFolder} onRename={onRenameFolder} renderChildren={renderNodes} onFolderDragStart={onFolderDragStart} onFolderDragEnd={onFolderDragEnd} draggingFolderId={draggedFolderId} /> ); }), [ folderNodes, selectedFolder, onToggle, onSelect, onDrop, onDragOver, onDragLeave, onDeleteFolder, onRenameFolder, onFolderDragStart, onFolderDragEnd, draggedFolderId, ], ); const rootNode = folderNodes.get('root'); const themeControls = typeof neutralHue === 'number' || typeof neutralHue === 'string' ? (

    Theme

    ) : null; return ( ); }; export default Sidebar; export { FolderNode };