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.
- package/dist/cjs/components/popovers/popover/popover.js +13 -2
- package/dist/cjs/components/selectors/multiselect/components/badge-list.js +51 -0
- package/dist/cjs/components/selectors/multiselect/components/options.js +14 -0
- package/dist/cjs/components/selectors/multiselect/multiselect.js +119 -0
- package/dist/cjs/components/ui/command/command.js +6 -0
- package/dist/cjs/hooks/use-debounce.js +18 -0
- package/dist/cjs/index.js +5 -0
- package/dist/esm/components/popovers/popover/popover.js +13 -2
- package/dist/esm/components/selectors/multiselect/components/badge-list.js +49 -0
- package/dist/esm/components/selectors/multiselect/components/options.js +12 -0
- package/dist/esm/components/selectors/multiselect/multiselect.js +117 -0
- package/dist/esm/components/ui/command/command.js +6 -1
- package/dist/esm/hooks/use-debounce.js +16 -0
- package/dist/esm/index.js +3 -1
- package/dist/styles.css +9507 -1
- package/dist/types/index.d.ts +40 -9
- package/package.json +1 -1
- package/dist/cjs/components/ui/button/button.js +0 -42
- package/dist/cjs/components/ui/icon/icon.js +0 -27
- package/dist/esm/components/ui/button/button.js +0 -40
- package/dist/esm/components/ui/icon/icon.js +0 -25
|
@@ -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';
|