"use client"; import * as React from "react"; import { useMemo, useState } from "react"; import { ChevronsUpDown, Search } from "lucide-react"; import { cn } from "./utils"; import { Button } from "./button"; import { Checkbox } from "./checkbox"; import { Input } from "./input"; import { Popover, PopoverContent, PopoverTrigger } from "./popover"; import type { SearchableSelectOption } from "./searchable-select"; export type { SearchableSelectOption }; function dedupeIds(ids: string[]): string[] { const seen = new Set(); const out: string[] = []; for (const id of ids) { const t = id.trim(); if (!t || seen.has(t)) continue; seen.add(t); out.push(t); } return out; } export function SearchableMultiSelect({ values, onValuesChange, options, placeholder = "Select…", searchPlaceholder = "Search…", emptyText = "No matching results.", disabled, className, }: { values: string[]; onValuesChange: (next: string[]) => void; options: SearchableSelectOption[]; placeholder?: string; searchPlaceholder?: string; emptyText?: string; disabled?: boolean; className?: string; }) { const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const normalized = useMemo(() => dedupeIds(values), [values]); const filtered = useMemo(() => { const s = q.trim().toLowerCase(); if (!s) return options; return options.filter( (o) => o.label.toLowerCase().includes(s) || o.value.toLowerCase().includes(s), ); }, [options, q]); const labelByValue = useMemo(() => { const m = new Map(); for (const o of options) m.set(o.value, o.label); return m; }, [options]); const summary = useMemo(() => { if (normalized.length === 0) return null; if (normalized.length === 1) { return labelByValue.get(normalized[0]) ?? normalized[0]; } const first = labelByValue.get(normalized[0]) ?? normalized[0]; const second = labelByValue.get(normalized[1]) ?? normalized[1]; if (normalized.length === 2) return `${first}, ${second}`; return `${first}, ${second} +${normalized.length - 2}`; }, [normalized, labelByValue]); const toggle = (id: string) => { const t = id.trim(); if (!t) return; if (normalized.includes(t)) { onValuesChange(normalized.filter((x) => x !== t)); } else { onValuesChange([...normalized, t]); } }; return ( { setOpen(o); if (!o) setQ(""); }} >
setQ(e.target.value)} placeholder={searchPlaceholder} className="h-8 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0" />
{filtered.length === 0 ? (
{emptyText}
) : ( filtered.map((opt) => ( )) )}
{normalized.length > 0 ? (
) : null}
); }