Asegúrese de tener un proyecto Next.js configurado. Si no, crea uno:
npx crear-siguiente-aplicación mi-aplicación --typescriptcd mi-aplicación
Instale los componentes shadcn necesarios:
npx shadcn@último inicio npx shadcn@latest agregar insignia de separador de botón emergente de comando
Cree multi-select.tsx
en su directorio components
:
// src/components/multi-select.tsximport * como Reaccionar desde "react";importar { cva, escriba VariantProps } desde "class-variance-authority";importar { icono de verificación, Xcírculo, Chevron abajo, Xicono, WandSparkles,} de "lucide-react"; importar { cn } de "@/lib/utils"; importar { Separador } de "@/components/ui/separator"; importar { Botón } de "@/components/ui/ botón";importar { Insignia } desde "@/components/ui/badge";importar { popover, contenido emergente, PopoverTrigger,} de "@/components/ui/popover";importar { Dominio, Comando vacío, grupo de comando, entrada de comando, elemento de comando, lista de comandos, CommandSeparator,} from "@/components/ui/command";/** * Variantes para que el componente de selección múltiple maneje diferentes estilos. * Utiliza class-variance-authority (cva) para definir diferentes estilos basados en la propiedad "variante". */const multiSelectVariants = cva( "m-1 transición facilidad de entrada-salida retardo-150 desplazamiento:-traducir-y-1 desplazamiento:escala-110 duración-300", {variantes: { variante: {valor predeterminado: "borde-primer plano/10 texto-primer plano bg-card hover:bg-card/80", secundario: "borde-primer plano/10 bg-texto secundario-segundo-primer plano hover:bg- secundario/80",destructivo: "borde-transparente bg-texto destructivo-destructivo-primer plano hover:bg-destructivo/80",invertido: "invertido", },},variantes predeterminadas: {variante: "predeterminada",}, });/** * Accesorios para el componente MultiSelect */interface MultiSelectProps extiende React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof multiSelectVariants> { /** * Una matriz de objetos de opción que se mostrarán en el componente de selección múltiple. * Cada objeto de opción tiene una etiqueta, un valor y un icono opcional. */ opciones: {/** El texto que se mostrará para la opción. */label: string;/** El valor único asociado con la opción. */value: string;/** Componente de icono opcional que se mostrará junto a la opción. */icon?: React.ComponentType<{ className?: string }>; }[]; /** * Función de devolución de llamada que se activa cuando cambian los valores seleccionados. * Recibe una matriz de los nuevos valores seleccionados. */ onValueChange: (valor: cadena []) => vacío; /** Los valores seleccionados predeterminados cuando se monta el componente. */ valorpredeterminado?: cadena[]; /** * Texto de marcador de posición que se mostrará cuando no se seleccione ningún valor. * Opcional, por defecto es "Seleccionar opciones". */ ¿marcador de posición?: cadena; /** * Duración de la animación en segundos para los efectos visuales (por ejemplo, insignias que rebotan). * Opcional, por defecto es 0 (sin animación). */ ¿animación?: número; /** * Número máximo de elementos a mostrar. Los elementos adicionales seleccionados se resumirán. * Opcional, el valor predeterminado es 3. */ maxCount?: número; /** * La modalidad del popover. Cuando se establece en verdadero, la interacción con elementos externos * se deshabilitará y solo el contenido emergente será visible para los lectores de pantalla. * Opcional, el valor predeterminado es falso. */ modalPopover?: booleano; /** * Si es verdadero, representa el componente de selección múltiple como hijo de otro componente. * Opcional, el valor predeterminado es falso. */ comoNiño?: booleano; /** * Nombres de clases adicionales para aplicar estilos personalizados al componente de selección múltiple. * Opcional, se puede utilizar para agregar estilos personalizados. */ className?: cadena;}exportar const MultiSelect = React.forwardRef< elemento de botón HTML, Propiedades de selección múltiple>( ({opciones, onValueChange, variante, defaultValue = [], marcador de posición = "Seleccionar opciones", animación = 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 === "Retroceso" && !event.currentTarget.value) {const newSelectedValues = [...selectedValues];newSelectedValues.pop();setSelectedValues(newSelectedValues);onValueChange(newSelectedValues); }};const toggleOption = (opción: cadena) => { const newSelectedValues = selectedValues.includes(opción)? valores seleccionados.filter((valor) => valor!== opción): [...valores seleccionados, opción]; setSelectedValues(nuevosValoresSeleccionados); onValueChange(newSelectedValues);};const handleClear = () => { setSelectedValues([]); onValueChange([]);};const handleTogglePopover = () => { setIsPopoverOpen((prev) => !prev);};const clearExtraOptions = () => { const newSelectedValues = selectedValues.slice(0, maxCount); setSelectedValues(nuevosValoresSeleccionados); onValueChange(newSelectedValues);};const toggleAll = () => { if (selectedValues.length === opciones.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 borde md redondeado min-h-10 h-centro-de-artículos automáticos justificar-entre bg-heredar hover:bg-heredar", className)} >{selectedValues.length > 0 ? ( <div className="flex justificar-entre elementos-center w-full"><div className="flex flex- ajustar elementos-centro"> {selectedValues.slice(0, maxCount).map((value) => {const option = options.find((o) => o.value === valor);const IconComponent = opción?.icon;return ( <Badgekey={valor}className={cn( isAnimating ? "animate-bounce" : "", multiSelectVariants({ variante }))}estilo={{ animaciónDuración: `${animación}s` }} >{IconComponent && ( <IconComponent className="h-4 w-4 mr-2" />)}{opción?.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-texto transparente-primer plano borde-primer plano/1 hover:bg-transparent",isAnimating? "animate-bounce" : "",multiSelectVariants({ variante }) )} estilo={{ AnimationDuration: `${animation}s` }}> {`+ ${selectedValues.length - maxCount} más`} <XCircleclassName= "ml-2 h-4 w-4 cursor-puntero"onClick={(evento) => { event.stopPropagation(); clearExtraOptions();}} /></Badge> )}</div><div className="flex items-center justify-between"> <XIconclassName="h-4 mx-2 cursor-puntero texto-silenciado-primer plano "onClick={(evento) => { evento.stopPropagation(); handleClear();}} /> <Separatororientation="vertical"className="flex min-h-6 h-full" /> <ChevronDown className="h-4 mx-2 cursor-puntero texto-silenciado-primer plano" / ></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-puntero texto-silenciado-primer plano mx-2" /> </div>)} </Button></PopoverTrigger><PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}> <Comando><CommandInput placeholder="Buscar..." onKeyDown={handleInputKeyDown}/><CommandList> <CommandEmpty>No se encontraron resultados.</CommandEmpty> <CommandGroup><CommandItem key="all" onSelect={toggleAll} className="cursor-pointer "> <divclassName={cn( "mr-2 flex h-4 w-4 elementos-centro justificar-centro redondeado-sm borde borde-primario", selectedValues.length === options.length? "bg-primary text-primary-foreground": "opacidad-50 [&_svg]:invisible")} ><CheckIcon className="h-4 w-4" /> </ div> <span>(Seleccionar todo)</span></CommandItem>{options.map((option) => { const isSelected = valores seleccionados.includes(opción.valor); return (<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer"> <divclassName={cn( "mr-2 flex h-4 w-4 elementos-centro justificar-centro redondeado-sm borde borde-primario", ¿está seleccionado? "bg-primary text-primary-forground": "opacidad-50 [&_svg]:invisible")} ><CheckIcon className="h-4 w-4" /> </div> {option.icon && (<option.icon className="mr-2 h-4 w-4 texto -silenciado-primer plano" /> )} <span>{option.label}</span></CommandItem> );})} </CommandGroup> <CommandSeparator /> <CommandGroup><div className="flex items-center justify-between"> {selectedValues.length > 0 && (<> <CommandItemonSelect={handleClear}className="flex-1 justify-center cursor-pointer" >Borrar </ CommandItem> <Separatororientation="vertical"className="flex min-h-6 h-full" /></> )} <CommandItemonSelect={() => setIsPopoverOpen(false)}className="flex-1 justify-center cursor-pointer max-w-full" >Cerrar </CommandItem></div> </CommandGroup></CommandList> < /Command></PopoverContent>{animación > 0 && valores seleccionados.longitud > 0 && ( <WandSparklesclassName={cn( "cursor-puntero mi-2 texto-primer plano bg-fondo w-3 h-3", isAnimating? "" : "texto-silenciado-primer plano")}onClick={() => setIsAnimating(!isAnimating)} />)} </Popover>); });MultiSelect.displayName =</sp