@underverse-ui/underverse 1.0.106 → 1.0.109
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/api-reference.json +1 -1
- package/dist/index.cjs +307 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +333 -116
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -7343,6 +7343,7 @@ var import_lucide_react14 = require("lucide-react");
|
|
|
7343
7343
|
// src/components/Combobox.tsx
|
|
7344
7344
|
var React24 = __toESM(require("react"), 1);
|
|
7345
7345
|
var import_react15 = require("react");
|
|
7346
|
+
var import_react_virtual = require("@tanstack/react-virtual");
|
|
7346
7347
|
var import_lucide_react13 = require("lucide-react");
|
|
7347
7348
|
var import_jsx_runtime29 = require("react/jsx-runtime");
|
|
7348
7349
|
var getOptionLabel = (option) => {
|
|
@@ -7388,9 +7389,19 @@ var Combobox = ({
|
|
|
7388
7389
|
groupBy,
|
|
7389
7390
|
renderOption,
|
|
7390
7391
|
renderValue,
|
|
7392
|
+
selectedOption: selectedOptionProp,
|
|
7391
7393
|
error,
|
|
7392
7394
|
helperText,
|
|
7393
|
-
useOverlayScrollbar = false
|
|
7395
|
+
useOverlayScrollbar = false,
|
|
7396
|
+
virtualized = false,
|
|
7397
|
+
estimatedItemHeight = 44,
|
|
7398
|
+
overscan = 8,
|
|
7399
|
+
searchMode = "auto",
|
|
7400
|
+
onSearchChange,
|
|
7401
|
+
searchDebounceMs = 0,
|
|
7402
|
+
minSearchLength = 0,
|
|
7403
|
+
maxInitialOptions,
|
|
7404
|
+
showSearchPromptWhenEmptyQuery = false
|
|
7394
7405
|
}) => {
|
|
7395
7406
|
const tv = useSmartTranslations("ValidationInput");
|
|
7396
7407
|
const [open, setOpen] = React24.useState(false);
|
|
@@ -7404,12 +7415,53 @@ var Combobox = ({
|
|
|
7404
7415
|
const autoId = (0, import_react15.useId)();
|
|
7405
7416
|
const resolvedId = id ? String(id) : `combobox-${autoId}`;
|
|
7406
7417
|
const labelId = label ? `${resolvedId}-label` : void 0;
|
|
7407
|
-
const enableSearch = options.length > 10;
|
|
7418
|
+
const enableSearch = options.length > 10 || searchMode === "manual" || minSearchLength > 0 || !!onSearchChange;
|
|
7419
|
+
const trimmedQuery = query.trim();
|
|
7420
|
+
const queryMeetsMinimum = trimmedQuery.length >= minSearchLength;
|
|
7421
|
+
const shouldPromptForSearch = minSearchLength > 0 && !queryMeetsMinimum && (searchMode === "manual" || showSearchPromptWhenEmptyQuery);
|
|
7408
7422
|
const filteredOptions = React24.useMemo(
|
|
7409
|
-
() =>
|
|
7410
|
-
|
|
7423
|
+
() => {
|
|
7424
|
+
if (shouldPromptForSearch) return [];
|
|
7425
|
+
if (!enableSearch || searchMode === "manual") return options;
|
|
7426
|
+
const normalizedQuery = trimmedQuery.toLowerCase();
|
|
7427
|
+
if (!normalizedQuery) return options;
|
|
7428
|
+
return options.filter((o) => getOptionLabel(o).toLowerCase().includes(normalizedQuery));
|
|
7429
|
+
},
|
|
7430
|
+
[enableSearch, options, searchMode, shouldPromptForSearch, trimmedQuery]
|
|
7431
|
+
);
|
|
7432
|
+
const renderLimitedOptions = React24.useMemo(
|
|
7433
|
+
() => {
|
|
7434
|
+
if (trimmedQuery || maxInitialOptions === void 0 || maxInitialOptions < 1) {
|
|
7435
|
+
return filteredOptions;
|
|
7436
|
+
}
|
|
7437
|
+
return filteredOptions.slice(0, maxInitialOptions);
|
|
7438
|
+
},
|
|
7439
|
+
[filteredOptions, maxInitialOptions, trimmedQuery]
|
|
7411
7440
|
);
|
|
7441
|
+
const canVirtualize = virtualized && !groupBy;
|
|
7442
|
+
const optionVirtualizer = (0, import_react_virtual.useVirtualizer)({
|
|
7443
|
+
count: canVirtualize ? renderLimitedOptions.length : 0,
|
|
7444
|
+
getScrollElement: () => optionsViewportRef.current,
|
|
7445
|
+
estimateSize: () => estimatedItemHeight,
|
|
7446
|
+
initialRect: { width: 0, height: maxHeight },
|
|
7447
|
+
overscan,
|
|
7448
|
+
enabled: canVirtualize
|
|
7449
|
+
});
|
|
7450
|
+
const virtualItems = canVirtualize ? optionVirtualizer.getVirtualItems() : [];
|
|
7412
7451
|
const triggerRef = React24.useRef(null);
|
|
7452
|
+
const scrollVirtualListToIndex = React24.useCallback((index) => {
|
|
7453
|
+
if (!canVirtualize || renderLimitedOptions.length === 0) return;
|
|
7454
|
+
optionVirtualizer.scrollToIndex(index, { align: "auto" });
|
|
7455
|
+
}, [canVirtualize, optionVirtualizer, renderLimitedOptions.length]);
|
|
7456
|
+
const scrollVirtualListToStart = React24.useCallback(() => {
|
|
7457
|
+
scrollVirtualListToIndex(0);
|
|
7458
|
+
}, [scrollVirtualListToIndex]);
|
|
7459
|
+
const moveActiveIndex = React24.useCallback((direction) => {
|
|
7460
|
+
if (renderLimitedOptions.length === 0) return;
|
|
7461
|
+
const next = activeIndex === null ? direction === 1 ? 0 : renderLimitedOptions.length - 1 : (activeIndex + direction + renderLimitedOptions.length) % renderLimitedOptions.length;
|
|
7462
|
+
setActiveIndex(next);
|
|
7463
|
+
scrollVirtualListToIndex(next);
|
|
7464
|
+
}, [activeIndex, renderLimitedOptions.length, scrollVirtualListToIndex]);
|
|
7413
7465
|
const handleSelect = (option) => {
|
|
7414
7466
|
if (getOptionDisabled(option)) return;
|
|
7415
7467
|
const val = getOptionValue(option);
|
|
@@ -7422,6 +7474,9 @@ var Combobox = ({
|
|
|
7422
7474
|
};
|
|
7423
7475
|
const handleClear = (e) => {
|
|
7424
7476
|
e.stopPropagation();
|
|
7477
|
+
clearValue();
|
|
7478
|
+
};
|
|
7479
|
+
const clearValue = () => {
|
|
7425
7480
|
onChange(null);
|
|
7426
7481
|
setOpen(false);
|
|
7427
7482
|
};
|
|
@@ -7429,13 +7484,26 @@ var Combobox = ({
|
|
|
7429
7484
|
if (!open) {
|
|
7430
7485
|
setQuery("");
|
|
7431
7486
|
setActiveIndex(null);
|
|
7487
|
+
scrollVirtualListToStart();
|
|
7432
7488
|
} else if (enableSearch) {
|
|
7433
7489
|
setTimeout(() => {
|
|
7434
7490
|
inputRef.current?.focus();
|
|
7435
7491
|
}, 100);
|
|
7436
7492
|
}
|
|
7437
|
-
}, [open,
|
|
7438
|
-
|
|
7493
|
+
}, [enableSearch, open, scrollVirtualListToStart]);
|
|
7494
|
+
React24.useEffect(() => {
|
|
7495
|
+
if (!onSearchChange) return void 0;
|
|
7496
|
+
const timeoutId = window.setTimeout(() => onSearchChange(query), searchDebounceMs);
|
|
7497
|
+
return () => window.clearTimeout(timeoutId);
|
|
7498
|
+
}, [onSearchChange, query, searchDebounceMs]);
|
|
7499
|
+
React24.useEffect(() => {
|
|
7500
|
+
if (process.env.NODE_ENV !== "production" && options.length > 300 && !virtualized && searchMode !== "manual" && maxInitialOptions === void 0) {
|
|
7501
|
+
console.warn(
|
|
7502
|
+
'[Underverse UI] Combobox received more than 300 options without virtualization, manual search, or maxInitialOptions. Use virtualized, searchMode="manual", or maxInitialOptions to avoid rendering a large dropdown.'
|
|
7503
|
+
);
|
|
7504
|
+
}
|
|
7505
|
+
}, [maxInitialOptions, options.length, searchMode, virtualized]);
|
|
7506
|
+
const selectedOption = findOptionByValue(options, value) ?? (selectedOptionProp && getOptionValue(selectedOptionProp) === value ? selectedOptionProp : void 0);
|
|
7439
7507
|
const displayValue = selectedOption ? getOptionLabel(selectedOption) : "";
|
|
7440
7508
|
const selectedIcon = selectedOption ? getOptionIcon(selectedOption) : void 0;
|
|
7441
7509
|
const hasValue = value !== void 0 && value !== null && value !== "";
|
|
@@ -7448,13 +7516,13 @@ var Combobox = ({
|
|
|
7448
7516
|
const groupedOptions = React24.useMemo(() => {
|
|
7449
7517
|
if (!groupBy) return null;
|
|
7450
7518
|
const groups = {};
|
|
7451
|
-
|
|
7519
|
+
renderLimitedOptions.forEach((opt) => {
|
|
7452
7520
|
const group = groupBy(opt);
|
|
7453
7521
|
if (!groups[group]) groups[group] = [];
|
|
7454
7522
|
groups[group].push(opt);
|
|
7455
7523
|
});
|
|
7456
7524
|
return groups;
|
|
7457
|
-
}, [
|
|
7525
|
+
}, [renderLimitedOptions, groupBy]);
|
|
7458
7526
|
const itemSizeStyles = {
|
|
7459
7527
|
sm: "px-2.5 py-1.5 text-xs gap-2",
|
|
7460
7528
|
md: "px-3 py-2.5 text-sm gap-3",
|
|
@@ -7470,60 +7538,75 @@ var Combobox = ({
|
|
|
7470
7538
|
md: "h-4 w-4",
|
|
7471
7539
|
lg: "h-5 w-5"
|
|
7472
7540
|
};
|
|
7473
|
-
const renderOptionItem = (item, index) => {
|
|
7541
|
+
const renderOptionItem = (item, index, virtualItem) => {
|
|
7474
7542
|
const itemValue = getOptionValue(item);
|
|
7475
7543
|
const itemLabel = getOptionLabel(item);
|
|
7476
7544
|
const itemIcon = getOptionIcon(item);
|
|
7477
7545
|
const itemDescription = getOptionDescription(item);
|
|
7478
7546
|
const itemDisabled = getOptionDisabled(item);
|
|
7479
7547
|
const isSelected = itemValue === value;
|
|
7480
|
-
return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
7481
|
-
"
|
|
7548
|
+
return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
7549
|
+
"li",
|
|
7482
7550
|
{
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
},
|
|
7493
|
-
|
|
7494
|
-
"
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7551
|
+
ref: virtualItem ? optionVirtualizer.measureElement : void 0,
|
|
7552
|
+
"data-index": virtualItem?.index,
|
|
7553
|
+
className: "list-none",
|
|
7554
|
+
style: virtualItem ? {
|
|
7555
|
+
position: "absolute",
|
|
7556
|
+
top: 0,
|
|
7557
|
+
left: 0,
|
|
7558
|
+
width: "100%",
|
|
7559
|
+
transform: `translateY(${virtualItem.start}px)`
|
|
7560
|
+
} : void 0,
|
|
7561
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
|
|
7562
|
+
"button",
|
|
7563
|
+
{
|
|
7564
|
+
id: `${resolvedId}-item-${index}`,
|
|
7565
|
+
type: "button",
|
|
7566
|
+
role: "option",
|
|
7567
|
+
tabIndex: -1,
|
|
7568
|
+
disabled: itemDisabled,
|
|
7569
|
+
"aria-selected": isSelected,
|
|
7570
|
+
onClick: () => handleSelect(item),
|
|
7571
|
+
style: {
|
|
7572
|
+
animationDelay: open ? `${Math.min(index * 15, 150)}ms` : "0ms"
|
|
7573
|
+
},
|
|
7574
|
+
className: cn(
|
|
7575
|
+
"dropdown-item group flex w-full items-center rounded-full text-left",
|
|
7576
|
+
itemSizeStyles[size],
|
|
7577
|
+
"outline-none focus:outline-none focus-visible:outline-none",
|
|
7578
|
+
"transition-all duration-150",
|
|
7579
|
+
!itemDisabled && "cursor-pointer hover:bg-accent/70 hover:shadow-sm",
|
|
7580
|
+
!itemDisabled && "focus:bg-accent/80 focus:text-accent-foreground",
|
|
7581
|
+
index === activeIndex && !itemDisabled && "bg-accent/60",
|
|
7582
|
+
isSelected && "bg-primary/10 text-primary font-medium",
|
|
7583
|
+
itemDisabled && "opacity-50 cursor-not-allowed"
|
|
7584
|
+
),
|
|
7585
|
+
children: [
|
|
7586
|
+
itemIcon && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
7587
|
+
"span",
|
|
7588
|
+
{
|
|
7589
|
+
className: cn("shrink-0 flex items-center justify-center", iconSizeStyles[size], isSelected ? "text-primary" : "text-muted-foreground"),
|
|
7590
|
+
children: itemIcon
|
|
7591
|
+
}
|
|
7592
|
+
),
|
|
7593
|
+
renderOption ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "flex-1 min-w-0", children: renderOption(item, isSelected) }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
7594
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "block truncate", children: itemLabel }),
|
|
7595
|
+
itemDescription && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: cn("block text-muted-foreground truncate mt-0.5", size === "sm" ? "text-[10px]" : "text-xs"), children: itemDescription })
|
|
7596
|
+
] }),
|
|
7597
|
+
isSelected && showSelectedIcon && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "shrink-0 ml-auto", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react13.Check, { className: cn(checkIconSizeStyles[size], "text-primary") }) })
|
|
7598
|
+
]
|
|
7599
|
+
}
|
|
7600
|
+
)
|
|
7601
|
+
},
|
|
7602
|
+
`${itemValue}-${index}`
|
|
7603
|
+
);
|
|
7520
7604
|
};
|
|
7521
7605
|
const dropdownBody = /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
|
|
7522
7606
|
"div",
|
|
7523
7607
|
{
|
|
7524
7608
|
"data-combobox-dropdown": true,
|
|
7525
7609
|
"data-state": open ? "open" : "closed",
|
|
7526
|
-
id: `${resolvedId}-listbox`,
|
|
7527
7610
|
className: "w-full rounded-2xl md:rounded-3xl overflow-hidden",
|
|
7528
7611
|
children: [
|
|
7529
7612
|
enableSearch && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: cn("relative border-b border-border/30", size === "sm" ? "p-2" : size === "lg" ? "p-3" : "p-2.5"), children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "relative", children: [
|
|
@@ -7544,24 +7627,19 @@ var Combobox = ({
|
|
|
7544
7627
|
onChange: (e) => {
|
|
7545
7628
|
setQuery(e.target.value);
|
|
7546
7629
|
setActiveIndex(null);
|
|
7630
|
+
scrollVirtualListToStart();
|
|
7547
7631
|
},
|
|
7548
7632
|
onKeyDown: (e) => {
|
|
7549
7633
|
if (e.key === "ArrowDown") {
|
|
7550
7634
|
e.preventDefault();
|
|
7551
|
-
|
|
7552
|
-
const next = prev === null ? 0 : prev + 1;
|
|
7553
|
-
return next >= filteredOptions.length ? 0 : next;
|
|
7554
|
-
});
|
|
7635
|
+
moveActiveIndex(1);
|
|
7555
7636
|
} else if (e.key === "ArrowUp") {
|
|
7556
7637
|
e.preventDefault();
|
|
7557
|
-
|
|
7558
|
-
const next = prev === null ? filteredOptions.length - 1 : prev - 1;
|
|
7559
|
-
return next < 0 ? filteredOptions.length - 1 : next;
|
|
7560
|
-
});
|
|
7638
|
+
moveActiveIndex(-1);
|
|
7561
7639
|
} else if (e.key === "Enter") {
|
|
7562
7640
|
e.preventDefault();
|
|
7563
|
-
if (activeIndex !== null &&
|
|
7564
|
-
handleSelect(
|
|
7641
|
+
if (activeIndex !== null && renderLimitedOptions[activeIndex] && !getOptionDisabled(renderLimitedOptions[activeIndex])) {
|
|
7642
|
+
handleSelect(renderLimitedOptions[activeIndex]);
|
|
7565
7643
|
}
|
|
7566
7644
|
} else if (e.key === "Escape") {
|
|
7567
7645
|
e.preventDefault();
|
|
@@ -7584,7 +7662,10 @@ var Combobox = ({
|
|
|
7584
7662
|
"button",
|
|
7585
7663
|
{
|
|
7586
7664
|
type: "button",
|
|
7587
|
-
onClick: () =>
|
|
7665
|
+
onClick: () => {
|
|
7666
|
+
setQuery("");
|
|
7667
|
+
scrollVirtualListToStart();
|
|
7668
|
+
},
|
|
7588
7669
|
className: "absolute right-3 top-1/2 -translate-y-1/2 p-0.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors",
|
|
7589
7670
|
children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react13.X, { className: cn(size === "sm" ? "h-3 w-3" : size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5") })
|
|
7590
7671
|
}
|
|
@@ -7594,6 +7675,7 @@ var Combobox = ({
|
|
|
7594
7675
|
"div",
|
|
7595
7676
|
{
|
|
7596
7677
|
ref: optionsViewportRef,
|
|
7678
|
+
id: `${resolvedId}-listbox`,
|
|
7597
7679
|
role: "listbox",
|
|
7598
7680
|
"aria-labelledby": labelId,
|
|
7599
7681
|
className: "overflow-y-auto overscroll-contain",
|
|
@@ -7601,7 +7683,14 @@ var Combobox = ({
|
|
|
7601
7683
|
children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: cn(size === "sm" ? "p-1" : size === "lg" ? "p-2" : "p-1.5"), children: loading2 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "px-3 py-10 text-center", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col items-center gap-3 animate-in fade-in-0 zoom-in-95 duration-300", children: [
|
|
7602
7684
|
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "relative", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "w-10 h-10 rounded-full border-2 border-primary/20 border-t-primary animate-spin" }) }),
|
|
7603
7685
|
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "text-sm text-muted-foreground", children: loadingText })
|
|
7604
|
-
] }) }) :
|
|
7686
|
+
] }) }) : shouldPromptForSearch ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "px-3 py-10 text-center", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col items-center gap-3 animate-in fade-in-0 zoom-in-95 duration-300", children: [
|
|
7687
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "w-12 h-12 rounded-full bg-muted/50 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react13.Search, { className: "h-6 w-6 text-muted-foreground/60" }) }),
|
|
7688
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "space-y-1", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "block text-sm font-medium text-foreground", children: [
|
|
7689
|
+
"Type at least ",
|
|
7690
|
+
minSearchLength,
|
|
7691
|
+
" characters to search"
|
|
7692
|
+
] }) })
|
|
7693
|
+
] }) }) : renderLimitedOptions.length > 0 ? groupedOptions ? (
|
|
7605
7694
|
// Render grouped options with global index tracking
|
|
7606
7695
|
(() => {
|
|
7607
7696
|
let globalIndex = 0;
|
|
@@ -7615,7 +7704,14 @@ var Combobox = ({
|
|
|
7615
7704
|
})()
|
|
7616
7705
|
) : (
|
|
7617
7706
|
// Render flat options
|
|
7618
|
-
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
7707
|
+
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
|
|
7708
|
+
"ul",
|
|
7709
|
+
{
|
|
7710
|
+
className: "space-y-0.5",
|
|
7711
|
+
style: canVirtualize ? { height: `${optionVirtualizer.getTotalSize()}px`, position: "relative" } : void 0,
|
|
7712
|
+
children: canVirtualize ? virtualItems.map((virtualItem) => renderOptionItem(renderLimitedOptions[virtualItem.index], virtualItem.index, virtualItem)) : renderLimitedOptions.map((item, index) => renderOptionItem(item, index))
|
|
7713
|
+
}
|
|
7714
|
+
)
|
|
7619
7715
|
) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "px-3 py-10 text-center", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col items-center gap-3 animate-in fade-in-0 zoom-in-95 duration-300", children: [
|
|
7620
7716
|
/* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "w-12 h-12 rounded-full bg-muted/50 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react13.SearchX, { className: "h-6 w-6 text-muted-foreground/60" }) }),
|
|
7621
7717
|
/* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "space-y-1", children: [
|
|
@@ -7626,7 +7722,10 @@ var Combobox = ({
|
|
|
7626
7722
|
"button",
|
|
7627
7723
|
{
|
|
7628
7724
|
type: "button",
|
|
7629
|
-
onClick: () =>
|
|
7725
|
+
onClick: () => {
|
|
7726
|
+
setQuery("");
|
|
7727
|
+
scrollVirtualListToStart();
|
|
7728
|
+
},
|
|
7630
7729
|
className: "px-3 py-1.5 text-xs font-medium text-primary bg-primary/10 rounded-full hover:bg-primary/20 transition-colors",
|
|
7631
7730
|
children: "Clear search"
|
|
7632
7731
|
}
|
|
@@ -7696,7 +7795,13 @@ var Combobox = ({
|
|
|
7696
7795
|
tabIndex: 0,
|
|
7697
7796
|
"aria-label": "Clear selection",
|
|
7698
7797
|
onClick: handleClear,
|
|
7699
|
-
onKeyDown: (e) =>
|
|
7798
|
+
onKeyDown: (e) => {
|
|
7799
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
7800
|
+
e.preventDefault();
|
|
7801
|
+
e.stopPropagation();
|
|
7802
|
+
clearValue();
|
|
7803
|
+
}
|
|
7804
|
+
},
|
|
7700
7805
|
className: cn(
|
|
7701
7806
|
"opacity-0 group-hover:opacity-100 transition-all duration-200",
|
|
7702
7807
|
"p-1 rounded-lg hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
|
@@ -15576,6 +15681,7 @@ function CalendarTimeline({
|
|
|
15576
15681
|
// src/components/MultiCombobox.tsx
|
|
15577
15682
|
var React39 = __toESM(require("react"), 1);
|
|
15578
15683
|
var import_react20 = require("react");
|
|
15684
|
+
var import_react_virtual2 = require("@tanstack/react-virtual");
|
|
15579
15685
|
var import_lucide_react24 = require("lucide-react");
|
|
15580
15686
|
var import_jsx_runtime45 = require("react/jsx-runtime");
|
|
15581
15687
|
var MultiCombobox = ({
|
|
@@ -15606,10 +15712,20 @@ var MultiCombobox = ({
|
|
|
15606
15712
|
groupBy,
|
|
15607
15713
|
renderOption,
|
|
15608
15714
|
renderTag,
|
|
15715
|
+
selectedOptions: selectedOptionsProp,
|
|
15609
15716
|
error,
|
|
15610
15717
|
helperText,
|
|
15611
15718
|
maxTagsVisible = 3,
|
|
15612
|
-
useOverlayScrollbar = false
|
|
15719
|
+
useOverlayScrollbar = false,
|
|
15720
|
+
virtualized = false,
|
|
15721
|
+
estimatedItemHeight = 44,
|
|
15722
|
+
overscan = 8,
|
|
15723
|
+
searchMode = "auto",
|
|
15724
|
+
onSearchChange,
|
|
15725
|
+
searchDebounceMs = 0,
|
|
15726
|
+
minSearchLength = 0,
|
|
15727
|
+
maxInitialOptions,
|
|
15728
|
+
showSearchPromptWhenEmptyQuery = false
|
|
15613
15729
|
}) => {
|
|
15614
15730
|
const tv = useSmartTranslations("ValidationInput");
|
|
15615
15731
|
const [query, setQuery] = React39.useState("");
|
|
@@ -15628,23 +15744,52 @@ var MultiCombobox = ({
|
|
|
15628
15744
|
),
|
|
15629
15745
|
[options]
|
|
15630
15746
|
);
|
|
15631
|
-
const enableSearch = normalizedOptions.length > 10;
|
|
15632
|
-
const
|
|
15633
|
-
|
|
15634
|
-
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15747
|
+
const enableSearch = normalizedOptions.length > 10 || searchMode === "manual" || minSearchLength > 0 || !!onSearchChange;
|
|
15748
|
+
const trimmedQuery = query.trim();
|
|
15749
|
+
const queryMeetsMinimum = trimmedQuery.length >= minSearchLength;
|
|
15750
|
+
const shouldPromptForSearch = minSearchLength > 0 && !queryMeetsMinimum && (searchMode === "manual" || showSearchPromptWhenEmptyQuery);
|
|
15751
|
+
const filtered = React39.useMemo(() => {
|
|
15752
|
+
if (shouldPromptForSearch) return [];
|
|
15753
|
+
if (!enableSearch || searchMode === "manual") return normalizedOptions;
|
|
15754
|
+
const normalizedQuery = trimmedQuery.toLowerCase();
|
|
15755
|
+
if (!normalizedQuery) return normalizedOptions;
|
|
15756
|
+
return normalizedOptions.filter(
|
|
15757
|
+
(opt) => opt.label.toLowerCase().includes(normalizedQuery) || opt.description?.toLowerCase().includes(normalizedQuery)
|
|
15758
|
+
);
|
|
15759
|
+
}, [enableSearch, normalizedOptions, searchMode, shouldPromptForSearch, trimmedQuery]);
|
|
15760
|
+
const renderLimitedOptions = React39.useMemo(() => {
|
|
15761
|
+
if (trimmedQuery || maxInitialOptions === void 0 || maxInitialOptions < 1) {
|
|
15762
|
+
return filtered;
|
|
15763
|
+
}
|
|
15764
|
+
return filtered.slice(0, maxInitialOptions);
|
|
15765
|
+
}, [filtered, maxInitialOptions, trimmedQuery]);
|
|
15766
|
+
const canVirtualize = virtualized && !groupBy;
|
|
15767
|
+
const optionVirtualizer = (0, import_react_virtual2.useVirtualizer)({
|
|
15768
|
+
count: canVirtualize ? renderLimitedOptions.length : 0,
|
|
15769
|
+
getScrollElement: () => optionsListRef.current,
|
|
15770
|
+
estimateSize: () => estimatedItemHeight,
|
|
15771
|
+
initialRect: { width: 0, height: maxHeight },
|
|
15772
|
+
overscan,
|
|
15773
|
+
enabled: canVirtualize
|
|
15774
|
+
});
|
|
15775
|
+
const virtualItems = canVirtualize ? optionVirtualizer.getVirtualItems() : [];
|
|
15776
|
+
const scrollVirtualListToIndex = React39.useCallback((index) => {
|
|
15777
|
+
if (!canVirtualize || renderLimitedOptions.length === 0) return;
|
|
15778
|
+
optionVirtualizer.scrollToIndex(index, { align: "auto" });
|
|
15779
|
+
}, [canVirtualize, optionVirtualizer, renderLimitedOptions.length]);
|
|
15780
|
+
const scrollVirtualListToStart = React39.useCallback(() => {
|
|
15781
|
+
scrollVirtualListToIndex(0);
|
|
15782
|
+
}, [scrollVirtualListToIndex]);
|
|
15638
15783
|
const groupedOptions = React39.useMemo(() => {
|
|
15639
15784
|
if (!groupBy) return null;
|
|
15640
15785
|
const groups = /* @__PURE__ */ new Map();
|
|
15641
|
-
|
|
15786
|
+
renderLimitedOptions.forEach((opt) => {
|
|
15642
15787
|
const group = groupBy(opt);
|
|
15643
15788
|
if (!groups.has(group)) groups.set(group, []);
|
|
15644
15789
|
groups.get(group).push(opt);
|
|
15645
15790
|
});
|
|
15646
15791
|
return groups;
|
|
15647
|
-
}, [
|
|
15792
|
+
}, [renderLimitedOptions, groupBy]);
|
|
15648
15793
|
const toggleSelect = (optionValue) => {
|
|
15649
15794
|
const option = normalizedOptions.find((o) => o.value === optionValue);
|
|
15650
15795
|
if (option?.disabled || disabledOptions.includes(optionValue)) return;
|
|
@@ -15662,11 +15807,26 @@ var MultiCombobox = ({
|
|
|
15662
15807
|
};
|
|
15663
15808
|
const handleKeyDown2 = (e) => {
|
|
15664
15809
|
if (!open) setOpen(true);
|
|
15665
|
-
if (e.key === "
|
|
15810
|
+
if (e.key === "ArrowDown") {
|
|
15666
15811
|
e.preventDefault();
|
|
15667
|
-
if (
|
|
15668
|
-
|
|
15812
|
+
if (renderLimitedOptions.length === 0) return;
|
|
15813
|
+
const next = activeIndex === null ? 0 : (activeIndex + 1) % renderLimitedOptions.length;
|
|
15814
|
+
setActiveIndex(next);
|
|
15815
|
+
scrollVirtualListToIndex(next);
|
|
15816
|
+
} else if (e.key === "ArrowUp") {
|
|
15817
|
+
e.preventDefault();
|
|
15818
|
+
if (renderLimitedOptions.length === 0) return;
|
|
15819
|
+
const next = activeIndex === null ? renderLimitedOptions.length - 1 : (activeIndex - 1 + renderLimitedOptions.length) % renderLimitedOptions.length;
|
|
15820
|
+
setActiveIndex(next);
|
|
15821
|
+
scrollVirtualListToIndex(next);
|
|
15822
|
+
} else if (e.key === "Enter") {
|
|
15823
|
+
e.preventDefault();
|
|
15824
|
+
if (activeIndex !== null && renderLimitedOptions[activeIndex]) {
|
|
15825
|
+
toggleSelect(renderLimitedOptions[activeIndex].value);
|
|
15669
15826
|
}
|
|
15827
|
+
} else if (e.key === "Escape") {
|
|
15828
|
+
e.preventDefault();
|
|
15829
|
+
setOpen(false);
|
|
15670
15830
|
}
|
|
15671
15831
|
};
|
|
15672
15832
|
const handleClearAll = () => {
|
|
@@ -15683,8 +15843,24 @@ var MultiCombobox = ({
|
|
|
15683
15843
|
setTimeout(() => {
|
|
15684
15844
|
inputRef.current?.focus();
|
|
15685
15845
|
}, 100);
|
|
15846
|
+
} else if (!open) {
|
|
15847
|
+
setQuery("");
|
|
15848
|
+
setActiveIndex(null);
|
|
15849
|
+
scrollVirtualListToStart();
|
|
15686
15850
|
}
|
|
15687
|
-
}, [open,
|
|
15851
|
+
}, [enableSearch, open, scrollVirtualListToStart]);
|
|
15852
|
+
React39.useEffect(() => {
|
|
15853
|
+
if (!onSearchChange) return void 0;
|
|
15854
|
+
const timeoutId = window.setTimeout(() => onSearchChange(query), searchDebounceMs);
|
|
15855
|
+
return () => window.clearTimeout(timeoutId);
|
|
15856
|
+
}, [onSearchChange, query, searchDebounceMs]);
|
|
15857
|
+
React39.useEffect(() => {
|
|
15858
|
+
if (process.env.NODE_ENV !== "production" && normalizedOptions.length > 300 && !virtualized && searchMode !== "manual" && maxInitialOptions === void 0) {
|
|
15859
|
+
console.warn(
|
|
15860
|
+
'[Underverse UI] MultiCombobox received more than 300 options without virtualization, manual search, or maxInitialOptions. Use virtualized, searchMode="manual", or maxInitialOptions to avoid rendering a large dropdown.'
|
|
15861
|
+
);
|
|
15862
|
+
}
|
|
15863
|
+
}, [maxInitialOptions, normalizedOptions.length, searchMode, virtualized]);
|
|
15688
15864
|
const sizeStyles8 = {
|
|
15689
15865
|
sm: {
|
|
15690
15866
|
trigger: "h-8 px-3 py-1.5 text-sm md:h-7 md:text-xs",
|
|
@@ -15718,25 +15894,38 @@ var MultiCombobox = ({
|
|
|
15718
15894
|
const labelId = label ? `${resolvedId}-label` : void 0;
|
|
15719
15895
|
const labelSize = size === "sm" ? "text-xs" : size === "lg" ? "text-base" : "text-sm";
|
|
15720
15896
|
const listboxId = `${resolvedId}-listbox`;
|
|
15721
|
-
const renderOptionItem = (item, index) => {
|
|
15897
|
+
const renderOptionItem = (item, index, virtualItem) => {
|
|
15722
15898
|
const isSelected = value.includes(item.value);
|
|
15723
15899
|
const isDisabled = item.disabled || disabledOptions.includes(item.value);
|
|
15724
15900
|
const optionIcon = item.icon;
|
|
15725
15901
|
const optionDesc = item.description;
|
|
15902
|
+
const itemStyle = {
|
|
15903
|
+
animationDelay: open ? `${Math.min(index * 20, 200)}ms` : "0ms",
|
|
15904
|
+
...virtualItem ? {
|
|
15905
|
+
position: "absolute",
|
|
15906
|
+
top: 0,
|
|
15907
|
+
left: 0,
|
|
15908
|
+
width: "100%",
|
|
15909
|
+
transform: `translateY(${virtualItem.start}px)`
|
|
15910
|
+
} : {}
|
|
15911
|
+
};
|
|
15912
|
+
const measureRef = virtualItem ? optionVirtualizer.measureElement : void 0;
|
|
15726
15913
|
if (renderOption) {
|
|
15727
15914
|
return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
15728
15915
|
"li",
|
|
15729
15916
|
{
|
|
15730
15917
|
ref: (node) => {
|
|
15918
|
+
measureRef?.(node);
|
|
15731
15919
|
listRef.current[index] = node;
|
|
15732
15920
|
},
|
|
15921
|
+
"data-index": virtualItem?.index,
|
|
15922
|
+
style: itemStyle,
|
|
15733
15923
|
onClick: (e) => {
|
|
15734
15924
|
e.preventDefault();
|
|
15735
15925
|
e.stopPropagation();
|
|
15736
15926
|
if (!isDisabled) toggleSelect(item.value);
|
|
15737
15927
|
inputRef.current?.focus();
|
|
15738
15928
|
},
|
|
15739
|
-
style: { animationDelay: open ? `${Math.min(index * 20, 200)}ms` : "0ms" },
|
|
15740
15929
|
className: cn("dropdown-item", isDisabled && "opacity-50 cursor-not-allowed pointer-events-none"),
|
|
15741
15930
|
children: renderOption(item, isSelected)
|
|
15742
15931
|
},
|
|
@@ -15747,15 +15936,17 @@ var MultiCombobox = ({
|
|
|
15747
15936
|
"li",
|
|
15748
15937
|
{
|
|
15749
15938
|
ref: (node) => {
|
|
15939
|
+
measureRef?.(node);
|
|
15750
15940
|
listRef.current[index] = node;
|
|
15751
15941
|
},
|
|
15942
|
+
"data-index": virtualItem?.index,
|
|
15943
|
+
style: itemStyle,
|
|
15752
15944
|
onClick: (e) => {
|
|
15753
15945
|
e.preventDefault();
|
|
15754
15946
|
e.stopPropagation();
|
|
15755
15947
|
if (!isDisabled) toggleSelect(item.value);
|
|
15756
15948
|
inputRef.current?.focus();
|
|
15757
15949
|
},
|
|
15758
|
-
style: { animationDelay: open ? `${Math.min(index * 20, 200)}ms` : "0ms" },
|
|
15759
15950
|
className: cn(
|
|
15760
15951
|
"dropdown-item flex cursor-pointer items-center gap-3 rounded-full transition-all duration-200",
|
|
15761
15952
|
sizeStyles8[size].item,
|
|
@@ -15819,6 +16010,7 @@ var MultiCombobox = ({
|
|
|
15819
16010
|
onChange: (e) => {
|
|
15820
16011
|
setQuery(e.target.value);
|
|
15821
16012
|
setActiveIndex(null);
|
|
16013
|
+
scrollVirtualListToStart();
|
|
15822
16014
|
},
|
|
15823
16015
|
onKeyDown: handleKeyDown2,
|
|
15824
16016
|
placeholder: searchPlaceholder,
|
|
@@ -15829,7 +16021,10 @@ var MultiCombobox = ({
|
|
|
15829
16021
|
"button",
|
|
15830
16022
|
{
|
|
15831
16023
|
type: "button",
|
|
15832
|
-
onClick: () =>
|
|
16024
|
+
onClick: () => {
|
|
16025
|
+
setQuery("");
|
|
16026
|
+
scrollVirtualListToStart();
|
|
16027
|
+
},
|
|
15833
16028
|
className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors",
|
|
15834
16029
|
children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react24.X, { className: "w-4 h-4" })
|
|
15835
16030
|
}
|
|
@@ -15850,30 +16045,52 @@ var MultiCombobox = ({
|
|
|
15850
16045
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react24.Sparkles, { className: "h-4 w-4 text-primary/60 absolute -top-1 -right-1 animate-pulse" })
|
|
15851
16046
|
] }),
|
|
15852
16047
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground font-medium", children: loadingText })
|
|
15853
|
-
] }) }) :
|
|
16048
|
+
] }) }) : shouldPromptForSearch ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("li", { className: "px-3 py-8 text-center text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-col items-center gap-3 animate-in fade-in-0 zoom-in-95 duration-300", children: [
|
|
16049
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react24.Search, { className: "h-10 w-10 opacity-30 text-muted-foreground" }),
|
|
16050
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "font-medium block text-foreground", children: [
|
|
16051
|
+
"Type at least ",
|
|
16052
|
+
minSearchLength,
|
|
16053
|
+
" characters to search"
|
|
16054
|
+
] })
|
|
16055
|
+
] }) }) : renderLimitedOptions.length ? groupedOptions ? (
|
|
15854
16056
|
// Render grouped options
|
|
15855
16057
|
Array.from(groupedOptions.entries()).map(([group, items]) => /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("li", { className: "mb-2", children: [
|
|
15856
16058
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "px-3 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 bg-popover/95 backdrop-blur-sm", children: group }),
|
|
15857
|
-
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("ul", { children: items.map((item) => renderOptionItem(item,
|
|
16059
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("ul", { children: items.map((item) => renderOptionItem(item, renderLimitedOptions.indexOf(item))) })
|
|
15858
16060
|
] }, group))
|
|
15859
16061
|
) : (
|
|
15860
16062
|
// Render flat options
|
|
15861
|
-
|
|
16063
|
+
canVirtualize ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("li", { role: "presentation", className: "list-none p-0", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("ul", { className: "relative", style: { height: `${optionVirtualizer.getTotalSize()}px` }, children: virtualItems.map((virtualItem) => renderOptionItem(renderLimitedOptions[virtualItem.index], virtualItem.index, virtualItem)) }) }) : renderLimitedOptions.map((item, index) => renderOptionItem(item, index))
|
|
15862
16064
|
) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("li", { className: cn("px-3 py-8 text-center text-muted-foreground"), children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-col items-center gap-3 animate-in fade-in-0 zoom-in-95 duration-300", children: [
|
|
15863
16065
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react24.SearchX, { className: "h-10 w-10 opacity-30 text-muted-foreground" }),
|
|
15864
16066
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "space-y-1", children: [
|
|
15865
16067
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "font-medium block", children: emptyText }),
|
|
15866
16068
|
query && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-xs opacity-60", children: "Try a different search term" })
|
|
15867
16069
|
] }),
|
|
15868
|
-
query && /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
|
|
15869
|
-
|
|
15870
|
-
|
|
15871
|
-
|
|
16070
|
+
query && /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
|
|
16071
|
+
"button",
|
|
16072
|
+
{
|
|
16073
|
+
type: "button",
|
|
16074
|
+
onClick: () => {
|
|
16075
|
+
setQuery("");
|
|
16076
|
+
scrollVirtualListToStart();
|
|
16077
|
+
},
|
|
16078
|
+
className: "text-xs text-primary hover:underline flex items-center gap-1",
|
|
16079
|
+
children: [
|
|
16080
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react24.X, { className: "w-3 h-3" }),
|
|
16081
|
+
"Clear search"
|
|
16082
|
+
]
|
|
16083
|
+
}
|
|
16084
|
+
)
|
|
15872
16085
|
] }) })
|
|
15873
16086
|
}
|
|
15874
16087
|
)
|
|
15875
16088
|
] });
|
|
15876
|
-
const
|
|
16089
|
+
const selectedOptionFallbackMap = React39.useMemo(
|
|
16090
|
+
() => new Map((selectedOptionsProp ?? []).map((option) => [option.value, option])),
|
|
16091
|
+
[selectedOptionsProp]
|
|
16092
|
+
);
|
|
16093
|
+
const selectedOptions = value.map((v) => normalizedOptions.find((o) => o.value === v) ?? selectedOptionFallbackMap.get(v)).filter(Boolean);
|
|
15877
16094
|
const visibleTags = maxTagsVisible ? selectedOptions.slice(0, maxTagsVisible) : selectedOptions;
|
|
15878
16095
|
const hiddenCount = maxTagsVisible ? Math.max(0, selectedOptions.length - maxTagsVisible) : 0;
|
|
15879
16096
|
const triggerButton = /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
|