art-bd-ui 1.0.0 → 1.0.2

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.
@@ -17,7 +17,18 @@ const popoverContentVariants = classVarianceAuthority.cva([
17
17
  sm: "w-56",
18
18
  lg: "w-96",
19
19
  },
20
+ matchTriggerWidth: {
21
+ true: null,
22
+ false: null,
23
+ },
20
24
  },
25
+ compoundVariants: [
26
+ {
27
+ size: ["default", "sm", "lg"],
28
+ matchTriggerWidth: true,
29
+ class: "w-(--radix-popover-trigger-width)",
30
+ },
31
+ ],
21
32
  defaultVariants: {
22
33
  size: "default",
23
34
  },
@@ -37,8 +48,8 @@ const PopoverTrigger = (_a) => {
37
48
  return jsxRuntime.jsx(reactPopover.Trigger, Object.assign({ "data-slot": "popover-trigger" }, props));
38
49
  };
39
50
  const PopoverContent = (_a) => {
40
- var { className, align = "center", arrow = false, withClose = false, sideOffset = 4, size, children } = _a, props = tslib.__rest(_a, ["className", "align", "arrow", "withClose", "sideOffset", "size", "children"]);
41
- return (jsxRuntime.jsx(reactPopover.Portal, { children: jsxRuntime.jsxs(reactPopover.Content, Object.assign({ "data-slot": "popover-content", align: align, sideOffset: sideOffset, className: utils.cn(popoverContentVariants({ size }), [
51
+ var { className, align = "center", arrow = false, withClose = false, sideOffset = 4, size, matchTriggerWidth = false, children } = _a, props = tslib.__rest(_a, ["className", "align", "arrow", "withClose", "sideOffset", "size", "matchTriggerWidth", "children"]);
52
+ return (jsxRuntime.jsx(reactPopover.Portal, { children: jsxRuntime.jsxs(reactPopover.Content, Object.assign({ "data-slot": "popover-content", align: align, sideOffset: sideOffset, className: utils.cn(popoverContentVariants({ size, matchTriggerWidth }), [
42
53
  "data-[state=open]:animate-in",
43
54
  "data-[state=open]:fade-in-0",
44
55
  "data-[state=open]:zoom-in-95",
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var react = require('react');
6
+ var badge = require('../../../ui/badge/badge.js');
7
+ var icon = require('../../../media/icon/icon.js');
8
+ var utils = require('../../../../lib/utils.js');
9
+
10
+ const BadgeList = react.memo(({ options, disabled, onRemove }) => {
11
+ const handleBadgeKeyDown = (e, opt) => {
12
+ e.stopPropagation();
13
+ if (disabled) {
14
+ return;
15
+ }
16
+ if (e.key === "Enter" || e.key === " ") {
17
+ e.preventDefault();
18
+ onRemove(opt);
19
+ }
20
+ else if (e.key === "Backspace" || e.key === "Delete") {
21
+ e.preventDefault();
22
+ onRemove(opt);
23
+ const nextBadge = e.currentTarget.nextElementSibling || e.currentTarget.previousElementSibling;
24
+ if (nextBadge instanceof HTMLElement) {
25
+ nextBadge.focus();
26
+ }
27
+ }
28
+ else if (e.key === "ArrowRight") {
29
+ e.preventDefault();
30
+ const nextBadge = e.currentTarget.nextElementSibling;
31
+ if (nextBadge instanceof HTMLElement) {
32
+ nextBadge.focus();
33
+ }
34
+ }
35
+ else if (e.key === "ArrowLeft") {
36
+ e.preventDefault();
37
+ const prevBadge = e.currentTarget.previousElementSibling;
38
+ if (prevBadge instanceof HTMLElement) {
39
+ prevBadge.focus();
40
+ }
41
+ }
42
+ };
43
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: options.map((opt) => (jsxRuntime.jsxs(badge.Badge, { tabIndex: disabled ? -1 : 0, role: "button", "aria-label": `Remove ${opt.label}`, variant: "secondary", className: utils.cn("gap-1 select-none", !disabled && "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", !disabled && "cursor-pointer hover:bg-secondary/80"), onClick: () => {
44
+ if (!disabled) {
45
+ onRemove(opt);
46
+ }
47
+ }, onKeyDown: (e) => handleBadgeKeyDown(e, opt), children: [opt.label, !disabled && jsxRuntime.jsx(icon.Icon, { name: "x", className: "size-3" })] }, opt.value))) }));
48
+ });
49
+ BadgeList.displayName = "BadgeList";
50
+
51
+ exports.BadgeList = BadgeList;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var react = require('react');
6
+ var command = require('../../../ui/command/command.js');
7
+ var checkbox = require('../../../forms/checkbox/checkbox.js');
8
+
9
+ const OptionItem = react.memo(({ option, selected, onSelect }) => (jsxRuntime.jsxs(command.CommandItem, { value: option.value, keywords: [option.label], onSelect: () => onSelect(option), className: "flex items-center gap-2", children: [jsxRuntime.jsx(checkbox.Checkbox, { tabIndex: -1, checked: selected, size: "sm" }), option.label] })));
10
+ OptionItem.displayName = "OptionItem";
11
+ const OptionsList = react.memo(({ options, selectedValues, onSelect }) => (jsxRuntime.jsx(jsxRuntime.Fragment, { children: options.map(({ value, label, children, data }) => children.length > 0 ? (jsxRuntime.jsx(command.CommandGroup, { heading: label, children: jsxRuntime.jsx(OptionsList, { options: children, selectedValues: selectedValues, onSelect: onSelect }) }, value)) : data ? (jsxRuntime.jsx(OptionItem, { option: data, selected: selectedValues.has(value), onSelect: onSelect }, value)) : null) })));
12
+ OptionsList.displayName = "OptionsList";
13
+
14
+ exports.OptionsList = OptionsList;
@@ -0,0 +1,119 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var tslib = require('tslib');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var react = require('react');
7
+ var options = require('./components/options.js');
8
+ var badgeList = require('./components/badge-list.js');
9
+ var command = require('../../ui/command/command.js');
10
+ var popover = require('../../popovers/popover/popover.js');
11
+ var utils = require('../../../lib/utils.js');
12
+ var useDebounce = require('../../../hooks/use-debounce.js');
13
+ require('clsx');
14
+ var toggle = require('../../../utils/toggle.js');
15
+ var useUpdateEffect = require('../../../hooks/useUpdateEffect.js');
16
+ require('lodash/throttle');
17
+ var iconButton = require('../../buttons/icon-button/icon-button.js');
18
+ var flex = require('../../layout/flex/flex.js');
19
+
20
+ function isPromise(value) {
21
+ return value instanceof Promise;
22
+ }
23
+ function getGroupedListOptions(options, groups = []) {
24
+ const lookup = groups.reduce((acc, { value, label }) => {
25
+ acc[value] = { value, label, children: [] };
26
+ return acc;
27
+ }, {});
28
+ const ungrouped = [];
29
+ for (const opt of options) {
30
+ const node = {
31
+ value: opt.value,
32
+ label: opt.label,
33
+ children: [],
34
+ data: opt,
35
+ };
36
+ if (opt.group && lookup[opt.group]) {
37
+ lookup[opt.group].children.push(node);
38
+ }
39
+ else {
40
+ ungrouped.push(node);
41
+ }
42
+ }
43
+ const result = [];
44
+ for (const grp of groups) {
45
+ const bucket = lookup[grp.value];
46
+ if (bucket.children.length > 0) {
47
+ result.push(bucket);
48
+ }
49
+ }
50
+ return result.concat(ungrouped);
51
+ }
52
+ const defaultValue = [];
53
+ const defaultOptions = [];
54
+ const defaultGroups = [];
55
+ const MultiSelect = (_a) => {
56
+ var { value = defaultValue, onChange = () => { }, options: options$1 = defaultOptions, groups = defaultGroups, createable = false, onCreate = () => { }, onSearch, debounceDelay = 500, placeholder = "Select options...", disabled = false, clearable = false, className } = _a, props = tslib.__rest(_a, ["value", "onChange", "options", "groups", "createable", "onCreate", "onSearch", "debounceDelay", "placeholder", "disabled", "clearable", "className"]);
57
+ const [open, setOpen] = react.useState(false);
58
+ const [loading, setLoading] = react.useState(false);
59
+ const [searchQuery, setSearchQuery] = react.useState("");
60
+ const debouncedSearch = useDebounce.useDebounce(searchQuery, debounceDelay);
61
+ const [searchResults, setSearchResults] = react.useState(options$1);
62
+ useUpdateEffect.useUpdateEffect(() => {
63
+ if (!onSearch || !debouncedSearch) {
64
+ return;
65
+ }
66
+ const search = () => tslib.__awaiter(void 0, void 0, void 0, function* () {
67
+ try {
68
+ const results = onSearch(debouncedSearch);
69
+ if (isPromise(results)) {
70
+ setLoading(true);
71
+ const options = yield results;
72
+ setSearchResults(options);
73
+ }
74
+ else if (Array.isArray(results)) {
75
+ setSearchResults(results);
76
+ }
77
+ }
78
+ catch (error) {
79
+ console.error("Search failed:", error);
80
+ setSearchResults([]);
81
+ }
82
+ finally {
83
+ setLoading(false);
84
+ }
85
+ });
86
+ void search();
87
+ }, [debouncedSearch, onSearch]);
88
+ const handleSelect = react.useCallback((opt) => {
89
+ onChange(toggle.toggle(value, opt, (o) => o.value));
90
+ }, [onChange, value]);
91
+ const handleRemove = react.useCallback((opt) => onChange(value.filter((v) => v.value !== opt.value)), [onChange, value]);
92
+ const handleClear = react.useCallback(() => onChange([]), [onChange]);
93
+ const optionsList = react.useMemo(() => getGroupedListOptions(searchResults, groups), [searchResults, groups]);
94
+ const selectedValues = react.useMemo(() => new Set(value.map((v) => v.value)), [value]);
95
+ const handleTriggerKeyDown = react.useCallback((e) => {
96
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
97
+ e.preventDefault();
98
+ setOpen(true);
99
+ }
100
+ }, []);
101
+ const handleClearKeyDown = react.useCallback((e) => {
102
+ if (e.key === "Enter" || e.key === " ") {
103
+ e.preventDefault();
104
+ handleClear();
105
+ }
106
+ }, [handleClear]);
107
+ const setPopoverVisibility = react.useCallback((open) => {
108
+ if (!disabled) {
109
+ setOpen(open);
110
+ }
111
+ }, [disabled]);
112
+ return (jsxRuntime.jsxs(popover.Popover, { open: open, onOpenChange: setPopoverVisibility, children: [jsxRuntime.jsx(popover.PopoverTrigger, { asChild: true, children: jsxRuntime.jsxs("div", Object.assign({}, props, { tabIndex: disabled ? -1 : 0, role: "combobox", "aria-expanded": open, "aria-haspopup": "listbox", "aria-controls": "multiselect-options", "aria-label": placeholder, "aria-disabled": disabled, onKeyDown: handleTriggerKeyDown, className: utils.cn("flex min-h-10 w-full flex-wrap items-center gap-1 rounded-md border bg-background px-3 py-2", !disabled && "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", disabled && "disabled:cursor-not-allowed disabled:opacity-50", className), children: [jsxRuntime.jsx(flex.Flex, { wrap: "wrap", gap: 1, align: "start", className: "flex-1", children: value.length > 0 ? (jsxRuntime.jsx(badgeList.BadgeList, { options: value, disabled: disabled, onRemove: handleRemove })) : (jsxRuntime.jsx("span", { className: "text-muted-foreground", children: placeholder })) }), clearable && !disabled && value.length > 0 && (jsxRuntime.jsx(iconButton.IconButton, { icon: "circle-x", radius: "full", variant: "ghost", className: "size-5 p-0.25 self-start shrink-0", onClick: handleClear, onKeyDown: handleClearKeyDown, disabled: disabled, tooltip: "Clear All", "aria-label": "Clear all selections" }))] })) }), jsxRuntime.jsx(popover.PopoverContent, { className: "p-0", align: "start", matchTriggerWidth: true, children: jsxRuntime.jsxs(command.Command, { id: "multiselect-options", role: "listbox", shouldFilter: !onSearch, children: [jsxRuntime.jsx(command.CommandInput, { placeholder: "Search...", value: searchQuery, onValueChange: setSearchQuery, disabled: disabled }), jsxRuntime.jsxs(command.CommandList, { children: [loading ? (jsxRuntime.jsx(command.CommandLoading, { children: "Searching\u2026" })) : optionsList.length > 0 ? (jsxRuntime.jsx(options.OptionsList, { options: optionsList, selectedValues: selectedValues, onSelect: handleSelect })) : (jsxRuntime.jsx(command.CommandEmpty, { children: "No results found." })), createable && !loading && searchQuery && debouncedSearch && (jsxRuntime.jsxs(command.CommandItem, { value: searchQuery, keywords: [searchQuery], onSelect: () => {
113
+ onCreate(searchQuery);
114
+ setSearchQuery("");
115
+ }, children: ["Create \"", searchQuery, "\""] }))] })] }) })] }));
116
+ };
117
+ MultiSelect.displayName = "MultiSelect";
118
+
119
+ exports.MultiSelect = MultiSelect;
@@ -43,6 +43,11 @@ const CommandEmpty = (_a) => {
43
43
  return jsxRuntime.jsx(cmdk.Command.Empty, Object.assign({ "data-slot": "command-empty", className: "py-6 text-center text-sm" }, props));
44
44
  };
45
45
  CommandEmpty.displayName = "CommandEmpty";
46
+ const CommandLoading = (_a) => {
47
+ var props = tslib.__rest(_a, []);
48
+ return jsxRuntime.jsx(cmdk.Command.Loading, Object.assign({ "data-slot": "command-loading", className: "py-6 text-center text-sm" }, props));
49
+ };
50
+ CommandEmpty.displayName = "CommandEmpty";
46
51
  const CommandGroup = (_a) => {
47
52
  var { className } = _a, props = tslib.__rest(_a, ["className"]);
48
53
  return (jsxRuntime.jsx(cmdk.Command.Group, Object.assign({ "data-slot": "command-group", className: utils.cn("overflow-hidden p-1", "text-foreground",
@@ -79,5 +84,6 @@ exports.CommandGroup = CommandGroup;
79
84
  exports.CommandInput = CommandInput;
80
85
  exports.CommandItem = CommandItem;
81
86
  exports.CommandList = CommandList;
87
+ exports.CommandLoading = CommandLoading;
82
88
  exports.CommandSeparator = CommandSeparator;
83
89
  exports.CommandShortcut = CommandShortcut;
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ function useDebounce(value, delay) {
6
+ const [debouncedValue, setDebouncedValue] = react.useState(value);
7
+ react.useEffect(() => {
8
+ const timer = setTimeout(() => {
9
+ setDebouncedValue(value);
10
+ }, delay);
11
+ return () => {
12
+ clearTimeout(timer);
13
+ };
14
+ }, [value, delay]);
15
+ return debouncedValue;
16
+ }
17
+
18
+ exports.useDebounce = useDebounce;
package/dist/cjs/index.js CHANGED
@@ -14,6 +14,7 @@ var radio = require('./components/forms/radio/radio.js');
14
14
  var select = require('./components/forms/select/select.js');
15
15
  var _switch = require('./components/forms/switch/switch.js');
16
16
  var textarea = require('./components/forms/textarea/textarea.js');
17
+ var multiselect = require('./components/selectors/multiselect/multiselect.js');
17
18
  var icon = require('./components/media/icon/icon.js');
18
19
  var avatar = require('./components/media/avatar/avatar.js');
19
20
  var aspectRatio = require('./components/media/aspect-ratio/aspect-ratio.js');
@@ -50,6 +51,7 @@ var useControlled = require('./hooks/useControlled.js');
50
51
  var usePrevious = require('./hooks/usePrevious.js');
51
52
  var useLatest = require('./hooks/useLatest.js');
52
53
  var useScrollState = require('./hooks/useScrollState.js');
54
+ var useDebounce = require('./hooks/use-debounce.js');
53
55
  var chain = require('./utils/chain.js');
54
56
  var mergeProps = require('./utils/mergeProps.js');
55
57
  var mergeRefs = require('./utils/mergeRefs.js');
@@ -93,6 +95,7 @@ exports.SelectTrigger = select.SelectTrigger;
93
95
  exports.SelectValue = select.SelectValue;
94
96
  exports.Switch = _switch.Switch;
95
97
  exports.Textarea = textarea.Textarea;
98
+ exports.MultiSelect = multiselect.MultiSelect;
96
99
  exports.Icon = icon.Icon;
97
100
  exports.Avatar = avatar.Avatar;
98
101
  exports.AvatarFallback = avatar.AvatarFallback;
@@ -150,6 +153,7 @@ exports.CommandGroup = command.CommandGroup;
150
153
  exports.CommandInput = command.CommandInput;
151
154
  exports.CommandItem = command.CommandItem;
152
155
  exports.CommandList = command.CommandList;
156
+ exports.CommandLoading = command.CommandLoading;
153
157
  exports.CommandSeparator = command.CommandSeparator;
154
158
  exports.CommandShortcut = command.CommandShortcut;
155
159
  exports.DataTable = dataTable.DataTable;
@@ -220,6 +224,7 @@ exports.useControlled = useControlled.useControlled;
220
224
  exports.usePrevious = usePrevious.usePrevious;
221
225
  exports.useLatest = useLatest.useLatest;
222
226
  exports.useScrollState = useScrollState.useScrollState;
227
+ exports.useDebounce = useDebounce.useDebounce;
223
228
  exports.chain = chain.chain;
224
229
  exports.mergeProps = mergeProps.mergeProps;
225
230
  exports.assignRef = mergeRefs.assignRef;
@@ -15,7 +15,18 @@ const popoverContentVariants = cva([
15
15
  sm: "w-56",
16
16
  lg: "w-96",
17
17
  },
18
+ matchTriggerWidth: {
19
+ true: null,
20
+ false: null,
21
+ },
18
22
  },
23
+ compoundVariants: [
24
+ {
25
+ size: ["default", "sm", "lg"],
26
+ matchTriggerWidth: true,
27
+ class: "w-(--radix-popover-trigger-width)",
28
+ },
29
+ ],
19
30
  defaultVariants: {
20
31
  size: "default",
21
32
  },
@@ -35,8 +46,8 @@ const PopoverTrigger = (_a) => {
35
46
  return jsx(Trigger, Object.assign({ "data-slot": "popover-trigger" }, props));
36
47
  };
37
48
  const PopoverContent = (_a) => {
38
- var { className, align = "center", arrow = false, withClose = false, sideOffset = 4, size, children } = _a, props = __rest(_a, ["className", "align", "arrow", "withClose", "sideOffset", "size", "children"]);
39
- return (jsx(Portal, { children: jsxs(Content, Object.assign({ "data-slot": "popover-content", align: align, sideOffset: sideOffset, className: cn(popoverContentVariants({ size }), [
49
+ var { className, align = "center", arrow = false, withClose = false, sideOffset = 4, size, matchTriggerWidth = false, children } = _a, props = __rest(_a, ["className", "align", "arrow", "withClose", "sideOffset", "size", "matchTriggerWidth", "children"]);
50
+ return (jsx(Portal, { children: jsxs(Content, Object.assign({ "data-slot": "popover-content", align: align, sideOffset: sideOffset, className: cn(popoverContentVariants({ size, matchTriggerWidth }), [
40
51
  "data-[state=open]:animate-in",
41
52
  "data-[state=open]:fade-in-0",
42
53
  "data-[state=open]:zoom-in-95",
@@ -0,0 +1,49 @@
1
+ "use client";
2
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
+ import { memo } from 'react';
4
+ import { Badge } from '../../../ui/badge/badge.js';
5
+ import { Icon } from '../../../media/icon/icon.js';
6
+ import { cn } from '../../../../lib/utils.js';
7
+
8
+ const BadgeList = memo(({ options, disabled, onRemove }) => {
9
+ const handleBadgeKeyDown = (e, opt) => {
10
+ e.stopPropagation();
11
+ if (disabled) {
12
+ return;
13
+ }
14
+ if (e.key === "Enter" || e.key === " ") {
15
+ e.preventDefault();
16
+ onRemove(opt);
17
+ }
18
+ else if (e.key === "Backspace" || e.key === "Delete") {
19
+ e.preventDefault();
20
+ onRemove(opt);
21
+ const nextBadge = e.currentTarget.nextElementSibling || e.currentTarget.previousElementSibling;
22
+ if (nextBadge instanceof HTMLElement) {
23
+ nextBadge.focus();
24
+ }
25
+ }
26
+ else if (e.key === "ArrowRight") {
27
+ e.preventDefault();
28
+ const nextBadge = e.currentTarget.nextElementSibling;
29
+ if (nextBadge instanceof HTMLElement) {
30
+ nextBadge.focus();
31
+ }
32
+ }
33
+ else if (e.key === "ArrowLeft") {
34
+ e.preventDefault();
35
+ const prevBadge = e.currentTarget.previousElementSibling;
36
+ if (prevBadge instanceof HTMLElement) {
37
+ prevBadge.focus();
38
+ }
39
+ }
40
+ };
41
+ return (jsx(Fragment, { children: options.map((opt) => (jsxs(Badge, { tabIndex: disabled ? -1 : 0, role: "button", "aria-label": `Remove ${opt.label}`, variant: "secondary", className: cn("gap-1 select-none", !disabled && "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", !disabled && "cursor-pointer hover:bg-secondary/80"), onClick: () => {
42
+ if (!disabled) {
43
+ onRemove(opt);
44
+ }
45
+ }, onKeyDown: (e) => handleBadgeKeyDown(e, opt), children: [opt.label, !disabled && jsx(Icon, { name: "x", className: "size-3" })] }, opt.value))) }));
46
+ });
47
+ BadgeList.displayName = "BadgeList";
48
+
49
+ export { BadgeList };
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+ import { memo } from 'react';
4
+ import { CommandItem, CommandGroup } from '../../../ui/command/command.js';
5
+ import { Checkbox } from '../../../forms/checkbox/checkbox.js';
6
+
7
+ const OptionItem = memo(({ option, selected, onSelect }) => (jsxs(CommandItem, { value: option.value, keywords: [option.label], onSelect: () => onSelect(option), className: "flex items-center gap-2", children: [jsx(Checkbox, { tabIndex: -1, checked: selected, size: "sm" }), option.label] })));
8
+ OptionItem.displayName = "OptionItem";
9
+ const OptionsList = memo(({ options, selectedValues, onSelect }) => (jsx(Fragment, { children: options.map(({ value, label, children, data }) => children.length > 0 ? (jsx(CommandGroup, { heading: label, children: jsx(OptionsList, { options: children, selectedValues: selectedValues, onSelect: onSelect }) }, value)) : data ? (jsx(OptionItem, { option: data, selected: selectedValues.has(value), onSelect: onSelect }, value)) : null) })));
10
+ OptionsList.displayName = "OptionsList";
11
+
12
+ export { OptionsList };
@@ -0,0 +1,117 @@
1
+ "use client";
2
+ import { __rest, __awaiter } from 'tslib';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import { useState, useCallback, useMemo } from 'react';
5
+ import { OptionsList } from './components/options.js';
6
+ import { BadgeList } from './components/badge-list.js';
7
+ import { Command, CommandInput, CommandList, CommandLoading, CommandEmpty, CommandItem } from '../../ui/command/command.js';
8
+ import { Popover, PopoverTrigger, PopoverContent } from '../../popovers/popover/popover.js';
9
+ import { cn } from '../../../lib/utils.js';
10
+ import { useDebounce } from '../../../hooks/use-debounce.js';
11
+ import 'clsx';
12
+ import { toggle } from '../../../utils/toggle.js';
13
+ import { useUpdateEffect } from '../../../hooks/useUpdateEffect.js';
14
+ import 'lodash/throttle';
15
+ import { IconButton } from '../../buttons/icon-button/icon-button.js';
16
+ import { Flex } from '../../layout/flex/flex.js';
17
+
18
+ function isPromise(value) {
19
+ return value instanceof Promise;
20
+ }
21
+ function getGroupedListOptions(options, groups = []) {
22
+ const lookup = groups.reduce((acc, { value, label }) => {
23
+ acc[value] = { value, label, children: [] };
24
+ return acc;
25
+ }, {});
26
+ const ungrouped = [];
27
+ for (const opt of options) {
28
+ const node = {
29
+ value: opt.value,
30
+ label: opt.label,
31
+ children: [],
32
+ data: opt,
33
+ };
34
+ if (opt.group && lookup[opt.group]) {
35
+ lookup[opt.group].children.push(node);
36
+ }
37
+ else {
38
+ ungrouped.push(node);
39
+ }
40
+ }
41
+ const result = [];
42
+ for (const grp of groups) {
43
+ const bucket = lookup[grp.value];
44
+ if (bucket.children.length > 0) {
45
+ result.push(bucket);
46
+ }
47
+ }
48
+ return result.concat(ungrouped);
49
+ }
50
+ const defaultValue = [];
51
+ const defaultOptions = [];
52
+ const defaultGroups = [];
53
+ const MultiSelect = (_a) => {
54
+ var { value = defaultValue, onChange = () => { }, options = defaultOptions, groups = defaultGroups, createable = false, onCreate = () => { }, onSearch, debounceDelay = 500, placeholder = "Select options...", disabled = false, clearable = false, className } = _a, props = __rest(_a, ["value", "onChange", "options", "groups", "createable", "onCreate", "onSearch", "debounceDelay", "placeholder", "disabled", "clearable", "className"]);
55
+ const [open, setOpen] = useState(false);
56
+ const [loading, setLoading] = useState(false);
57
+ const [searchQuery, setSearchQuery] = useState("");
58
+ const debouncedSearch = useDebounce(searchQuery, debounceDelay);
59
+ const [searchResults, setSearchResults] = useState(options);
60
+ useUpdateEffect(() => {
61
+ if (!onSearch || !debouncedSearch) {
62
+ return;
63
+ }
64
+ const search = () => __awaiter(void 0, void 0, void 0, function* () {
65
+ try {
66
+ const results = onSearch(debouncedSearch);
67
+ if (isPromise(results)) {
68
+ setLoading(true);
69
+ const options = yield results;
70
+ setSearchResults(options);
71
+ }
72
+ else if (Array.isArray(results)) {
73
+ setSearchResults(results);
74
+ }
75
+ }
76
+ catch (error) {
77
+ console.error("Search failed:", error);
78
+ setSearchResults([]);
79
+ }
80
+ finally {
81
+ setLoading(false);
82
+ }
83
+ });
84
+ void search();
85
+ }, [debouncedSearch, onSearch]);
86
+ const handleSelect = useCallback((opt) => {
87
+ onChange(toggle(value, opt, (o) => o.value));
88
+ }, [onChange, value]);
89
+ const handleRemove = useCallback((opt) => onChange(value.filter((v) => v.value !== opt.value)), [onChange, value]);
90
+ const handleClear = useCallback(() => onChange([]), [onChange]);
91
+ const optionsList = useMemo(() => getGroupedListOptions(searchResults, groups), [searchResults, groups]);
92
+ const selectedValues = useMemo(() => new Set(value.map((v) => v.value)), [value]);
93
+ const handleTriggerKeyDown = useCallback((e) => {
94
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
95
+ e.preventDefault();
96
+ setOpen(true);
97
+ }
98
+ }, []);
99
+ const handleClearKeyDown = useCallback((e) => {
100
+ if (e.key === "Enter" || e.key === " ") {
101
+ e.preventDefault();
102
+ handleClear();
103
+ }
104
+ }, [handleClear]);
105
+ const setPopoverVisibility = useCallback((open) => {
106
+ if (!disabled) {
107
+ setOpen(open);
108
+ }
109
+ }, [disabled]);
110
+ return (jsxs(Popover, { open: open, onOpenChange: setPopoverVisibility, children: [jsx(PopoverTrigger, { asChild: true, children: jsxs("div", Object.assign({}, props, { tabIndex: disabled ? -1 : 0, role: "combobox", "aria-expanded": open, "aria-haspopup": "listbox", "aria-controls": "multiselect-options", "aria-label": placeholder, "aria-disabled": disabled, onKeyDown: handleTriggerKeyDown, className: cn("flex min-h-10 w-full flex-wrap items-center gap-1 rounded-md border bg-background px-3 py-2", !disabled && "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", disabled && "disabled:cursor-not-allowed disabled:opacity-50", className), children: [jsx(Flex, { wrap: "wrap", gap: 1, align: "start", className: "flex-1", children: value.length > 0 ? (jsx(BadgeList, { options: value, disabled: disabled, onRemove: handleRemove })) : (jsx("span", { className: "text-muted-foreground", children: placeholder })) }), clearable && !disabled && value.length > 0 && (jsx(IconButton, { icon: "circle-x", radius: "full", variant: "ghost", className: "size-5 p-0.25 self-start shrink-0", onClick: handleClear, onKeyDown: handleClearKeyDown, disabled: disabled, tooltip: "Clear All", "aria-label": "Clear all selections" }))] })) }), jsx(PopoverContent, { className: "p-0", align: "start", matchTriggerWidth: true, children: jsxs(Command, { id: "multiselect-options", role: "listbox", shouldFilter: !onSearch, children: [jsx(CommandInput, { placeholder: "Search...", value: searchQuery, onValueChange: setSearchQuery, disabled: disabled }), jsxs(CommandList, { children: [loading ? (jsx(CommandLoading, { children: "Searching\u2026" })) : optionsList.length > 0 ? (jsx(OptionsList, { options: optionsList, selectedValues: selectedValues, onSelect: handleSelect })) : (jsx(CommandEmpty, { children: "No results found." })), createable && !loading && searchQuery && debouncedSearch && (jsxs(CommandItem, { value: searchQuery, keywords: [searchQuery], onSelect: () => {
111
+ onCreate(searchQuery);
112
+ setSearchQuery("");
113
+ }, children: ["Create \"", searchQuery, "\""] }))] })] }) })] }));
114
+ };
115
+ MultiSelect.displayName = "MultiSelect";
116
+
117
+ export { MultiSelect };
@@ -41,6 +41,11 @@ const CommandEmpty = (_a) => {
41
41
  return jsx(Command$1.Empty, Object.assign({ "data-slot": "command-empty", className: "py-6 text-center text-sm" }, props));
42
42
  };
43
43
  CommandEmpty.displayName = "CommandEmpty";
44
+ const CommandLoading = (_a) => {
45
+ var props = __rest(_a, []);
46
+ return jsx(Command$1.Loading, Object.assign({ "data-slot": "command-loading", className: "py-6 text-center text-sm" }, props));
47
+ };
48
+ CommandEmpty.displayName = "CommandEmpty";
44
49
  const CommandGroup = (_a) => {
45
50
  var { className } = _a, props = __rest(_a, ["className"]);
46
51
  return (jsx(Command$1.Group, Object.assign({ "data-slot": "command-group", className: cn("overflow-hidden p-1", "text-foreground",
@@ -70,4 +75,4 @@ const CommandShortcut = (_a) => {
70
75
  };
71
76
  CommandShortcut.displayName = "CommandShortcut";
72
77
 
73
- export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut };
78
+ export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandLoading, CommandSeparator, CommandShortcut };
@@ -0,0 +1,16 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ function useDebounce(value, delay) {
4
+ const [debouncedValue, setDebouncedValue] = useState(value);
5
+ useEffect(() => {
6
+ const timer = setTimeout(() => {
7
+ setDebouncedValue(value);
8
+ }, delay);
9
+ return () => {
10
+ clearTimeout(timer);
11
+ };
12
+ }, [value, delay]);
13
+ return debouncedValue;
14
+ }
15
+
16
+ export { useDebounce };
package/dist/esm/index.js CHANGED
@@ -12,6 +12,7 @@ export { RadioGroup, RadioGroupItem } from './components/forms/radio/radio.js';
12
12
  export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from './components/forms/select/select.js';
13
13
  export { Switch } from './components/forms/switch/switch.js';
14
14
  export { Textarea } from './components/forms/textarea/textarea.js';
15
+ export { MultiSelect } from './components/selectors/multiselect/multiselect.js';
15
16
  export { Icon } from './components/media/icon/icon.js';
16
17
  export { Avatar, AvatarFallback, AvatarImage } from './components/media/avatar/avatar.js';
17
18
  export { AspectRatio } from './components/media/aspect-ratio/aspect-ratio.js';
@@ -26,7 +27,7 @@ export { Text } from './components/typography/text/text.js';
26
27
  export { Alert, AlertDescription, AlertTitle } from './components/ui/alert/alert.js';
27
28
  export { Badge, badgeVariants } from './components/ui/badge/badge.js';
28
29
  export { Calendar } from './components/ui/calendar/calendar.js';
29
- export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from './components/ui/command/command.js';
30
+ export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandLoading, CommandSeparator, CommandShortcut } from './components/ui/command/command.js';
30
31
  export { DataTable } from './components/ui/data-table/data-table.js';
31
32
  export { DatePicker } from './components/ui/date-picker/date-picker.js';
32
33
  export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from './components/ui/dropdown-menu/dropdown-menu.js';
@@ -48,6 +49,7 @@ export { useControlled } from './hooks/useControlled.js';
48
49
  export { usePrevious } from './hooks/usePrevious.js';
49
50
  export { useLatest } from './hooks/useLatest.js';
50
51
  export { useScrollState } from './hooks/useScrollState.js';
52
+ export { useDebounce } from './hooks/use-debounce.js';
51
53
  export { chain } from './utils/chain.js';
52
54
  export { mergeProps } from './utils/mergeProps.js';
53
55
  export { assignRef, mergeRefs } from './utils/mergeRefs.js';