periplo-ui 3.8.1 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Combobox/Combobox.d.ts +69 -0
- package/dist/components/Combobox/Combobox.js +76 -0
- package/dist/components/Combobox/Combobox.js.map +1 -0
- package/dist/components/MultiSelect/MultiSelect.js +2 -2
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type ComboboxOption = {
|
|
2
|
+
/** The unique identifier or value of the option */
|
|
3
|
+
value: string;
|
|
4
|
+
/** The text to display for the option */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Additional properties that can be used in custom rendering */
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
};
|
|
9
|
+
export type ComboboxProps = {
|
|
10
|
+
/** Array of options to display in the combobox */
|
|
11
|
+
options: ComboboxOption[];
|
|
12
|
+
/** Currently selected value */
|
|
13
|
+
value?: string;
|
|
14
|
+
/** Callback fired when selection changes */
|
|
15
|
+
onChange: (value: string) => void;
|
|
16
|
+
/** Placeholder text shown when no option is selected */
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/** Placeholder text for the search input field */
|
|
19
|
+
searchPlaceholder?: string;
|
|
20
|
+
/** Message shown when no options match the search query */
|
|
21
|
+
emptyMessage?: string;
|
|
22
|
+
/** Additional CSS classes to apply to the combobox trigger */
|
|
23
|
+
className?: string;
|
|
24
|
+
/** Whether the combobox is disabled */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/** Maximum height of the options list. Can be any valid CSS height value */
|
|
27
|
+
maxHeight?: string | number;
|
|
28
|
+
/** Custom render function for options. If not provided, defaults to showing a checkbox and label */
|
|
29
|
+
renderOption?: (option: ComboboxOption, isSelected: boolean) => React.ReactNode;
|
|
30
|
+
/** Whether the selection can be cleared by selecting the same option again */
|
|
31
|
+
clearable?: boolean;
|
|
32
|
+
/** Whether to close the dropdown when an option is selected */
|
|
33
|
+
closeOnSelect?: boolean;
|
|
34
|
+
/** Whether the combobox is in a loading state */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
/** Message to show when in loading state */
|
|
37
|
+
loadingPlaceholder?: string;
|
|
38
|
+
/** Whether the combobox has an error */
|
|
39
|
+
error?: boolean | string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* <Combobox
|
|
47
|
+
* options={[
|
|
48
|
+
* { value: '1', label: 'Option 1' },
|
|
49
|
+
* { value: '2', label: 'Option 2' }
|
|
50
|
+
* ]}
|
|
51
|
+
* value={selectedValue}
|
|
52
|
+
* onChange={setValue}
|
|
53
|
+
* />
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @example Custom Rendering
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <Combobox
|
|
59
|
+
* options={options}
|
|
60
|
+
* renderOption={(option, isSelected) => (
|
|
61
|
+
* <div className="flex items-center">
|
|
62
|
+
* <CustomIcon className="mr-2" />
|
|
63
|
+
* {option.label}
|
|
64
|
+
* </div>
|
|
65
|
+
* )}
|
|
66
|
+
* />
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare const Combobox: ({ options, value, onChange, placeholder, searchPlaceholder, emptyMessage, className, disabled, maxHeight, renderOption, clearable, closeOnSelect, loading, loadingPlaceholder, error, }: ComboboxProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { CaretUpDown, Check } from '@phosphor-icons/react';
|
|
3
|
+
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../Command/Command.js';
|
|
4
|
+
import { PopoverRoot, PopoverTrigger, PopoverContent } from '../Popover/Popover.js';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { cn } from '../../lib/utils.js';
|
|
7
|
+
import { Button } from '../Button/Button.js';
|
|
8
|
+
|
|
9
|
+
const Combobox = ({
|
|
10
|
+
options,
|
|
11
|
+
value,
|
|
12
|
+
onChange,
|
|
13
|
+
placeholder = "Seleccionar...",
|
|
14
|
+
searchPlaceholder = "Buscar...",
|
|
15
|
+
emptyMessage = "No se encontraron resultados.",
|
|
16
|
+
className = "w-60",
|
|
17
|
+
disabled = false,
|
|
18
|
+
maxHeight = "300px",
|
|
19
|
+
renderOption,
|
|
20
|
+
clearable = false,
|
|
21
|
+
closeOnSelect = true,
|
|
22
|
+
loading = false,
|
|
23
|
+
loadingPlaceholder = "Cargando...",
|
|
24
|
+
error = false
|
|
25
|
+
}) => {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const selectedOption = options.find((option) => option.value === value);
|
|
28
|
+
const handleSelect = (currentValue) => {
|
|
29
|
+
const newValue = clearable && currentValue === value ? "" : currentValue;
|
|
30
|
+
onChange(newValue);
|
|
31
|
+
if (closeOnSelect) {
|
|
32
|
+
setOpen(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-1", children: [
|
|
36
|
+
/* @__PURE__ */ jsxs(PopoverRoot, { open, onOpenChange: setOpen, children: [
|
|
37
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
38
|
+
Button,
|
|
39
|
+
{
|
|
40
|
+
type: "button",
|
|
41
|
+
disabled,
|
|
42
|
+
variant: "text",
|
|
43
|
+
className: cn(
|
|
44
|
+
"flex h-10 w-full items-center justify-between rounded-lg border border-neutral-200 bg-white px-3 text-left outline-none transition-colors",
|
|
45
|
+
open && "border-neutral-950",
|
|
46
|
+
"hover:bg-neutral-50",
|
|
47
|
+
"focus-visible:border-neutral-950 focus-visible:ring-0",
|
|
48
|
+
disabled && "cursor-not-allowed bg-neutral-50 opacity-50",
|
|
49
|
+
error && "border-error-400 focus-visible:border-error-700",
|
|
50
|
+
className
|
|
51
|
+
),
|
|
52
|
+
"aria-expanded": open,
|
|
53
|
+
"aria-haspopup": "listbox",
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsx("span", { className: cn("block truncate", !selectedOption?.label && "text-neutral-300"), children: selectedOption?.label ?? placeholder }),
|
|
56
|
+
/* @__PURE__ */ jsx(CaretUpDown, { className: "h-4 w-4 shrink-0 opacity-50" })
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
) }),
|
|
60
|
+
/* @__PURE__ */ jsx(PopoverContent, { className: "p-0", children: /* @__PURE__ */ jsxs(Command, { children: [
|
|
61
|
+
/* @__PURE__ */ jsx(CommandInput, { placeholder: searchPlaceholder, disabled: loading }),
|
|
62
|
+
/* @__PURE__ */ jsx(CommandList, { style: { maxHeight }, children: loading ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex items-center justify-center py-6 text-sm", children: loadingPlaceholder }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
63
|
+
/* @__PURE__ */ jsx(CommandEmpty, { children: emptyMessage }),
|
|
64
|
+
/* @__PURE__ */ jsx(CommandGroup, { children: options.map((option) => /* @__PURE__ */ jsx(CommandItem, { value: option.value, onSelect: handleSelect, children: renderOption ? renderOption(option, value === option.value) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
65
|
+
/* @__PURE__ */ jsx(Check, { className: `mr-2 h-4 w-4 ${value === option.value ? "opacity-100" : "opacity-0"}` }),
|
|
66
|
+
option.label
|
|
67
|
+
] }) }, option.value)) })
|
|
68
|
+
] }) })
|
|
69
|
+
] }) })
|
|
70
|
+
] }),
|
|
71
|
+
typeof error === "string" && /* @__PURE__ */ jsx("span", { className: "text-sm text-error-500", children: error })
|
|
72
|
+
] });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export { Combobox };
|
|
76
|
+
//# sourceMappingURL=Combobox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Combobox.js","sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["import { CaretUpDown, Check } from '@phosphor-icons/react'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\nimport { useState } from 'react'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../Button'\n\nexport type ComboboxOption = {\n /** The unique identifier or value of the option */\n value: string\n /** The text to display for the option */\n label: string\n /** Additional properties that can be used in custom rendering */\n [key: string]: any\n}\n\nexport type ComboboxProps = {\n /** Array of options to display in the combobox */\n options: ComboboxOption[]\n /** Currently selected value */\n value?: string\n /** Callback fired when selection changes */\n onChange: (value: string) => void\n /** Placeholder text shown when no option is selected */\n placeholder?: string\n /** Placeholder text for the search input field */\n searchPlaceholder?: string\n /** Message shown when no options match the search query */\n emptyMessage?: string\n /** Additional CSS classes to apply to the combobox trigger */\n className?: string\n /** Whether the combobox is disabled */\n disabled?: boolean\n /** Maximum height of the options list. Can be any valid CSS height value */\n maxHeight?: string | number\n /** Custom render function for options. If not provided, defaults to showing a checkbox and label */\n renderOption?: (option: ComboboxOption, isSelected: boolean) => React.ReactNode\n /** Whether the selection can be cleared by selecting the same option again */\n clearable?: boolean\n /** Whether to close the dropdown when an option is selected */\n closeOnSelect?: boolean\n /** Whether the combobox is in a loading state */\n loading?: boolean\n /** Message to show when in loading state */\n loadingPlaceholder?: string\n /** Whether the combobox has an error */\n error?: boolean | string\n}\n\n/**\n * A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.\n *\n * @example\n * ```tsx\n * <Combobox\n * options={[\n * { value: '1', label: 'Option 1' },\n * { value: '2', label: 'Option 2' }\n * ]}\n * value={selectedValue}\n * onChange={setValue}\n * />\n * ```\n *\n * @example Custom Rendering\n * ```tsx\n * <Combobox\n * options={options}\n * renderOption={(option, isSelected) => (\n * <div className=\"flex items-center\">\n * <CustomIcon className=\"mr-2\" />\n * {option.label}\n * </div>\n * )}\n * />\n * ```\n */\nexport const Combobox = ({\n options,\n value,\n onChange,\n placeholder = 'Seleccionar...',\n searchPlaceholder = 'Buscar...',\n emptyMessage = 'No se encontraron resultados.',\n className = 'w-60',\n disabled = false,\n maxHeight = '300px',\n renderOption,\n clearable = false,\n closeOnSelect = true,\n loading = false,\n loadingPlaceholder = 'Cargando...',\n error = false,\n}: ComboboxProps) => {\n const [open, setOpen] = useState(false)\n\n const selectedOption = options.find((option) => option.value === value)\n\n const handleSelect = (currentValue: string) => {\n const newValue = clearable && currentValue === value ? '' : currentValue\n onChange(newValue)\n if (closeOnSelect) {\n setOpen(false)\n }\n }\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <PopoverRoot open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n disabled={disabled}\n variant=\"text\"\n className={cn(\n 'flex h-10 w-full items-center justify-between rounded-lg border border-neutral-200 bg-white px-3 text-left outline-none transition-colors',\n open && 'border-neutral-950',\n 'hover:bg-neutral-50',\n 'focus-visible:border-neutral-950 focus-visible:ring-0',\n disabled && 'cursor-not-allowed bg-neutral-50 opacity-50',\n error && 'border-error-400 focus-visible:border-error-700',\n className,\n )}\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n >\n <span className={cn('block truncate', !selectedOption?.label && 'text-neutral-300')}>\n {selectedOption?.label ?? placeholder}\n </span>\n <CaretUpDown className=\"h-4 w-4 shrink-0 opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"p-0\">\n <Command>\n <CommandInput placeholder={searchPlaceholder} disabled={loading} />\n <CommandList style={{ maxHeight }}>\n {loading ? (\n <div className=\"text-muted-foreground flex items-center justify-center py-6 text-sm\">\n {loadingPlaceholder}\n </div>\n ) : (\n <>\n <CommandEmpty>{emptyMessage}</CommandEmpty>\n <CommandGroup>\n {options.map((option) => (\n <CommandItem key={option.value} value={option.value} onSelect={handleSelect}>\n {renderOption ? (\n renderOption(option, value === option.value)\n ) : (\n <>\n <Check className={`mr-2 h-4 w-4 ${value === option.value ? 'opacity-100' : 'opacity-0'}`} />\n {option.label}\n </>\n )}\n </CommandItem>\n ))}\n </CommandGroup>\n </>\n )}\n </CommandList>\n </Command>\n </PopoverContent>\n </PopoverRoot>\n {typeof error === 'string' && <span className=\"text-sm text-error-500\">{error}</span>}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -46,8 +46,8 @@ const MultiSelect = React.forwardRef(
|
|
|
46
46
|
}
|
|
47
47
|
}, []);
|
|
48
48
|
React.useEffect(() => {
|
|
49
|
-
onChange?.(selected.map((
|
|
50
|
-
}, [selected
|
|
49
|
+
onChange?.(selected.map((item) => item.value));
|
|
50
|
+
}, [selected]);
|
|
51
51
|
return /* @__PURE__ */ jsx(PopoverRoot, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(Command, { onKeyDown: handleKeyDown, children: [
|
|
52
52
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
53
53
|
Button,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.js","sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\nimport { CaretUpDown, Check } from '@phosphor-icons/react'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\nimport { Chip } from '../Chip'\nimport { Button } from '../Button'\nimport { ControllerFieldState } from 'react-hook-form'\n\n/**\n * Represents a selectable option in the MultiSelect component\n */\ntype Option = {\n /** Unique identifier for the option */\n value: string\n /** Display text for the option */\n label: string\n}\n\nexport interface MultiSelectProps {\n /**\n * Array of options to display in the select menu\n */\n options: Array<Option>\n\n /**\n * Props to be passed to the individual Chip components\n */\n chipProps?: React.ComponentProps<typeof Chip>\n\n /**\n * Callback fired when selected values change\n * @param value Array of selected option values\n */\n onChange?: (value: string[]) => void\n\n /**\n * Form field state from react-hook-form\n */\n fieldState?: ControllerFieldState\n\n /**\n * Initially selected option values\n */\n defaultValue?: Array<string>\n\n /**\n * Text shown in the select button when no option is selected\n * @default \"Select your options...\"\n */\n placeholder?: string\n\n /**\n * Text shown when no options match the search query\n * @default \"Options not found\"\n */\n notFoundText?: string\n\n /**\n * Placeholder text for the search input\n * @default \"Search...\"\n */\n commandInputPlaceholder?: string\n\n /**\n * Maximum number of items that can be selected\n * Shows a counter chip when specified\n */\n max?: number\n}\n\n/**\n * A multi-select component with search functionality and chip display for selected items.\n * Supports keyboard navigation, search filtering, and maximum selection limit.\n *\n * @example\n * // Basic usage\n * <MultiSelect\n * options={[\n * { value: '1', label: 'Option 1' },\n * { value: '2', label: 'Option 2' }\n * ]}\n * onChange={(values) => console.log(values)}\n * />\n *\n * // With maximum selection limit\n * <MultiSelect\n * options={options}\n * max={3}\n * placeholder=\"Select up to 3 options\"\n * />\n *\n * // With custom chip styling\n * <MultiSelect\n * options={options}\n * chipProps={{ variant: 'primary' }}\n * />\n */\nexport const MultiSelect = React.forwardRef<HTMLInputElement, MultiSelectProps>(\n (\n {\n options,\n onChange,\n chipProps,\n defaultValue,\n placeholder = 'Select your options...',\n notFoundText = 'Options not found',\n fieldState,\n commandInputPlaceholder = 'Search...',\n max,\n },\n _ref,\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const [open, setOpen] = React.useState(false)\n const [selected, setSelected] = React.useState<MultiSelectProps['options']>(\n options.filter((option) => defaultValue?.includes(option.value) ?? []),\n )\n const [inputValue, setInputValue] = React.useState('')\n\n const handleUnselect = React.useCallback((item: Option) => {\n setSelected((prev) => prev.filter((s) => s.value !== item.value))\n }, [])\n\n const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {\n const input = inputRef.current\n if (input) {\n if (e.key === 'Delete' || e.key === 'Backspace') {\n if (input.value === '') {\n setSelected((prev) => {\n const newSelected = [...prev]\n newSelected.pop()\n return newSelected\n })\n }\n }\n\n if (e.key === 'Escape') {\n input.blur()\n }\n }\n }, [])\n\n React.useEffect(() => {\n onChange?.(selected.map((
|
|
1
|
+
{"version":3,"file":"MultiSelect.js","sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\nimport { CaretUpDown, Check } from '@phosphor-icons/react'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\nimport { Chip } from '../Chip'\nimport { Button } from '../Button'\nimport { ControllerFieldState } from 'react-hook-form'\n\n/**\n * Represents a selectable option in the MultiSelect component\n */\ntype Option = {\n /** Unique identifier for the option */\n value: string\n /** Display text for the option */\n label: string\n}\n\nexport interface MultiSelectProps {\n /**\n * Array of options to display in the select menu\n */\n options: Array<Option>\n\n /**\n * Props to be passed to the individual Chip components\n */\n chipProps?: React.ComponentProps<typeof Chip>\n\n /**\n * Callback fired when selected values change\n * @param value Array of selected option values\n */\n onChange?: (value: string[]) => void\n\n /**\n * Form field state from react-hook-form\n */\n fieldState?: ControllerFieldState\n\n /**\n * Initially selected option values\n */\n defaultValue?: Array<string>\n\n /**\n * Text shown in the select button when no option is selected\n * @default \"Select your options...\"\n */\n placeholder?: string\n\n /**\n * Text shown when no options match the search query\n * @default \"Options not found\"\n */\n notFoundText?: string\n\n /**\n * Placeholder text for the search input\n * @default \"Search...\"\n */\n commandInputPlaceholder?: string\n\n /**\n * Maximum number of items that can be selected\n * Shows a counter chip when specified\n */\n max?: number\n}\n\n/**\n * A multi-select component with search functionality and chip display for selected items.\n * Supports keyboard navigation, search filtering, and maximum selection limit.\n *\n * @example\n * // Basic usage\n * <MultiSelect\n * options={[\n * { value: '1', label: 'Option 1' },\n * { value: '2', label: 'Option 2' }\n * ]}\n * onChange={(values) => console.log(values)}\n * />\n *\n * // With maximum selection limit\n * <MultiSelect\n * options={options}\n * max={3}\n * placeholder=\"Select up to 3 options\"\n * />\n *\n * // With custom chip styling\n * <MultiSelect\n * options={options}\n * chipProps={{ variant: 'primary' }}\n * />\n */\nexport const MultiSelect = React.forwardRef<HTMLInputElement, MultiSelectProps>(\n (\n {\n options,\n onChange,\n chipProps,\n defaultValue,\n placeholder = 'Select your options...',\n notFoundText = 'Options not found',\n fieldState,\n commandInputPlaceholder = 'Search...',\n max,\n },\n _ref,\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const [open, setOpen] = React.useState(false)\n const [selected, setSelected] = React.useState<MultiSelectProps['options']>(\n options.filter((option) => defaultValue?.includes(option.value) ?? []),\n )\n const [inputValue, setInputValue] = React.useState('')\n\n const handleUnselect = React.useCallback((item: Option) => {\n setSelected((prev) => prev.filter((s) => s.value !== item.value))\n }, [])\n\n const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {\n const input = inputRef.current\n if (input) {\n if (e.key === 'Delete' || e.key === 'Backspace') {\n if (input.value === '') {\n setSelected((prev) => {\n const newSelected = [...prev]\n newSelected.pop()\n return newSelected\n })\n }\n }\n\n if (e.key === 'Escape') {\n input.blur()\n }\n }\n }, [])\n\n React.useEffect(() => {\n onChange?.(selected.map((item) => item.value))\n }, [selected])\n\n return (\n <PopoverRoot open={open} onOpenChange={setOpen}>\n <Command onKeyDown={handleKeyDown}>\n <PopoverTrigger asChild>\n <Button\n variant=\"ghost\"\n role=\"combobox\"\n aria-expanded={open}\n className={`w-full justify-between bg-white ${\n fieldState?.invalid ? 'border-error-400 focus-within:border-error-700' : ''\n }`}\n >\n {placeholder}\n <CaretUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"px-0 py-0\">\n <CommandInput\n placeholder={commandInputPlaceholder}\n ref={inputRef}\n value={inputValue}\n onValueChange={setInputValue}\n >\n {max && (\n <Chip\n variant={selected.length >= max ? 'primary' : 'default'}\n size=\"sm\"\n className=\"my-1 whitespace-nowrap\"\n >\n {selected.length} / {max}\n </Chip>\n )}\n </CommandInput>\n\n <CommandList>\n <CommandEmpty>{notFoundText}</CommandEmpty>\n {open && options.length > 0 && (\n <CommandGroup>\n {options.map((item) => (\n <CommandItem\n key={`item-${item.value}`}\n onMouseDown={(e) => {\n e.preventDefault()\n e.stopPropagation()\n }}\n onSelect={(value) => {\n const isSelected = selected.find((s) => s.label === value)\n if (isSelected) {\n handleUnselect(isSelected)\n } else if (!max || selected.length < max) {\n setInputValue('')\n setSelected((prev) => [...prev, item])\n }\n }}\n className=\"cursor-pointer\"\n >\n <div className=\"flex\">\n <div className=\"mr-2\">\n {selected.find((s) => s.value === item.value) ? (\n <Check className=\"h-4 w-4\" />\n ) : (\n <div className=\"h-4 w-4\" />\n )}\n </div>\n <div>{item.label}</div>\n </div>\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n </CommandList>\n </PopoverContent>\n {selected.length > 0 && (\n <div className=\"flex flex-wrap gap-2 pt-2\">\n {selected.map((item) => (\n <Chip key={item.value} {...chipProps} className=\"gap-2 pr-1\" onRemove={() => handleUnselect(item)}>\n {item.label}\n </Chip>\n ))}\n </div>\n )}\n </Command>\n </PopoverRoot>\n )\n },\n)\n\nMultiSelect.displayName = 'MultiSelect'\n"],"names":[],"mappings":";;;;;;;;;AAmGO;AAA0B;AAE7B;AACE;AACA;AACA;AACA;AACc;AACC;AACf;AAC0B;AAC1B;AAIF;AACA;AACA;AAAsC;AACiC;AAEvE;AAEA;AACE;AAAgE;AAGlE;AACE;AACA;AACE;AACE;AACE;AACE;AACA;AACA;AAAO;AACR;AACH;AAGF;AACE;AAAW;AACb;AACF;AAGF;AACE;AAA6C;AAG/C;AAGM;AACE;AAAC;AAAA;AACS;AACH;AACU;AAGf;AAEC;AAAA;AACyD;AAAA;AAAA;AAE9D;AAEE;AAAA;AAAC;AAAA;AACc;AACR;AACE;AACQ;AAGb;AAAC;AAAA;AAC+C;AACzC;AACK;AAET;AAAS;AAAO;AAAI;AAAA;AAAA;AACvB;AAAA;AAEJ;AAGE;AAA4B;AAItB;AAAC;AAAA;AAGG;AACA;AAAkB;AACpB;AAEE;AACA;AACE;AAAyB;AAEzB;AACA;AAAqC;AACvC;AACF;AACU;AAGR;AAMA;AACiB;AACnB;AAAA;AAzBuB;AA4B7B;AAEJ;AACF;AAQE;AAGN;AAGN;AAEA;;"}
|