Certifique-se de ter um projeto Next.js configurado. Caso contrário, crie um:
npx create-next-app meu-app --typescriptcd meu-app
Instale os componentes shadcn necessários:
npx shadcn@latest init npx shadcn@latest adicionar emblema separador de botão popover de comando
Crie multi-select.tsx
em seu diretório components
:
// src/components/multi-select.tsximport * as React from "react";import { cva, type VariantProps } from "class-variance-authority";import { Ícone de verificação, XCírculo, Chevron para baixo, XIcon, WandSparkles,} de "lucide-react";importar {cn} de "@/lib/utils";importar {Separador} de "@/components/ui/separator";importar {Botão} de "@/components/ui/ button";importar { Badge } de "@/components/ui/badge";importar { Popover, PopoverConteúdo, PopoverTrigger,} de "@/components/ui/popover";importar { Comando, ComandoVazio, Grupo de comando, Entrada de Comando, CommandItem, Lista de Comandos, CommandSeparator,} from "@/components/ui/command";/** * Variantes para o componente de seleção múltipla lidar com estilos diferentes. * Usa autoridade de variação de classe (cva) para definir estilos diferentes com base na propriedade "variante". */const multiSelectVariantes = cva( "m-1 transição facilidade de entrada e saída atraso-150 pairar:-translate-y-1 pairar:escala-110 duração-300", {variantes: {variante: {padrão: "border-foreground/10 text-foreground bg-card hover:bg-card/80",secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg- secundário/80",destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",invertido: "invertido", },},defaultVariantes: { variante: "default",}, });/** * Props para componente MultiSelect */interface MultiSelectProps estende React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof multiSelectVariants> { /** * Uma matriz de objetos de opção a serem exibidos no componente de seleção múltipla. * Cada objeto de opção possui um rótulo, um valor e um ícone opcional. */ options: {/** o texto a ser exibido para a opção. */label: string;/** O valor exclusivo associado à opção. */value: string;/** Componente de ícone opcional para exibir ao lado da opção. */icon?: React.ComponentType<{ className?: string }>; }[]; /** * Função de retorno de chamada acionada quando os valores selecionados mudam. * Recebe um array dos novos valores selecionados. */ onValueChange: (valor: string[]) => void; /** Os valores padrão selecionados quando o componente é montado. */ valorpadrão?: string[]; /** * Texto de espaço reservado a ser exibido quando nenhum valor for selecionado. * Opcional, o padrão é "Selecionar opções". */ espaço reservado?: string; /** * Duração da animação em segundos para os efeitos visuais (por exemplo, emblemas saltitantes). * Opcional, o padrão é 0 (sem animação). */ animação?: número; /** * Número máximo de itens a serem exibidos. Itens extras selecionados serão resumidos. * Opcional, o padrão é 3. */ maxContagem?: número; /** * A modalidade do popover. Quando definido como verdadeiro, a interação com elementos externos * será desativada e apenas o conteúdo popover ficará visível para leitores de tela. * Opcional, o padrão é falso. */ modalPopover?: booleano; /** * Se verdadeiro, renderiza o componente de seleção múltipla como filho de outro componente. * Opcional, o padrão é falso. */ asChild?: booleano; /** * Nomes de classes adicionais para aplicar estilos personalizados ao componente de seleção múltipla. * Opcional, pode ser usado para adicionar estilos personalizados. */ className?: string;}export const MultiSelect = React.forwardRef< HTMLButtonElement, MultiSelectProps>( ({opções, onValueChange, variante, defaultValue = [], placeholder = "Selecionar opções", animação = 0, maxCount = 3, modalPopover = false, asChild = false, className, ...props},ref ) => {const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);const [isAnimating, setIsAnimating] = React.useState(false) ;const handleInputKeyDown = (evento: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") {setIsPopoverOpen(true); } else if (event.key === "Backspace" && !event.currentTarget.value) {const newSelectedValues = [...selectedValues];newSelectedValues.pop();setSelectedValues(newSelectedValues);onValueChange(newSelectedValues); }};const toggleOption = (opção: string) => { const novosValoresSelecionados = valores selecionados.includes(opção)? valores selecionados.filter((valor) => valor!== opção): [...valores selecionados, opção]; setSelectedValues(newSelectedValues); onValueChange(newSelectedValues);};const handleClear = () => { setSelectedValues([]); onValueChange([]);};const handleTogglePopover = () => { setIsPopoverOpen((prev) => !prev);};const clearExtraOptions = () => { const newSelectedValues = selectedValues.slice(0, maxCount); setSelectedValues(newSelectedValues); onValueChange(newSelectedValues);};const toggleAll = () => { if (selectedValues.length === options.length) {handleClear(); } else {const allValues = options.map((option) => option.value);setSelectedValues(allValues);onValueChange(allValues); }};return ( <Popoveropen={isPopoverOpen}onOpenChange={setIsPopoverOpen}modal={modalPopover} ><PopoverTrigger asChild> <Buttonref={ref}{...props}onClick={handleTogglePopover}className={cn( "flex w-full p-1 borda arredondada md min-h-10 h-auto items-center justificar-entre bg-inherit hover:bg-inherit", className)} >{selectedValues.length > 0 ? ( <div className="flex justify-between items-center w-full"><div className="flex flex- wrap items-center"> {selectedValues.slice(0, maxCount).map((value) => {const option = options.find((o) => o.value === valor);const IconComponent = opção?.icon;return ( <Badgekey={value}className={cn( isAnimating ? "animate-bounce" : "", multiSelectVariants({variant }))}style={{ animaçãoDuration: `${animation}s` }} >{IconComponent && ( <IconComponent className="h-4 w-4 mr-2" />)}{option?.label}<XCircle className="ml-2 h-4 w-4 cursor-pointer" onClick={(event) => {event.stopPropagation();toggleOption(value }}); /> </Badge> })} {selectedValues.length > maxCount && (<Badge className={cn("bg-transparent text-foreground border-foreground/1 hover:bg-transparent",estáAnimando ? "animate-bounce" : "",multiSelectVariants({variant }) )} style={{animationDuration: `${animation}s` }}> {`+ ${selectedValues.length - maxCount} more`} <XCircleclassName= "ml-2 h-4 w-4 cursor-ponteiro"onClick={(event) => { event.stopPropagation(); clearExtraOptions();}} /></Badge> )}</div><div className="flex items-center justificar-between"> <XIconclassName="h-4 mx-2 cursor-pointer text-muted-foreground "onClick={(evento) => {evento.stopPropagation(); handleClear();}} /> <Separatororientation="vertical"className="flex min-h-6 h-full" /> <ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" / </div> </div>) : ( <div className="flex items-center justificar-entre w-full mx-auto"><span className="text-sm text-muted-foreground mx-3"> {placeholder}</span><ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" /> </div>)} </Button></PopoverTrigger><PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}> <Command><CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown}/><CommandList> <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty> <CommandGroup><CommandItem key="all" onSelect={toggleAll} className="cursor-pointer "> <divclassName={cn( "mr-2 flex h-4 w-4 itens-centro justificar-centro arredondado-sm borda borda-primária", selectValues.length === options.length? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:invisible")} ><CheckIcon className="h-4 w-4" /> </ div> <span>(Selecionar tudo)</span></CommandItem>{options.map((option) => { const isSelected = valores selecionados.includes(opção.valor); return (<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer"> <divclassName={cn( "mr-2 flex h-4 w-4 itens-centro justificar-centro arredondado-sm borda border-primary", isSelected? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:invisível")} ><CheckIcon className="h-4 w-4" /> </div> {option.icon && (<option.icon className="mr-2 h-4 w-4 texto -muted-foreground" /> )} <span>{option.label}</span></CommandItem> );})} </CommandGroup> <CommandSeparator /> <CommandGroup><div className="flex items-center justificar-between"> {selectedValues.length > 0 && (<> <CommandItemonSelect={handleClear}className="flex-1 justificar-center cursor-pointer" >Limpar </CommandItem> <Separatororientation="vertical"className="flex min-h-6 h-full" /></> )} <CommandItemonSelect={() => setIsPopoverOpen(false)}className="flex-1 justify-center cursor-pointer max-w-full" >Fechar </CommandItem></div> </CommandGroup></CommandList> < /Command></PopoverContent>{animação > 0 && valores selecionados.length > 0 && ( <WandSparklesclassName={cn( "ponteiro de cursor my-2 texto-foreground bg-background w-3 h-3", isAnimating ? "" : "text-muted-foreground")}onClick={() => setIsAnimating(!isAnimating)} />)} </Popover>); });MultiSelect.displayName =</sp