import type { DsProps } from './props'; import * as React from 'react'; import { Check, ChevronDown, X } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Badge } from '@/components/ui/badge'; import { SearchInput } from '@/components/ds/SearchInput'; import { useTranslation } from 'react-i18next'; /** * MultiSelect — pick several values from a list. * * Selections render as removable chips in the trigger. Above `searchThreshold` * options a filter field appears, so short lists stay uncluttered and long ones * stay usable. Options may be strings or `{ value, label, count }`. */ /** @param {any} props */ export function MultiSelect({ options = [], value = [], onChange, placeholder = 'Select...', /** Chips shown before collapsing into "+N more". */ maxChips = 2, searchThreshold = 8, disabled = false, className = '', align = 'start', emptyMessage = 'No options', }: DsProps) { const { t } = useTranslation(); const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(''); const normalized = React.useMemo( () => options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o)), [options] ); const filtered = React.useMemo(() => { if (!query) return normalized; const q = query.toLowerCase(); return normalized.filter((o) => o.label.toLowerCase().includes(q)); }, [normalized, query]); const labelFor = (v) => normalized.find((o) => o.value === v)?.label ?? v; const toggle = (optionValue) => { const next = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue]; onChange?.(next); }; const remove = (optionValue, event) => { // The chip's X sits inside the trigger — don't open the popover. event.stopPropagation(); onChange?.(value.filter((v) => v !== optionValue)); }; const visible = value.slice(0, maxChips); const overflow = value.length - visible.length; return ( {normalized.length >= searchThreshold && (
)}
{filtered.length === 0 && (

{emptyMessage}

)} {filtered.map((option) => { const selected = value.includes(option.value); return ( ); })}
{value.length > 0 && (
)}
); }