import { useState, useMemo, useCallback, memo } from 'react' import type { VaultEntry, SidebarSelection, ModifiedFile } from '../types' import { Input } from '@/components/ui/input' import { MagnifyingGlass, Plus, CaretDown, CaretRight, Warning, } from '@phosphor-icons/react' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' import { NoteItem, getTypeIcon } from './NoteItem' import { SortDropdown } from './SortDropdown' import { type SortOption, type RelationshipGroup, getSortComparator, buildRelationshipGroups, filterEntries, sortByModified, relativeDate, getDisplayDate, loadSortPreferences, saveSortPreferences, } from '../utils/noteListHelpers' // Re-export for consumers export { sortByModified, filterEntries, buildRelationshipGroups, getSortComparator } export type { SortOption } interface NoteListProps { entries: VaultEntry[] selection: SidebarSelection selectedNote: VaultEntry | null allContent: Record modifiedFiles?: ModifiedFile[] onSelectNote: (entry: VaultEntry) => void onCreateNote: () => void } function PinnedCard({ entry, typeEntryMap, onSelectNote, showDate }: { entry: VaultEntry typeEntryMap: Record onSelectNote: (entry: VaultEntry) => void showDate?: boolean }) { const te = typeEntryMap[entry.isA ?? ''] const color = getTypeColor(entry.isA ?? '', te?.color) const bgColor = getTypeLightColor(entry.isA ?? '', te?.color) const Icon = getTypeIcon(entry.isA, te?.icon) return (
onSelectNote(entry)}>
{entry.title}
{entry.snippet}
{showDate &&
{relativeDate(getDisplayDate(entry))}
}
) } function RelationshipGroupSection({ group, isCollapsed, sortPrefs, onToggle, handleSortChange, renderItem }: { group: RelationshipGroup isCollapsed: boolean sortPrefs: Record onToggle: () => void handleSortChange: (groupLabel: string, option: SortOption) => void renderItem: (entry: VaultEntry) => React.ReactNode }) { const groupSort = sortPrefs[group.label] ?? 'modified' const sortedEntries = [...group.entries].sort(getSortComparator(groupSort)) return (
{!isCollapsed && sortedEntries.map((entry) => renderItem(entry))}
) } function TrashWarningBanner({ expiredCount }: { expiredCount: number }) { if (expiredCount === 0) return null return (
Notes in trash for 30+ days will be permanently deleted
{expiredCount} {expiredCount === 1 ? 'note is' : 'notes are'} past the 30-day retention period
) } function EmptyMessage({ text }: { text: string }) { return
{text}
} function resolveHeaderTitle(selection: SidebarSelection, typeDocument: VaultEntry | null): string { if (selection.kind === 'entity') return selection.entry.title if (typeDocument) return typeDocument.title if (selection.kind === 'filter' && selection.filter === 'archived') return 'Archive' if (selection.kind === 'filter' && selection.filter === 'trash') return 'Trash' return 'Notes' } function useTypeEntryMap(entries: VaultEntry[]) { return useMemo(() => { const map: Record = {} for (const e of entries) { if (e.isA === 'Type') map[e.title] = e } return map }, [entries]) } // --- View sub-components --- function EntityView({ entity, groups, query, collapsedGroups, sortPrefs, onToggleGroup, onSortChange, renderItem, typeEntryMap, onSelectNote }: { entity: VaultEntry; groups: RelationshipGroup[]; query: string collapsedGroups: Set; sortPrefs: Record onToggleGroup: (label: string) => void; onSortChange: (label: string, opt: SortOption) => void renderItem: (entry: VaultEntry) => React.ReactNode typeEntryMap: Record; onSelectNote: (entry: VaultEntry) => void }) { return (
{groups.length === 0 ? : groups.map((group) => ( onToggleGroup(group.label)} handleSortChange={onSortChange} renderItem={renderItem} /> )) }
) } function ListView({ typeDocument, isTrashView, expiredTrashCount, searched, query, renderItem, typeEntryMap, onSelectNote }: { typeDocument: VaultEntry | null; isTrashView: boolean; expiredTrashCount: number searched: VaultEntry[]; query: string renderItem: (entry: VaultEntry) => React.ReactNode typeEntryMap: Record; onSelectNote: (entry: VaultEntry) => void }) { const emptyText = isTrashView ? 'Trash is empty' : (query ? 'No matching notes' : 'No notes found') return (
{typeDocument && } {searched.length === 0 ? : searched.map((entry) => renderItem(entry)) }
) } // --- Pure helpers --- function filterByQuery(items: T[], query: string): T[] { return query ? items.filter((e) => e.title.toLowerCase().includes(query)) : items } function filterGroupsByQuery(groups: RelationshipGroup[], query: string): RelationshipGroup[] { if (!query) return groups return groups.map((g) => ({ ...g, entries: filterByQuery(g.entries, query) })).filter((g) => g.entries.length > 0) } function countExpiredTrash(entries: VaultEntry[]): number { const now = Date.now() / 1000 return entries.filter((e) => e.trashedAt && (now - e.trashedAt) >= 86400 * 30).length } // --- Data hooks --- interface NoteListDataParams { entries: VaultEntry[]; selection: SidebarSelection; allContent: Record query: string; listSort: SortOption; modifiedFiles?: ModifiedFile[] } function useNoteListData({ entries, selection, allContent, query, listSort, modifiedFiles }: NoteListDataParams) { const isEntityView = selection.kind === 'entity' const isTrashView = selection.kind === 'filter' && selection.filter === 'trash' const typeDocument = useMemo(() => { if (selection.kind !== 'sectionGroup') return null return entries.find((e) => e.isA === 'Type' && e.title === selection.type) ?? null }, [selection, entries]) const searched = useMemo(() => { if (isEntityView) return [] const sorted = [...filterEntries(entries, selection)].sort(getSortComparator(listSort)) return filterByQuery(sorted, query) }, [entries, selection, modifiedFiles, isEntityView, listSort, query]) const searchedGroups = useMemo(() => { if (!isEntityView) return [] const groups = buildRelationshipGroups(selection.entry, entries, allContent) return filterGroupsByQuery(groups, query) }, [isEntityView, selection, entries, allContent, query]) const expiredTrashCount = useMemo( () => isTrashView ? countExpiredTrash(searched) : 0, [isTrashView, searched], ) return { isEntityView, isTrashView, typeDocument, searched, searchedGroups, expiredTrashCount } } // --- Main component --- function NoteListInner({ entries, selection, selectedNote, allContent, modifiedFiles, onSelectNote, onCreateNote }: NoteListProps) { const [search, setSearch] = useState('') const [searchVisible, setSearchVisible] = useState(false) const [collapsedGroups, setCollapsedGroups] = useState>(new Set()) const [sortPrefs, setSortPrefs] = useState>(loadSortPreferences) const handleSortChange = useCallback((groupLabel: string, option: SortOption) => { setSortPrefs((prev) => { const next = { ...prev, [groupLabel]: option }; saveSortPreferences(next); return next }) }, []) const toggleGroup = useCallback((label: string) => { setCollapsedGroups((prev) => { const next = new Set(prev); if (next.has(label)) next.delete(label); else next.add(label); return next }) }, []) const typeEntryMap = useTypeEntryMap(entries) const query = search.trim().toLowerCase() const listSort = sortPrefs['__list__'] ?? 'modified' const { isEntityView, isTrashView, typeDocument, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, allContent, query, listSort, modifiedFiles }) const renderItem = useCallback((entry: VaultEntry) => ( ), [selectedNote?.path, onSelectNote, typeEntryMap]) return (

{resolveHeaderTitle(selection, typeDocument)}

{!isEntityView && }
{searchVisible && (
setSearch(e.target.value)} className="h-8 text-[13px]" autoFocus />
)}
{isEntityView && selection.kind === 'entity' ? ( ) : ( )}
) } export const NoteList = memo(NoteListInner)