import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useDashboardStore } from "../store"; import { useI18n } from "../contexts/I18nContext"; const typeBadgeColors: Record = { file: "text-node-file border border-node-file/30 bg-node-file/10", function: "text-node-function border border-node-function/30 bg-node-function/10", class: "text-node-class border border-node-class/30 bg-node-class/10", module: "text-node-module border border-node-module/30 bg-node-module/10", concept: "text-node-concept border border-node-concept/30 bg-node-concept/10", config: "text-node-config border border-node-config/30 bg-node-config/10", document: "text-node-document border border-node-document/30 bg-node-document/10", service: "text-node-service border border-node-service/30 bg-node-service/10", table: "text-node-table border border-node-table/30 bg-node-table/10", endpoint: "text-node-endpoint border border-node-endpoint/30 bg-node-endpoint/10", pipeline: "text-node-pipeline border border-node-pipeline/30 bg-node-pipeline/10", schema: "text-node-schema border border-node-schema/30 bg-node-schema/10", resource: "text-node-resource border border-node-resource/30 bg-node-resource/10", domain: "text-node-concept border border-node-concept/30 bg-node-concept/10", flow: "text-node-pipeline border border-node-pipeline/30 bg-node-pipeline/10", step: "text-node-function border border-node-function/30 bg-node-function/10", }; export default function SearchBar() { const searchQuery = useDashboardStore((s) => s.searchQuery); const searchResults = useDashboardStore((s) => s.searchResults); const graph = useDashboardStore((s) => s.graph); const setSearchQuery = useDashboardStore((s) => s.setSearchQuery); const navigateToNodeInLayer = useDashboardStore((s) => s.navigateToNodeInLayer); const searchMode = useDashboardStore((s) => s.searchMode); const setSearchMode = useDashboardStore((s) => s.setSearchMode); const { t } = useI18n(); const [dropdownOpen, setDropdownOpen] = useState(false); const containerRef = useRef(null); const inputRef = useRef(null); // Build a lookup map for node details const nodeMap = useMemo( () => new Map((graph?.nodes ?? []).map((n) => [n.id, n])), [graph], ); const topResults = searchResults.slice(0, 5); const handleInputChange = useCallback( (e: React.ChangeEvent) => { setSearchQuery(e.target.value); setDropdownOpen(true); }, [setSearchQuery], ); const handleResultClick = useCallback( (nodeId: string) => { navigateToNodeInLayer(nodeId); setDropdownOpen(false); }, [navigateToNodeInLayer], ); // Close dropdown on Escape useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setDropdownOpen(false); inputRef.current?.blur(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, []); // Close dropdown on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setDropdownOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const showDropdown = dropdownOpen && searchQuery.trim() && topResults.length > 0; return (
setDropdownOpen(true)} placeholder={t.search.placeholder} data-testid="search-input" className="flex-1 min-w-0 bg-elevated text-text-primary text-sm rounded-lg px-3 py-1.5 border border-border-subtle focus:outline-none focus:border-accent/50 placeholder-text-muted" />
{searchQuery.trim() && ( {searchResults.length} {t.search.result}{searchResults.length !== 1 ? "s" : ""}{" "} ({searchMode}) )}
{/* Dropdown results */} {showDropdown && (
{topResults.map((result) => { const node = nodeMap.get(result.nodeId); if (!node) return null; const relevance = Math.round((1 - result.score) * 100); const badgeColor = typeBadgeColors[node.type] ?? typeBadgeColors.file; return ( ); })}
)}
); }