import { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Search } from 'lucide-react'; import { Dialog, DialogContent, DialogTitle } from '../ui/dialog'; import { classNames } from '../../lib/utils'; import { searchNavItems, type SearchItem } from './nav-search'; /** Props for {@link SidebarSearch}. */ interface SidebarSearchProps { collapsed: boolean; items: SearchItem[]; } /** * Command palette search. Opens on click or ⌘K / Ctrl+K, filters the flattened * nav destinations, and navigates on select (arrow keys + Enter supported). * * In the panel it is a full search field; collapsed to the rail it keeps only * the icon, since a field 104px wide can hold neither placeholder nor hint. * * @param {SidebarSearchProps} props - Component props * @returns {React.ReactElement} Search trigger + dialog */ function SidebarSearch({ collapsed, items, }: SidebarSearchProps): React.ReactElement { const navigate = useNavigate(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [activeIndex, setActiveIndex] = useState(0); // Default to the ⌘ label on server + first client render (no hydration // mismatch); swap to "Ctrl" on non-Apple platforms after mount. const [isMac, setIsMac] = useState(true); useEffect(() => { const platform = navigator.platform || navigator.userAgent || ''; setIsMac(/mac|iphone|ipad|ipod/i.test(platform)); }, []); const shortcut = isMac ? '⌘K' : 'Ctrl K'; // Global ⌘K / Ctrl+K toggles the palette. useEffect(() => { const onKey = (event: KeyboardEvent): void => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); setOpen(previous => !previous); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); const results = useMemo(() => searchNavItems(items, query), [query, items]); useEffect(() => setActiveIndex(0), [query]); const navigateTo = useCallback( (item: SearchItem): void => { setOpen(false); setQuery(''); // `navigate` would push an absolute audit URL onto this origin, where it // matches the catch-all and renders a 404. if (item.external) window.open(item.to, '_blank', 'noopener,noreferrer'); else navigate(item.to); }, [navigate] ); const onInputKeyDown = ( event: React.KeyboardEvent ): void => { if (event.key === 'ArrowDown') { event.preventDefault(); setActiveIndex(index => Math.min(index + 1, results.length - 1)); } else if (event.key === 'ArrowUp') { event.preventDefault(); setActiveIndex(index => Math.max(index - 1, 0)); } else if (event.key === 'Enter') { event.preventDefault(); const item = results[activeIndex]; if (item) navigateTo(item); } }; const onOpenChange = (nextOpen: boolean): void => { setOpen(nextOpen); if (!nextOpen) setQuery(''); }; return ( <> Search navigation
setQuery(event.target.value)} onKeyDown={onInputKeyDown} placeholder="Search pages…" className="w-full bg-transparent py-4 text-base text-foreground outline-none placeholder:text-muted-foreground" /> Esc
{results.length === 0 ? (

No pages match “{query}”.

) : ( results.map((item, index) => { const Icon = item.icon; return ( ); }) )}
); } export default SidebarSearch;