import { useRef, useEffect, useCallback } from 'react' import { useMemo } from 'react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import type { SearchResult, VaultEntry } from '../types' import { useUnifiedSearch } from '../hooks/useUnifiedSearch' import { getTypeColor, getTypeLightColor, buildTypeEntryMap } from '../utils/typeColors' import { formatSearchSubtitle } from '../utils/noteListHelpers' interface SearchPanelProps { open: boolean vaultPath: string entries: VaultEntry[] onSelectNote: (entry: VaultEntry) => void onClose: () => void } export function SearchPanel({ open, vaultPath, entries, onSelectNote, onClose }: SearchPanelProps) { const { query, setQuery, results, selectedIndex, setSelectedIndex, loading, elapsedMs, } = useUnifiedSearch(vaultPath, open) const inputRef = useRef(null) const listRef = useRef(null) useEffect(() => { if (open) setTimeout(() => inputRef.current?.focus(), 50) }, [open]) useEffect(() => { if (!listRef.current) return const selected = listRef.current.children[selectedIndex] as HTMLElement | undefined selected?.scrollIntoView({ block: 'nearest' }) }, [selectedIndex]) const handleSelect = useCallback((result: SearchResult) => { const entry = entries.find(e => e.path === result.path) if (entry) { onSelectNote(entry) onClose() } }, [entries, onSelectNote, onClose]) useEffect(() => { if (!open) return const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() onClose() } else if (e.key === 'ArrowDown') { e.preventDefault() setSelectedIndex(i => Math.min(i + 1, results.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setSelectedIndex(i => Math.max(i - 1, 0)) } else if (e.key === 'Enter') { e.preventDefault() if (results[selectedIndex]) handleSelect(results[selectedIndex]) } } window.addEventListener('keydown', handleKey) return () => window.removeEventListener('keydown', handleKey) }, [open, results, selectedIndex, handleSelect, setSelectedIndex, onClose]) const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const entryLookup = useMemo(() => { const map = new Map() for (const e of entries) map.set(e.path, e) return map }, [entries]) if (!open) return null return (
e.stopPropagation()} >
) } import { forwardRef } from 'react' interface SearchInputProps { query: string loading: boolean onChange: (value: string) => void } const SearchInput = forwardRef( function SearchInput({ query, loading, onChange }, ref) { return (
onChange(e.target.value)} /> {loading && ( )}
) }, ) interface SearchContentProps { query: string results: SearchResult[] selectedIndex: number loading: boolean elapsedMs: number | null entryLookup: Map typeEntryMap: Record listRef: React.RefObject onSelect: (result: SearchResult) => void onHover: (index: number) => void } function SearchContent({ query, results, selectedIndex, loading, elapsedMs, entryLookup, typeEntryMap, listRef, onSelect, onHover, }: SearchContentProps) { return (
{!query.trim() && (

Search across all note contents

Enter to open · Esc to close

)} {query.trim() && results.length === 0 && loading && (
Searching...
)} {query.trim() && results.length === 0 && !loading && (

No results found

)} {results.length > 0 && ( <>
{results.length} result{results.length !== 1 ? 's' : ''}{elapsedMs !== null ? ` · ${elapsedMs}ms` : ''}
{results.map((result, i) => { const entry = entryLookup.get(result.path) const isA = entry?.isA ?? result.noteType const noteType = isA && isA !== 'Note' ? isA : null const typeColor = noteType ? getTypeColor(isA, typeEntryMap[isA ?? '']?.color) : undefined const typeLightColor = noteType ? getTypeLightColor(isA, typeEntryMap[isA ?? '']?.color) : undefined const subtitle = entry ? formatSearchSubtitle(entry) : null return (
onSelect(result)} onMouseEnter={() => onHover(i)} >
{result.title} {noteType && ( {noteType} )}
{subtitle && (

{subtitle}

)}
) })}
)}
) }