"use client"; import { useState, useCallback, useRef, useEffect } from "react"; import { Check } from "lucide-react"; import { Popover, PopoverTrigger, PopoverContent, } from "@multica/ui/components/ui/popover"; const HIGHLIGHT_CLASS = "bg-accent"; const ITEM_SELECTOR = "button[data-picker-item]:not(:disabled)"; // --------------------------------------------------------------------------- // PropertyPicker — generic Popover shell with optional search // --------------------------------------------------------------------------- export function PropertyPicker({ open, onOpenChange, trigger, triggerRender, width = "w-48", align = "end", searchable = false, searchPlaceholder = "Filter...", onSearchChange, children, }: { open: boolean; onOpenChange: (v: boolean) => void; trigger: React.ReactNode; triggerRender?: React.ReactElement; width?: string; align?: "start" | "center" | "end"; searchable?: boolean; searchPlaceholder?: string; onSearchChange?: (query: string) => void; children: React.ReactNode; }) { const [query, setQuery] = useState(""); const [highlightedIndex, setHighlightedIndex] = useState(-1); const listRef = useRef(null); const getItems = useCallback(() => { if (!listRef.current) return []; return Array.from( listRef.current.querySelectorAll(ITEM_SELECTOR), ); }, []); // Apply/remove highlight class via DOM when index changes useEffect(() => { const items = getItems(); for (const item of items) { item.classList.remove(HIGHLIGHT_CLASS); } if (highlightedIndex >= 0 && highlightedIndex < items.length) { items[highlightedIndex]?.classList.add(HIGHLIGHT_CLASS); } }, [highlightedIndex, getItems, children]); // re-run when children change (filtered list updates) const handleOpenChange = useCallback( (v: boolean) => { onOpenChange(v); if (!v) { setQuery(""); setHighlightedIndex(-1); onSearchChange?.(""); } }, [onOpenChange, onSearchChange], ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { const items = getItems(); if (items.length === 0) return; if (e.key === "ArrowDown") { e.preventDefault(); setHighlightedIndex((prev) => { const next = prev < items.length - 1 ? prev + 1 : 0; items[next]?.scrollIntoView({ block: "nearest" }); return next; }); } else if (e.key === "ArrowUp") { e.preventDefault(); setHighlightedIndex((prev) => { const next = prev > 0 ? prev - 1 : items.length - 1; items[next]?.scrollIntoView({ block: "nearest" }); return next; }); } else if (e.key === "Enter") { e.preventDefault(); if (highlightedIndex >= 0 && highlightedIndex < items.length) { items[highlightedIndex]?.click(); } else if (items.length === 1) { // Auto-select when only one result items[0]?.click(); } } }, [getItems, highlightedIndex], ); return ( {trigger} {searchable && (
{ setQuery(e.target.value); setHighlightedIndex(0); onSearchChange?.(e.target.value); }} onKeyDown={handleKeyDown} placeholder={searchPlaceholder} aria-label="Filter options" className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none" />
)}
{children}
); } // --------------------------------------------------------------------------- // PickerItem — single selectable row // --------------------------------------------------------------------------- export function PickerItem({ selected, disabled, onClick, hoverClassName, children, }: { selected: boolean; disabled?: boolean; onClick: () => void; hoverClassName?: string; children: React.ReactNode; }) { return ( ); } // --------------------------------------------------------------------------- // PickerSection — group header // --------------------------------------------------------------------------- export function PickerSection({ label, children, }: { label: string; children: React.ReactNode; }) { return (
{label}
{children}
); } // --------------------------------------------------------------------------- // PickerEmpty — no results state // --------------------------------------------------------------------------- export function PickerEmpty() { return (
No results
); }