import React, { useCallback, useEffect, useMemo, useRef, useId } from 'react'; import { createPortal } from 'react-dom'; import { ChevronIcon, TrashIcon, EditIcon, FolderIcon, SidebarCollapseIcon, LogoutIcon, ChevronDownIcon, SettingsIcon, CheckIcon, PlusIcon, FolderPlusIcon, RestoreIcon, SunIcon, MoonIcon, DesktopIcon, } from '../ui/icons'; import PanelHeader from '../ui/PanelHeader'; import useFloatingMenu from '../ui/useFloatingMenu'; import { getTagColorStyle } from '../utils/colors'; import { useSidebarContext } from './SidebarContext'; 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)} onDoubleClick={() => { if (canToggle) { onToggle(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, tags = [], untaggedFilterId = null, activeTagIds = [], onToggleTagFilter, correspondents = [], activeCorrespondentIds = [], onToggleCorrespondentFilter, onManageTags, onManageCorrespondents, onCreateTag, onCreateCorrespondent, searchQuery = '', onSearchChange, onSearchSubmit, onSearchClear, isFilterActive, onLogout, status, tenantName, tenants = [], activeTenantId = null, onSelectTenant, onOpenSettings, }) => { const { setCollapsed, neutralHue, setNeutralHue, resetNeutralHue, themeMode, cycleThemeMode, themeModes, } = useSidebarContext(); const handleCollapse = useCallback(() => { setCollapsed(true); }, [setCollapsed]); 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 untaggedActive = untaggedFilterId ? activeTagSet.has(untaggedFilterId) : false; 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(() => { resetNeutralHue(); }, [resetNeutralHue]); const handleNeutralHueChange = useCallback( (value) => { if (value === '') { resetNeutralHue(); return; } const parsed = Number(value); if (Number.isNaN(parsed)) { return; } setNeutralHue(parsed); }, [resetNeutralHue, setNeutralHue], ); const themeModeList = themeModes && themeModes.length ? themeModes : THEME_MODES; const themeModeIndex = themeModeList.indexOf(themeMode); const safeThemeModeIndex = themeModeIndex === -1 ? 0 : themeModeIndex; const resolvedThemeMode = themeModeList[safeThemeModeIndex] || THEME_MODES[0]; const nextThemeMode = themeModeList[(safeThemeModeIndex + 1) % themeModeList.length]; const themeModeLabel = THEME_MODE_LABELS[resolvedThemeMode] || THEME_MODE_LABELS.system; const nextThemeLabel = THEME_MODE_LABELS[nextThemeMode] || THEME_MODE_LABELS.system; const themeModeIcon = resolvedThemeMode === 'dark' ? : resolvedThemeMode === 'light' ? : ; const handleThemeModeToggle = useCallback(() => { cycleThemeMode(); }, [cycleThemeMode]); 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 { isOpen: tenantMenuOpen, toggle: toggleTenantMenuFloating, close: closeTenantMenu, menuRef: tenantMenuRef, menuStyle: tenantMenuStyle, updatePosition: refreshTenantMenuPosition, } = useFloatingMenu({ anchorRef: tenantButtonRef, minWidth: 220, offset: 6, }); const showTenantList = tenants.length > 1; const toggleTenantMenu = useCallback(() => { if (!tenantMenuOpen && tenants.length === 0 && onSelectTenant) { onSelectTenant(null, { refreshOnly: true }); } toggleTenantMenuFloating(); }, [tenantMenuOpen, tenants.length, onSelectTenant, toggleTenantMenuFloating]); useEffect(() => { if (tenantMenuOpen) { refreshTenantMenuPosition(); } }, [tenantMenuOpen, refreshTenantMenuPosition, tenants.length]); const handleTenantSelect = useCallback( (tenant) => { const targetId = tenant?.id || null; if (!targetId) { return; } closeTenantMenu(); onSelectTenant?.(tenant); }, [closeTenantMenu, onSelectTenant], ); const handleLogoutFromMenu = useCallback(() => { closeTenantMenu(); onLogout?.(); }, [closeTenantMenu, onLogout]); const handleSettingsFromMenu = useCallback(() => { closeTenantMenu(); onOpenSettings?.(); }, [closeTenantMenu, 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 };