Files
tolaria/src/components/Sidebar.tsx
2026-04-10 13:52:39 +02:00

242 lines
8.4 KiB
TypeScript

import { useState, useRef, useCallback, memo } from 'react'
import type { VaultEntry, FolderNode, SidebarSelection, ViewFile } from '../types'
import {
KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent,
} from '@dnd-kit/core'
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'
import { FolderTree } from './FolderTree'
import {
applyCustomization,
computeReorder,
useEntryCounts,
useOutsideClick,
useSidebarCollapsed,
useSidebarSections,
} from './sidebar/sidebarHooks'
import {
ContextMenuOverlay,
CustomizeOverlay,
FavoritesSection,
type SidebarSectionProps,
SidebarTitleBar,
SidebarTopNav,
TypesSection,
ViewsSection,
} from './sidebar/SidebarSections'
interface SidebarProps {
entries: VaultEntry[]
selection: SidebarSelection
onSelect: (selection: SidebarSelection) => void
onSelectNote?: (entry: VaultEntry) => void
onCreateType?: (type: string) => void
onCreateNewType?: () => void
onCustomizeType?: (typeName: string, icon: string, color: string) => void
onUpdateTypeTemplate?: (typeName: string, template: string) => void
onReorderSections?: (orderedTypes: { typeName: string; order: number }[]) => void
onRenameSection?: (typeName: string, label: string) => void
onToggleTypeVisibility?: (typeName: string) => void
onSelectFavorite?: (entry: VaultEntry) => void
onReorderFavorites?: (orderedPaths: string[]) => void
views?: ViewFile[]
onCreateView?: () => void
onEditView?: (filename: string) => void
onDeleteView?: (filename: string) => void
folders?: FolderNode[]
onCreateFolder?: (name: string) => void
showInbox?: boolean
inboxCount?: number
onCollapse?: () => void
}
export const Sidebar = memo(function Sidebar({
entries,
selection,
onSelect,
onCustomizeType,
onUpdateTypeTemplate,
onReorderSections,
onRenameSection,
onToggleTypeVisibility,
onSelectFavorite,
onReorderFavorites,
views = [],
onCreateView,
onEditView,
onDeleteView,
folders = [],
onCreateFolder,
showInbox = true,
inboxCount = 0,
onCollapse,
onCreateNewType,
}: SidebarProps) {
const [customizeTarget, setCustomizeTarget] = useState<string | null>(null)
const [contextMenuPos, setContextMenuPos] = useState<{ x: number; y: number } | null>(null)
const [contextMenuType, setContextMenuType] = useState<string | null>(null)
const [renamingType, setRenamingType] = useState<string | null>(null)
const [renameInitialValue, setRenameInitialValue] = useState('')
const [showCustomize, setShowCustomize] = useState(false)
const contextMenuRef = useRef<HTMLDivElement>(null)
const popoverRef = useRef<HTMLDivElement>(null)
const customizeRef = useRef<HTMLDivElement>(null)
const { typeEntryMap, allSectionGroups, visibleSections, sectionIds } = useSidebarSections(entries)
const { activeCount, archivedCount } = useEntryCounts(entries)
const { collapsed: groupCollapsed, toggle: toggleGroup } = useSidebarCollapsed()
const isSectionVisible = useCallback((type: string) => typeEntryMap[type]?.visible !== false, [typeEntryMap])
const toggleVisibility = useCallback((type: string) => onToggleTypeVisibility?.(type), [onToggleTypeVisibility])
const closeContextMenu = useCallback(() => {
setContextMenuPos(null)
setContextMenuType(null)
}, [])
const closeCustomize = useCallback(() => setShowCustomize(false), [])
const closeCustomizeTarget = useCallback(() => setCustomizeTarget(null), [])
useOutsideClick(customizeRef, showCustomize, closeCustomize)
useOutsideClick(contextMenuRef, !!contextMenuPos, closeContextMenu)
useOutsideClick(popoverRef, !!customizeTarget, closeCustomizeTarget)
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
)
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
const reordered = computeReorder(sectionIds, active.id as string, over.id as string)
if (reordered) onReorderSections?.(reordered.map((typeName, order) => ({ typeName, order })))
}, [sectionIds, onReorderSections])
const handleContextMenu = useCallback((event: React.MouseEvent, type: string) => {
event.preventDefault()
event.stopPropagation()
setContextMenuPos({ x: event.clientX, y: event.clientY })
setContextMenuType(type)
}, [])
const cancelRename = useCallback(() => setRenamingType(null), [])
const handleStartRename = useCallback((type: string) => {
closeContextMenu()
const group = allSectionGroups.find((sectionGroup) => sectionGroup.type === type)
setRenameInitialValue(group?.label ?? type)
setRenamingType(type)
}, [allSectionGroups, closeContextMenu])
const handleRenameSubmit = useCallback((value: string) => {
if (renamingType) onRenameSection?.(renamingType, value)
setRenamingType(null)
}, [renamingType, onRenameSection])
const handleCustomize = useCallback((prop: 'icon' | 'color', value: string) => {
applyCustomization(customizeTarget, typeEntryMap, onCustomizeType, prop, value)
}, [customizeTarget, typeEntryMap, onCustomizeType])
const handleChangeTemplate = useCallback((template: string) => {
if (customizeTarget) onUpdateTypeTemplate?.(customizeTarget, template)
}, [customizeTarget, onUpdateTypeTemplate])
const sectionProps: SidebarSectionProps = {
entries,
selection,
onSelect,
onContextMenu: handleContextMenu,
renamingType,
renameInitialValue,
onRenameSubmit: handleRenameSubmit,
onRenameCancel: cancelRename,
}
const hasFavorites = entries.some((entry) => entry.favorite && !entry.archived)
const hasViews = views.length > 0 || !!onCreateView
return (
<aside className="flex h-full flex-col overflow-hidden border-r border-[var(--sidebar-border)] bg-sidebar text-sidebar-foreground">
<SidebarTitleBar onCollapse={onCollapse} />
<nav className="flex-1 overflow-y-auto">
<SidebarTopNav
selection={selection}
onSelect={onSelect}
showInbox={showInbox}
inboxCount={inboxCount}
activeCount={activeCount}
archivedCount={archivedCount}
/>
{hasFavorites && (
<div className="border-b border-border">
<FavoritesSection
entries={entries}
selection={selection}
onSelect={onSelect}
onSelectNote={onSelectFavorite}
onReorder={onReorderFavorites}
collapsed={groupCollapsed.favorites}
onToggle={() => toggleGroup('favorites')}
/>
</div>
)}
{hasViews && (
<ViewsSection
views={views}
selection={selection}
onSelect={onSelect}
collapsed={groupCollapsed.views}
onToggle={() => toggleGroup('views')}
onCreateView={onCreateView}
onEditView={onEditView}
onDeleteView={onDeleteView}
entries={entries}
/>
)}
<TypesSection
visibleSections={visibleSections}
allSectionGroups={allSectionGroups}
sectionIds={sectionIds}
sensors={sensors}
handleDragEnd={handleDragEnd}
sectionProps={sectionProps}
collapsed={groupCollapsed.sections}
onToggle={() => toggleGroup('sections')}
showCustomize={showCustomize}
setShowCustomize={setShowCustomize}
isSectionVisible={isSectionVisible}
toggleVisibility={toggleVisibility}
onCreateNewType={onCreateNewType}
customizeRef={customizeRef}
/>
<FolderTree
folders={folders}
selection={selection}
onSelect={onSelect}
onCreateFolder={onCreateFolder}
collapsed={groupCollapsed.folders}
onToggle={() => toggleGroup('folders')}
/>
</nav>
<ContextMenuOverlay
pos={contextMenuPos}
type={contextMenuType}
innerRef={contextMenuRef}
onOpenCustomize={(type) => {
closeContextMenu()
setCustomizeTarget(type)
}}
onStartRename={handleStartRename}
/>
<CustomizeOverlay
target={customizeTarget}
typeEntryMap={typeEntryMap}
innerRef={popoverRef}
onCustomize={handleCustomize}
onChangeTemplate={handleChangeTemplate}
onClose={closeCustomizeTarget}
/>
</aside>
)
})