'use client' import { ChevronsUpDown } from 'lucide-react' import { createContext, forwardRef, useCallback, useContext, useId, useState, PropsWithChildren, useRef, useEffect, } from 'react' import { cn, Badge, Checkbox_Shadcn_ as Checkbox, Popover_Shadcn_ as Popover, PopoverContent_Shadcn_ as PopoverContent, PopoverTrigger_Shadcn_ as PopoverTrigger, } from 'ui' interface Item { value: string label: string } const MultiSelectContext = createContext<{ selected: Item[] handleSelect: (item: Item) => void setOpen: (open: boolean) => void }>({ selected: [], handleSelect: () => {}, setOpen: () => {}, }) function MultiSelectProvider({ selected, onSelectedChange, setOpen, children, }: PropsWithChildren<{ selected: Item[] onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void setOpen: (open: boolean) => void }>) { const handleSelect = useCallback( (item: Item) => { onSelectedChange((current: Item[]) => { const isSelected = current.some((currItem) => currItem.value === item.value) if (isSelected) { return current.filter((currItem) => currItem.value !== item.value) } else { return [...current, item] } }) }, [onSelectedChange] ) return ( {children} ) } function useMultiSelect() { const context = useContext(MultiSelectContext) if (!context) { throw new Error('useMultiSelect must be used within a MultiSelectProvider') } return context } export function MultiSelect({ open: _controlledOpen, setOpen: _setControlledOpen, selected, onSelectedChange, children, ...props }: PropsWithChildren< { open?: boolean setOpen?: React.Dispatch> selected?: Item[] onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void } & React.ComponentProps >) { const [_internalOpen, _setInternalOpen] = useState(false) const open = _controlledOpen ?? _internalOpen const setOpen = _setControlledOpen ?? _setInternalOpen return ( {children} ) } const Trigger = forwardRef< HTMLButtonElement, { label?: string; className?: string } & React.ComponentProps >(({ label, className, ...props }, ref) => { const { selected } = useMultiSelect() const [measuredWidth, setMeasuredWidth] = useState(0) const measuredRef = (node: HTMLElement) => { if (node) { setMeasuredWidth(node.getBoundingClientRect().width) } } const badgeWidth = 100 // Approximate width of a badge in pixels const maxBadges = Math.max(1, Math.floor((measuredWidth || 200) / badgeWidth)) const visibleBadges = selected.slice(0, maxBadges) const extraBadgesCount = Math.max(0, selected.length - maxBadges) const badgeClasses = 'bg-200' return ( {selected.length === 0 ? ( // Leading prevents shift when switching to badges {label} ) : ( {visibleBadges.map((item) => ( {item.label} ))} {extraBadgesCount > 0 && +{extraBadgesCount}} )} ) }) Trigger.displayName = 'Trigger' MultiSelect.Trigger = Trigger const Content = forwardRef< HTMLDivElement, PropsWithChildren<{ className?: string } & React.ComponentProps> >(({ children, className, ...props }, ref) => { return ( {children} ) }) Content.displayName = 'Content' MultiSelect.Content = Content export function Item({ item, className }: { item: Item; className?: string }) { const id = useId() const { selected, handleSelect } = useMultiSelect() return ( sel.value === item.value)} onCheckedChange={() => handleSelect(item)} /> {item.label} ) } MultiSelect.Item = Item