import { Badge, Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, FormControl, FormItem, FormLabel, FormMessage, Popover, PopoverContent, PopoverTrigger, Separator, } from "@repo/shadcn-ui/components"; import { cn } from "@repo/shadcn-ui/lib/utils"; import { type VariantProps, cva } from "class-variance-authority"; import { CheckCircle2Icon, CheckIcon, ChevronDownIcon, Loader2Icon, WandSparklesIcon, XCircleIcon, } from "lucide-react"; import * as React from "react"; import { type Control, type FieldPath, type FieldValues, useController, useFormContext, useFormState, } from "react-hook-form"; import { useTranslation } from "../../locales/i18n.ts"; /* -------------------- Variants -------------------- */ const multiSelectFieldVariants = cva( "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", { variants: { variant: { default: "border-foreground/10 text-foreground bg-primary hover:bg-primary/80 text-primary-foreground", secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", inverted: "inverted", }, }, defaultVariants: { variant: "default", }, } ); /* -------------------- Tipos -------------------- */ export type MultiSelectFieldOptionType = { label: string; value: string; group?: string; icon?: React.ComponentType<{ className?: string }>; }; /** * Props base (visuales y de comportamiento) */ interface MultiSelectFieldBaseProps extends React.ButtonHTMLAttributes, VariantProps { options: MultiSelectFieldOptionType[]; onValueChange?: (values: string[]) => void; // ahora opcional (RHF-first) onValidateOption?: (value: string) => boolean; defaultValue?: string[]; placeholder?: string; animation?: number; maxCount?: number; modalPopover?: boolean; asChild?: boolean; className?: string; selectAllVisible?: boolean; } /** * Props adicionales para paridad con TextField (RHF + UX) */ interface MultiSelectFieldTextFieldLikeProps { name: FieldPath; control?: Control; // opcional; si no, se usa useFormContext() label?: string; description?: string; required?: boolean; readOnly?: boolean; disabledWhileSubmitting?: boolean; showSuccessWhenValid?: boolean; showValidatingSpinner?: boolean; } /** * Props completas */ export type MultiSelectFieldProps = MultiSelectFieldBaseProps & MultiSelectFieldTextFieldLikeProps; /* -------------------- Componente -------------------- */ export const MultiSelectFieldInner = React.forwardRef( ( { // RHF-like name, control: controlProp, label, description, required, readOnly, disabledWhileSubmitting = true, showSuccessWhenValid = true, showValidatingSpinner = true, // tu API actual options, onValueChange, onValidateOption, variant, defaultValue = [], placeholder, animation = 0, maxCount = 3, modalPopover = false, asChild = false, className, selectAllVisible = false, // button props, etc. ...buttonProps }: MultiSelectFieldProps, ref: React.Ref ) => { const formCtx = useFormContext(); const control = controlProp ?? formCtx?.control; if (!control) { throw new Error( "MultiSelectField requiere 'control' o estar dentro de (useFormContext)." ); } const { t } = useTranslation(); // RHF: estado del campo const { field, fieldState } = useController({ control, name, defaultValue: undefined, }); const { isSubmitting, isValidating } = useFormState({ control, name }); // Inicializa el valor con defaultValue si RHF no trae nada: React.useEffect(() => { if (!Array.isArray(field.value) || field.value.length === 0) { if (defaultValue.length > 0) field.onChange(defaultValue); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const selectedValues: string[] = Array.isArray(field.value) ? field.value : []; const disabled = (disabledWhileSubmitting && isSubmitting) || buttonProps.disabled || readOnly; const invalid = fieldState.invalid && (fieldState.isDirty || fieldState.isTouched); const valid = !fieldState.invalid && (fieldState.isDirty || fieldState.isTouched) && selectedValues.length > 0; // A11y ids const describedById = description ? `${name}-desc` : undefined; const errorId = fieldState.error ? `${name}-err` : undefined; const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const [isAnimating, setIsAnimating] = React.useState(false); // Agrupar opciones const grouped = options.reduce>((acc, item) => { const key = item.group || ""; if (!acc[key]) acc[key] = []; acc[key].push(item); return acc; }, {}); const emitChange = (values: string[]) => { field.onChange(values); onValueChange?.(values); }; const toggleOption = (option: string) => { if (onValidateOption && !onValidateOption(option)) { console.warn(`Option "${option}" is not valid.`); return; } const next = selectedValues.includes(option) ? selectedValues.filter((v) => v !== option) : [...selectedValues, option]; emitChange(next); }; const handleClear = () => emitChange([]); const clearExtraOptions = () => emitChange(selectedValues.slice(0, maxCount)); const toggleAll = () => { if (selectedValues.length === options.length) { handleClear(); } else { emitChange(options.map((o) => o.value)); } }; const handleInputKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") setIsPopoverOpen(true); else if (event.key === "Backspace" && !event.currentTarget.value) { const next = selectedValues.slice(0, -1); emitChange(next); } }; const buttonAriaDescribedBy = cn(describedById, errorId); return ( {label && (
{label} {required && } {fieldState.isDirty && ( (modificado) )}
)} setIsPopoverOpen(false)} > {t("components.multi_select.no_results")} {selectAllVisible && (
(Select All)
)} {Object.keys(grouped).map((group) => ( {grouped[group].map((option) => { const isSelected = selectedValues.includes(option.value); const Icon = option.icon; return ( toggleOption(option.value)} className='cursor-pointer' >
{Icon && } {option.label}
); })}
))}
{selectedValues.length > 0 && ( <> {t("components.multi_select.clear_selection")} )} setIsPopoverOpen(false)} className='flex-1 cursor-pointer justify-center' > {t("components.multi_select.close")}
{animation! > 0 && selectedValues.length > 0 && ( setIsAnimating((s) => !s)} /> )}

{description || "\u00A0"}

); } ); MultiSelectFieldInner.displayName = "MultiSelectField"; export const MultiSelectField = MultiSelectFieldInner as ( p: MultiSelectFieldProps & { ref?: React.Ref } ) => React.JSX.Element;