import { Button } from "$components/Button"; import { Dialog } from "$components/Dialog"; import { NO_MOTION_TRANSITION } from "$constants"; import { useSkipAnimation } from "$hooks/useMotion"; import { useViewportTier } from "$hooks/useViewportTier"; import { FileTextIcon, SearchIcon, XIcon } from "$icons"; import type { SearchFilters } from "$state/types"; import type { SearchHit } from "$types"; import { formatShortcut } from "$utils/shortcuts"; import { cn } from "$utils/tw"; import { AnimatePresence, motion } from "motion/react"; import type { ChangeEventHandler, MouseEventHandler } from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ClearAllFiltersButton, CloseButton, FilterLocationButton, ToggleButton } from "./Buttons"; type VisibleFiltersProps = { showFilters: boolean; locations: Array<{ id: number; name: string }>; filters: SearchFilters; handleToggleLocation: (locationId: number) => void; activeFilterCount: number; handleClearFilters: () => void; skipAnimation: boolean; }; type SearchInputProps = { query: string; handleQueryChange: ChangeEventHandler; clearQuery: MouseEventHandler; compact?: boolean; }; type ResultsProps = { isSearching: boolean; results: SearchHit[]; query: string; onSelectResult: (hit: SearchHit) => void; }; type LocationListProps = { locations: Array<{ id: number; name: string }>; filters: SearchFilters; toggler: (locationId: number) => void; }; type SearchResultsHeaderProps = { query: string; isSearching: boolean; results: SearchHit[]; showFilters: boolean; locations: Array<{ id: number; name: string }>; filters: SearchFilters; handleToggleLocation: (locationId: number) => void; activeFilterCount: number; handleClearFilters: () => void; onClose: () => void; handleQueryChange: ChangeEventHandler; clearQuery: MouseEventHandler; toggleFilters: MouseEventHandler; compact: boolean; skipAnimation: boolean; }; type SearchPanelProps = { isOpen: boolean; query: string; results: SearchHit[]; isSearching: boolean; locations: Array<{ id: number; name: string }>; filters: SearchFilters; onQueryChange: (query: string) => void; onFiltersChange: (filters: SearchFilters) => void; onSelectResult: (hit: SearchHit) => void; onClose: () => void; }; type SearchResultProps = { hit: SearchHit; onSelectResult: (hit: SearchHit) => void }; function HighlightedSnippet({ text, matches }: { text: string; matches: Array<{ start: number; end: number }> }) { if (!matches || matches.length === 0) { return {text}; } const parts: React.ReactNode[] = []; let lastEnd = 0; for (const [index, match] of matches.entries()) { const matchKey = `text-${index}`; const markKey = `mark-${index}`; if (match.start > lastEnd) { parts.push({text.slice(lastEnd, match.start)}); } parts.push( {text.slice(match.start, match.end)} , ); lastEnd = match.end; } if (lastEnd < text.length) { parts.push({text.slice(lastEnd)}); } return
{parts}
; } function HighlightLabel({ hit }: { hit: SearchHit }) { return (
{hit.title} Line {hit.line}
); } function SearchResult({ hit, onSelectResult }: SearchResultProps) { const handleClick = useCallback(() => onSelectResult(hit), [onSelectResult, hit]); return ( ); } function LocationList({ locations, filters, toggler }: LocationListProps) { return (
{locations.map((location) => ( ))}
); } function Results({ isSearching, results, query, onSelectResult }: ResultsProps) { if (isSearching) { return
Searching...
; } else if (results.length === 0) { return (
{query ? ( <>

No results found

Try adjusting your search or filters

) : ( <>

Start typing to search

Search across all your documents

)}
); } return (
{results.map((hit, index) => { const k = `${hit.location_id}-${hit.rel_path}-${hit.line}-${index}`; return ; })}
); } function SearchInput({ query, handleQueryChange, clearQuery, compact = false }: SearchInputProps) { const searchShortcut = useMemo(() => formatShortcut("Cmd+Shift+F"), []); return (
{query ? ( ) : ( {searchShortcut} )}
); } function getAnimationProps(skipAnimation: boolean) { const filters = { duration: 0.2, ease: "easeOut" } as const; return { initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: skipAnimation ? NO_MOTION_TRANSITION : filters, }; } function VisibleFilters( { showFilters, locations, filters, handleToggleLocation, activeFilterCount, handleClearFilters, skipAnimation }: VisibleFiltersProps, ) { return ( {showFilters && ( {activeFilterCount > 0 && } )} ); } function SearchResultsHeader( { query, isSearching, results, showFilters, locations, filters, handleToggleLocation, activeFilterCount, handleClearFilters, onClose, handleQueryChange, clearQuery, toggleFilters, compact, skipAnimation, }: SearchResultsHeaderProps, ) { return (
{query && !isSearching && (
{results.length} result{results.length === 1 ? "" : "s"} for "{query}"
)}
); } const GLASS_STYLE = { background: "rgba(35, 38, 43, 0.75)", backdropFilter: "blur(20px)" }; export function SearchPanel( { isOpen, query, results, isSearching, locations, filters, onQueryChange, onFiltersChange, onSelectResult, onClose }: SearchPanelProps, ) { const { viewportWidth, isCompact } = useViewportTier(); const [showFilters, setShowFilters] = useState(false); const skipAnimation = useSkipAnimation(); useEffect(() => { if (!isOpen) { setShowFilters(false); } }, [isOpen]); const handleToggleLocation = useCallback((locationId: number) => { const currentLocations = filters.locations || []; const newLocations = currentLocations.includes(locationId) ? currentLocations.filter((id) => id !== locationId) : [...currentLocations, locationId]; onFiltersChange({ ...filters, locations: newLocations }); }, [filters, onFiltersChange]); const handleClearFilters = useCallback(() => { onFiltersChange({}); }, [onFiltersChange]); const handleQueryChange: ChangeEventHandler = useCallback((e) => { onQueryChange(e.target.value); }, [onQueryChange]); const clearQuery: MouseEventHandler = useCallback(() => { onQueryChange(""); }, [onQueryChange]); const toggleFilters: MouseEventHandler = useCallback(() => { setShowFilters((previous) => !previous); }, []); const activeFilterCount = useMemo( () => (filters.locations?.length ?? 0) + (filters.fileTypes?.length ?? 0) + (filters.dateRange ? 1 : 0), [filters], ); const compact = isCompact || viewportWidth < 640; const panelClassName = useMemo( () => cn( "flex flex-col overflow-hidden border border-stroke-subtle/10 shadow-2xl", compact ? "w-full rounded-t-xl max-h-[80vh]" : "w-full max-w-lg rounded-xl max-h-[70vh]", ), [compact], ); const panelBodyClassName = useMemo(() => cn("overflow-y-auto px-3 py-3", compact ? "flex-1" : "max-h-[50vh]"), [ compact, ]); const containerClassName = useMemo( () => cn( "z-[var(--z-modal)] flex pointer-events-none", compact ? "items-end justify-stretch" : "items-center justify-center px-4 py-8", ), [compact], ); return (
); }