UNPKG

ivt

Version:

Ivt Components Library

381 lines (378 loc) 17.8 kB
import { cva } from 'class-variance-authority'; import * as React from 'react'; import { c } from '../chunks/index.module-1-Lm1QYF.mjs'; import { c as cn } from '../chunks/utils-05LlW3Cl.mjs'; import { X } from '../chunks/x-BOMmTgZd.mjs'; import { C as ChevronsUpDown } from '../chunks/chevrons-up-down-BFiJwHit.mjs'; import { C as Check } from '../chunks/check-BBGTedl-.mjs'; import { W as WandSparkles } from '../chunks/wand-sparkles-BIbU0kjG.mjs'; import { L as Label } from '../chunks/label-atj6gghV.mjs'; import { P as Popover, a as PopoverTrigger, b as PopoverContent } from '../chunks/popover-CsYW0nDm.mjs'; import { B as Button } from '../chunks/button-Co_1yLv6.mjs'; import { B as Badge } from '../chunks/badge-rV4HbE_R.mjs'; import { c as TooltipProvider, T as Tooltip, a as TooltipTrigger, b as TooltipContent } from '../chunks/tooltip-BTEGteNb.mjs'; import { S as Separator } from '../chunks/separator-StpvupIv.mjs'; import { C as Command, b as CommandInput, c as CommandList, d as CommandEmpty, e as CommandGroup, f as CommandItem, h as CommandSeparator } from '../chunks/command-IckfUQsK.mjs'; import '../chunks/bundle-mjs-BYcyWisL.mjs'; import '../chunks/createLucideIcon-DLrNgMqk.mjs'; import '../chunks/index-DgKlJYZP.mjs'; import 'react-dom'; import '@radix-ui/react-slot'; import 'react/jsx-runtime'; import '../chunks/index-Bl-WJHvp.mjs'; import '../chunks/index-1tQVI0Jh.mjs'; import '../chunks/index-DT8WgpCS.mjs'; import '../chunks/index-DUpRrJTH.mjs'; import '../chunks/index-Cbm3--wc.mjs'; import '../chunks/index-DvCZGX3H.mjs'; import '../chunks/tslib.es6-DXUeYCTx.mjs'; import '../chunks/index-tkRL9Tft.mjs'; import '../chunks/index-DKOlG3mh.mjs'; import '../chunks/index-aLIsJMgt.mjs'; import '../chunks/index-DmY774z-.mjs'; import '../chunks/index-BTe1rv5Z.mjs'; import '../chunks/index-C6s8KI_8.mjs'; import '../chunks/index-D4FMFHi9.mjs'; import '../chunks/index-An4yBrAZ.mjs'; import '../chunks/index-BRYGnp2Q.mjs'; import '../chunks/dialog-BkF50Tmo.mjs'; /** * Variants for the multi-select component to handle different styles. * Uses class-variance-authority (cva) to define different styles based on "variant" prop. */ const multiSelectVariants = cva("m-1 transition ease-in-out delay-150 duration-300", { variants: { variant: { default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", positive: "border-transparent bg-positive text-positive-foreground shadow hover:bg-positive/80", warning: "border-transparent bg-warning text-warning-foreground shadow hover:bg-warning/80", info: "border-transparent bg-info text-info-foreground shadow hover:bg-info/80", outline: "text-foreground bg-transparent hover:bg-transparent border" }, subtle: { true: "", false: "" } }, defaultVariants: { variant: "default" } }); const AutoCompleteMultiSelect = /*#__PURE__*/ React.forwardRef(({ selectedOptions = [], onChange, fetchOptions, variant, placeholder = "Select options", searchPlaceholder = "Buscar...", messageEmpty = "Nenhum resultado encontrado.", animation = 0, maxCount = 3, modalPopover = false, className, label, description, subtle = false, focusOnSelect = true, id, onError }, ref)=>{ const inputRef = React.useRef(null); const lastClickedIndexRef = React.useRef(-1); const isShiftPressedRef = React.useRef(false); const generatedId = React.useId(); const inputId = id || generatedId; // Estado interno para resposta imediata const [internalSelected, setInternalSelected] = React.useState(selectedOptions); const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const [isAnimating, setIsAnimating] = React.useState(false); const [inputValue, setInputValue] = React.useState(""); const [options, setOptions] = React.useState([]); const [loading, setLoading] = React.useState(false); // Sincroniza estado interno quando selectedOptions muda (do pai) React.useEffect(()=>{ setInternalSelected(selectedOptions); }, [ selectedOptions ]); // Listener global para detectar Shift React.useEffect(()=>{ const handleKeyDown = (e)=>{ if (e.key === "Shift") { isShiftPressedRef.current = true; } }; const handleKeyUp = (e)=>{ if (e.key === "Shift") { isShiftPressedRef.current = false; } }; window.addEventListener("keydown", handleKeyDown); window.addEventListener("keyup", handleKeyUp); return ()=>{ window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); }; }, []); // Busca assíncrona com debounce (do AutoComplete) const debouncedFetch = c(async (term)=>{ if (!term.trim()) { setOptions([]); setLoading(false); return; } setLoading(true); try { const result = await fetchOptions(term); setOptions(result); } catch (err) { if (onError) { onError(err); } setOptions([]); } finally{ setLoading(false); } }, 500); const handleInputChange = (value)=>{ setInputValue(value); if (value.trim() === "") { setOptions([]); setLoading(false); return; } setLoading(true); debouncedFetch(value); }; const handleInputKeyDown = (event)=>{ if (event.key === "Enter") { setIsPopoverOpen(true); } }; const toggleOption = (option, index)=>{ // Normaliza para string para comparação const optionValueStr = String(option.value); const isSelected = internalSelected.find((o)=>String(o.value) === optionValueStr); const newSelectedOptions = isSelected ? internalSelected.filter((value)=>String(value.value) !== optionValueStr) : [ ...internalSelected, option ]; // Atualiza estado interno IMEDIATAMENTE setInternalSelected(newSelectedOptions); // Notifica o pai onChange(newSelectedOptions); // Armazena o índice do último clique lastClickedIndexRef.current = index; }; const removeOption = (option)=>{ // Normaliza para string para comparação const optionValueStr = String(option.value); const newSelectedOptions = internalSelected.filter((value)=>String(value.value) !== optionValueStr); setInternalSelected(newSelectedOptions); onChange(newSelectedOptions); }; const toggleInterval = (index)=>{ // Se não houver último clique registrado, apenas seleciona o item atual if (lastClickedIndexRef.current === -1 || internalSelected.length < 1) { toggleOption(options[index], index); return; } const lastIndex = lastClickedIndexRef.current; const start = Math.min(lastIndex, index); const end = Math.max(lastIndex, index) + 1; const rangeOptions = options.slice(start, end); const rangeValuesStr = rangeOptions.map((o)=>String(o.value)); const areAllSelected = rangeValuesStr.every((v)=>internalSelected.some((opt)=>String(opt.value) === v)); if (areAllSelected) { // Desselecionar todos os itens do intervalo const newSelectedOptions = internalSelected.filter((v)=>!rangeValuesStr.includes(String(v.value))); setInternalSelected(newSelectedOptions); onChange(newSelectedOptions); } else { // Adicionar todos os itens do intervalo sem duplicados const existingValuesStr = new Set(internalSelected.map((opt)=>String(opt.value))); const newOptions = rangeOptions.filter((opt)=>!existingValuesStr.has(String(opt.value))); const newSelectedOptions = [ ...internalSelected, ...newOptions ]; setInternalSelected(newSelectedOptions); onChange(newSelectedOptions); } // Atualiza o último índice clicado lastClickedIndexRef.current = index; }; const handleClear = ()=>{ setInternalSelected([]); onChange([]); }; const handleTogglePopover = ()=>{ setIsPopoverOpen((prev)=>!prev); }; const clearExtraOptions = ()=>{ const newSelectedOptions = internalSelected.slice(0, maxCount); setInternalSelected(newSelectedOptions); onChange(newSelectedOptions); }; const toggleAll = ()=>{ if (internalSelected.length === options.length) { handleClear(); } else { setInternalSelected(options); onChange(options); } }; const subtleStyles = (variant)=>{ if (variant === "default") { return "border-transparent bg-accent text-primary hover:bg-accent/80"; } if (variant === "destructive") { return "border-transparent bg-destructive-foreground text-destructive hover:bg-destructive-foreground/80"; } if (variant === "positive") { return "border-transparent bg-positive-foreground text-positive hover:bg-positive-foreground/80"; } if (variant === "warning") { return "border-transparent bg-warning-foreground text-warning hover:bg-warning-foreground/80"; } if (variant === "info") { return "border-transparent bg-info-foreground text-info hover:bg-info-foreground/80"; } return ""; }; const finalSubtleStyles = subtleStyles(variant); return /*#__PURE__*/ React.createElement("div", { className: "font-lato space-y-2" }, label && /*#__PURE__*/ React.createElement(Label, { htmlFor: inputId, className: "text-foreground text-sm font-medium gap-0" }, label), /*#__PURE__*/ React.createElement(Popover, { open: isPopoverOpen, onOpenChange: setIsPopoverOpen, modal: modalPopover }, /*#__PURE__*/ React.createElement(PopoverTrigger, { asChild: true }, /*#__PURE__*/ React.createElement(Button, { id: inputId, ref: ref, onClick: handleTogglePopover, variant: "outline", "aria-expanded": isPopoverOpen, className: cn("flex h-auto min-h-9 w-full items-center justify-between rounded-md border bg-inherit px-4 py-2 hover:bg-inherit [&_svg]:pointer-events-auto", className) }, internalSelected.length > 0 ? /*#__PURE__*/ React.createElement("div", { className: "flex w-full items-center justify-between" }, /*#__PURE__*/ React.createElement("div", { className: "flex flex-wrap items-center" }, internalSelected.slice(0, maxCount).map((value)=>{ const option = value; const IconComponent = option?.icon; return /*#__PURE__*/ React.createElement(Badge, { key: value.value, className: cn("px-1", isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }), subtle && finalSubtleStyles), style: { animationDuration: `${animation}s` } }, IconComponent && /*#__PURE__*/ React.createElement(IconComponent, { className: "mr-2 h-4 w-4" }), option?.label, /*#__PURE__*/ React.createElement(X, { className: "ml-2 size-3 cursor-pointer", onClick: (event)=>{ event.stopPropagation(); removeOption(value); } })); }), internalSelected.length > maxCount && /*#__PURE__*/ React.createElement(Badge, { className: cn("text-foreground border-foreground/1 bg-transparent px-1 hover:bg-transparent", isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }), subtle && finalSubtleStyles), style: { animationDuration: `${animation}s` } }, `+ ${internalSelected.length - maxCount} itens`, /*#__PURE__*/ React.createElement(X, { className: "ml-2 size-3 cursor-pointer", onClick: (event)=>{ event.stopPropagation(); clearExtraOptions(); } }))), /*#__PURE__*/ React.createElement("div", { className: "flex items-center justify-between gap-1" }, /*#__PURE__*/ React.createElement(TooltipProvider, null, /*#__PURE__*/ React.createElement(Tooltip, null, /*#__PURE__*/ React.createElement(TooltipTrigger, null, /*#__PURE__*/ React.createElement(X, { className: "text-muted-foreground h-4 cursor-pointer", onClick: (event)=>{ event.stopPropagation(); handleClear(); } })), /*#__PURE__*/ React.createElement(TooltipContent, null, /*#__PURE__*/ React.createElement("span", { className: "text-xs" }, "Limpar tudo")))), /*#__PURE__*/ React.createElement(Separator, { orientation: "vertical", className: "flex h-full min-h-6" }), /*#__PURE__*/ React.createElement(ChevronsUpDown, { className: "text-muted-foreground h-4 cursor-pointer" }))) : /*#__PURE__*/ React.createElement("div", { className: "flex w-full items-center justify-between" }, /*#__PURE__*/ React.createElement("span", { className: "text-muted-foreground" }, placeholder), /*#__PURE__*/ React.createElement(ChevronsUpDown, { className: "text-muted-foreground h-4 cursor-pointer" })))), /*#__PURE__*/ React.createElement(PopoverContent, { className: "w-auto p-0", align: "start", onEscapeKeyDown: ()=>setIsPopoverOpen(false), onOpenAutoFocus: (e)=>{ e.preventDefault(); // Foca no input sem causar scroll inputRef.current?.focus({ preventScroll: true }); } }, /*#__PURE__*/ React.createElement(Command, { shouldFilter: false }, /*#__PURE__*/ React.createElement(CommandInput, { ref: inputRef, placeholder: searchPlaceholder, value: inputValue, onValueChange: handleInputChange, onKeyDown: handleInputKeyDown }), /*#__PURE__*/ React.createElement(CommandList, null, /*#__PURE__*/ React.createElement(CommandEmpty, null, loading ? "Carregando..." : inputValue.trim() === "" ? "Digite algo para buscar..." : messageEmpty), options.length > 0 && /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(CommandGroup, null, /*#__PURE__*/ React.createElement(CommandItem, { key: "all", onSelect: ()=>{ toggleAll(); if (focusOnSelect) { inputRef.current?.focus({ preventScroll: true }); } }, className: "flex cursor-pointer items-center justify-between" }, /*#__PURE__*/ React.createElement("span", null, "(Selecionar tudo)"), /*#__PURE__*/ React.createElement("div", { className: cn(internalSelected.length === options.length ? "" : "opacity-50 [&_svg]:invisible") }, /*#__PURE__*/ React.createElement(Check, { className: "text-foreground h-4 w-4" }))), options.map((option, index)=>{ // Comparação convertendo AMBOS para string const optionValueStr = String(option.value); const isSelected = internalSelected.some((opt)=>String(opt.value) === optionValueStr); return /*#__PURE__*/ React.createElement(CommandItem, { key: `${option.value}-${isSelected}`, value: option.value, onSelect: ()=>{ // Usa o ref que é atualizado pelos event listeners globais const isShiftPressed = isShiftPressedRef.current; if (isShiftPressed) { toggleInterval(index); } else { toggleOption(option, index); } if (focusOnSelect) { // Foca sem causar scroll inputRef.current?.focus({ preventScroll: true }); } }, className: "flex cursor-pointer items-center justify-between" }, /*#__PURE__*/ React.createElement("div", { className: "flex items-center" }, option.icon && /*#__PURE__*/ React.createElement(option.icon, { className: "text-muted-foreground mr-2 h-4 w-4" }), /*#__PURE__*/ React.createElement("span", null, option.label)), /*#__PURE__*/ React.createElement("div", { className: cn(isSelected ? "" : "opacity-50 [&_svg]:invisible") }, /*#__PURE__*/ React.createElement(Check, { className: "text-foreground h-4 w-4" }))); })), /*#__PURE__*/ React.createElement(CommandSeparator, null))))), animation > 0 && internalSelected.length > 0 && /*#__PURE__*/ React.createElement(WandSparkles, { className: cn("text-foreground bg-background my-2 h-3 w-3 cursor-pointer", isAnimating ? "" : "text-muted-foreground"), onClick: ()=>setIsAnimating(!isAnimating) })), description && /*#__PURE__*/ React.createElement("p", { className: "text-muted-foreground text-sm" }, description)); }); AutoCompleteMultiSelect.displayName = "AutoCompleteMultiSelect"; export { AutoCompleteMultiSelect }; //# sourceMappingURL=index.mjs.map