@papernote/ui 2.0.0 → 2.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/components/Combobox.d.ts +3 -3
- package/dist/components/Combobox.d.ts.map +1 -1
- package/dist/components/Select.d.ts +3 -3
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.esm.js +182 -121
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +182 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Combobox.tsx +416 -330
- package/src/components/Select.tsx +367 -241
package/dist/index.esm.js
CHANGED
|
@@ -705,15 +705,15 @@ function usePrefersMobile() {
|
|
|
705
705
|
|
|
706
706
|
// Size classes for trigger button
|
|
707
707
|
const sizeClasses$d = {
|
|
708
|
-
sm:
|
|
709
|
-
md:
|
|
710
|
-
lg:
|
|
708
|
+
sm: "h-8 text-sm py-1",
|
|
709
|
+
md: "h-10 text-base py-2",
|
|
710
|
+
lg: "h-12 text-base py-3 min-h-touch", // 44px touch target
|
|
711
711
|
};
|
|
712
712
|
// Size classes for options
|
|
713
713
|
const optionSizeClasses = {
|
|
714
|
-
sm:
|
|
715
|
-
md:
|
|
716
|
-
lg:
|
|
714
|
+
sm: "py-2 text-sm",
|
|
715
|
+
md: "py-2.5 text-sm",
|
|
716
|
+
lg: "py-3.5 text-base min-h-touch", // 44px touch target for mobile
|
|
717
717
|
};
|
|
718
718
|
/**
|
|
719
719
|
* Select - Dropdown select component with search, groups, virtual scrolling, and mobile support
|
|
@@ -786,9 +786,9 @@ const optionSizeClasses = {
|
|
|
786
786
|
* ```
|
|
787
787
|
*/
|
|
788
788
|
const Select = forwardRef((props, ref) => {
|
|
789
|
-
const { options = [], groups = [], value, onChange, placeholder =
|
|
789
|
+
const { options = [], groups = [], value, onChange, placeholder = "Select an option", searchable = false, disabled = false, label, helperText, error, loading = false, clearable = false, creatable = false, onCreateOption, virtualized = false, virtualHeight = "300px", virtualItemHeight = 42, size = "md", mobileMode = "auto", usePortal = true, required = false, } = props;
|
|
790
790
|
const [isOpen, setIsOpen] = useState(false);
|
|
791
|
-
const [searchQuery, setSearchQuery] = useState(
|
|
791
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
792
792
|
const [scrollTop, setScrollTop] = useState(0);
|
|
793
793
|
const [activeDescendant] = useState(undefined);
|
|
794
794
|
const [dropdownPosition, setDropdownPosition] = useState(null);
|
|
@@ -801,10 +801,10 @@ const Select = forwardRef((props, ref) => {
|
|
|
801
801
|
const nativeSelectRef = useRef(null);
|
|
802
802
|
// Detect mobile viewport
|
|
803
803
|
const isMobile = useIsMobile();
|
|
804
|
-
const useMobileSheet = mobileMode ===
|
|
805
|
-
const useNativeSelect = mobileMode ===
|
|
804
|
+
const useMobileSheet = mobileMode === "auto" && isMobile;
|
|
805
|
+
const useNativeSelect = mobileMode === "native" && isMobile;
|
|
806
806
|
// Auto-size for mobile
|
|
807
|
-
const effectiveSize = isMobile && size ===
|
|
807
|
+
const effectiveSize = isMobile && size === "md" ? "lg" : size;
|
|
808
808
|
// Generate unique IDs for ARIA
|
|
809
809
|
const labelId = useId();
|
|
810
810
|
const listboxId = useId();
|
|
@@ -818,11 +818,8 @@ const Select = forwardRef((props, ref) => {
|
|
|
818
818
|
close: () => setIsOpen(false),
|
|
819
819
|
}));
|
|
820
820
|
// Flatten all options (from both options and groups)
|
|
821
|
-
const allOptions = [
|
|
822
|
-
|
|
823
|
-
...groups.flatMap(group => group.options)
|
|
824
|
-
];
|
|
825
|
-
const selectedOption = allOptions.find(opt => opt.value === value);
|
|
821
|
+
const allOptions = [...options, ...groups.flatMap((group) => group.options)];
|
|
822
|
+
const selectedOption = allOptions.find((opt) => opt.value === value);
|
|
826
823
|
// Filter options/groups based on search
|
|
827
824
|
const getFilteredData = () => {
|
|
828
825
|
if (!searchable || !searchQuery) {
|
|
@@ -830,41 +827,58 @@ const Select = forwardRef((props, ref) => {
|
|
|
830
827
|
}
|
|
831
828
|
const query = searchQuery.toLowerCase();
|
|
832
829
|
// Filter flat options
|
|
833
|
-
const filteredOptions = options.filter(opt => opt.label.toLowerCase().includes(query));
|
|
830
|
+
const filteredOptions = options.filter((opt) => opt.label.toLowerCase().includes(query));
|
|
834
831
|
// Filter grouped options
|
|
835
832
|
const filteredGroups = groups
|
|
836
|
-
.map(group => ({
|
|
833
|
+
.map((group) => ({
|
|
837
834
|
...group,
|
|
838
|
-
options: group.options.filter(opt => opt.label.toLowerCase().includes(query))
|
|
835
|
+
options: group.options.filter((opt) => opt.label.toLowerCase().includes(query)),
|
|
839
836
|
}))
|
|
840
|
-
.filter(group => group.options.length > 0);
|
|
837
|
+
.filter((group) => group.options.length > 0);
|
|
841
838
|
return { options: filteredOptions, groups: filteredGroups };
|
|
842
839
|
};
|
|
843
840
|
const { options: filteredOptions, groups: filteredGroups } = getFilteredData();
|
|
844
841
|
// Virtual scrolling calculations
|
|
845
|
-
const totalItems = filteredOptions.length + filteredGroups.flatMap(g => g.options).length;
|
|
842
|
+
const totalItems = filteredOptions.length + filteredGroups.flatMap((g) => g.options).length;
|
|
846
843
|
const useVirtualScrolling = virtualized && totalItems > 50;
|
|
847
844
|
const visibleRangeStart = useVirtualScrolling
|
|
848
845
|
? Math.floor(scrollTop / virtualItemHeight)
|
|
849
846
|
: 0;
|
|
850
847
|
const visibleRangeEnd = useVirtualScrolling
|
|
851
|
-
? Math.min(visibleRangeStart +
|
|
848
|
+
? Math.min(visibleRangeStart +
|
|
849
|
+
Math.ceil(parseInt(virtualHeight) / virtualItemHeight) +
|
|
850
|
+
5, totalItems)
|
|
852
851
|
: totalItems;
|
|
853
852
|
// Flatten all filtered items for virtualization
|
|
854
853
|
const allFilteredItems = [
|
|
855
|
-
...filteredOptions.map((opt, idx) => ({
|
|
856
|
-
|
|
854
|
+
...filteredOptions.map((opt, idx) => ({
|
|
855
|
+
type: "option",
|
|
856
|
+
option: opt,
|
|
857
|
+
groupIndex: -1,
|
|
858
|
+
optionIndex: idx,
|
|
859
|
+
})),
|
|
860
|
+
...filteredGroups.flatMap((group, groupIdx) => group.options.map((opt, optIdx) => ({
|
|
861
|
+
type: "grouped",
|
|
862
|
+
option: opt,
|
|
863
|
+
groupIndex: groupIdx,
|
|
864
|
+
optionIndex: optIdx,
|
|
865
|
+
groupLabel: group.label,
|
|
866
|
+
}))),
|
|
857
867
|
];
|
|
858
868
|
const visibleItems = useVirtualScrolling
|
|
859
869
|
? allFilteredItems.slice(visibleRangeStart, visibleRangeEnd)
|
|
860
870
|
: allFilteredItems;
|
|
861
|
-
const offsetY = useVirtualScrolling
|
|
862
|
-
|
|
871
|
+
const offsetY = useVirtualScrolling
|
|
872
|
+
? visibleRangeStart * virtualItemHeight
|
|
873
|
+
: 0;
|
|
874
|
+
const totalHeight = useVirtualScrolling
|
|
875
|
+
? totalItems * virtualItemHeight
|
|
876
|
+
: "auto";
|
|
863
877
|
// Check if we should show "Create" option
|
|
864
878
|
const showCreateOption = creatable &&
|
|
865
|
-
searchQuery.trim() !==
|
|
866
|
-
!filteredOptions.some(opt => opt.label.toLowerCase() === searchQuery.toLowerCase()) &&
|
|
867
|
-
!filteredGroups.some(group => group.options.some(opt => opt.label.toLowerCase() === searchQuery.toLowerCase()));
|
|
879
|
+
searchQuery.trim() !== "" &&
|
|
880
|
+
!filteredOptions.some((opt) => opt.label.toLowerCase() === searchQuery.toLowerCase()) &&
|
|
881
|
+
!filteredGroups.some((group) => group.options.some((opt) => opt.label.toLowerCase() === searchQuery.toLowerCase()));
|
|
868
882
|
// Handle creating new option
|
|
869
883
|
const handleCreateOption = () => {
|
|
870
884
|
if (onCreateOption) {
|
|
@@ -874,7 +888,7 @@ const Select = forwardRef((props, ref) => {
|
|
|
874
888
|
// If no callback, just select the typed value
|
|
875
889
|
onChange?.(searchQuery.trim());
|
|
876
890
|
}
|
|
877
|
-
setSearchQuery(
|
|
891
|
+
setSearchQuery("");
|
|
878
892
|
setIsOpen(false);
|
|
879
893
|
};
|
|
880
894
|
// Handle click outside (desktop dropdown only)
|
|
@@ -888,14 +902,14 @@ const Select = forwardRef((props, ref) => {
|
|
|
888
902
|
const isOutsideDropdown = dropdownRef.current && !dropdownRef.current.contains(target);
|
|
889
903
|
if (isOutsideSelect && isOutsideDropdown) {
|
|
890
904
|
setIsOpen(false);
|
|
891
|
-
setSearchQuery(
|
|
905
|
+
setSearchQuery("");
|
|
892
906
|
}
|
|
893
907
|
};
|
|
894
908
|
if (isOpen) {
|
|
895
|
-
document.addEventListener(
|
|
909
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
896
910
|
}
|
|
897
911
|
return () => {
|
|
898
|
-
document.removeEventListener(
|
|
912
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
899
913
|
};
|
|
900
914
|
}, [isOpen, useMobileSheet]);
|
|
901
915
|
// Focus search input when opened
|
|
@@ -929,8 +943,8 @@ const Select = forwardRef((props, ref) => {
|
|
|
929
943
|
const hasSpaceBelow = spaceBelow >= dropdownHeight + gap;
|
|
930
944
|
const hasSpaceAbove = spaceAbove >= dropdownHeight + gap;
|
|
931
945
|
// Prefer bottom placement, flip to top if not enough space below but enough above
|
|
932
|
-
const placement = hasSpaceBelow || !hasSpaceAbove ?
|
|
933
|
-
const top = placement ===
|
|
946
|
+
const placement = hasSpaceBelow || !hasSpaceAbove ? "bottom" : "top";
|
|
947
|
+
const top = placement === "bottom"
|
|
934
948
|
? rect.bottom + gap
|
|
935
949
|
: rect.top - dropdownHeight - gap;
|
|
936
950
|
setDropdownPosition({
|
|
@@ -943,23 +957,23 @@ const Select = forwardRef((props, ref) => {
|
|
|
943
957
|
// Initial position calculation
|
|
944
958
|
updatePosition();
|
|
945
959
|
// Listen for scroll events on all scrollable ancestors
|
|
946
|
-
window.addEventListener(
|
|
947
|
-
window.addEventListener(
|
|
960
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
961
|
+
window.addEventListener("resize", updatePosition);
|
|
948
962
|
return () => {
|
|
949
|
-
window.removeEventListener(
|
|
950
|
-
window.removeEventListener(
|
|
963
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
964
|
+
window.removeEventListener("resize", updatePosition);
|
|
951
965
|
};
|
|
952
966
|
}, [isOpen, useMobileSheet, usePortal]);
|
|
953
967
|
// Lock body scroll when mobile sheet is open
|
|
954
968
|
useEffect(() => {
|
|
955
969
|
if (useMobileSheet && isOpen) {
|
|
956
|
-
document.body.style.overflow =
|
|
970
|
+
document.body.style.overflow = "hidden";
|
|
957
971
|
}
|
|
958
972
|
else {
|
|
959
|
-
document.body.style.overflow =
|
|
973
|
+
document.body.style.overflow = "";
|
|
960
974
|
}
|
|
961
975
|
return () => {
|
|
962
|
-
document.body.style.overflow =
|
|
976
|
+
document.body.style.overflow = "";
|
|
963
977
|
};
|
|
964
978
|
}, [isOpen, useMobileSheet]);
|
|
965
979
|
// Handle escape key for mobile sheet
|
|
@@ -967,83 +981,105 @@ const Select = forwardRef((props, ref) => {
|
|
|
967
981
|
if (!useMobileSheet || !isOpen)
|
|
968
982
|
return;
|
|
969
983
|
const handleEscape = (e) => {
|
|
970
|
-
if (e.key ===
|
|
984
|
+
if (e.key === "Escape") {
|
|
971
985
|
setIsOpen(false);
|
|
972
|
-
setSearchQuery(
|
|
986
|
+
setSearchQuery("");
|
|
973
987
|
}
|
|
974
988
|
};
|
|
975
|
-
document.addEventListener(
|
|
976
|
-
return () => document.removeEventListener(
|
|
989
|
+
document.addEventListener("keydown", handleEscape);
|
|
990
|
+
return () => document.removeEventListener("keydown", handleEscape);
|
|
977
991
|
}, [isOpen, useMobileSheet]);
|
|
978
992
|
const handleSelect = (optionValue) => {
|
|
979
993
|
onChange?.(optionValue);
|
|
980
994
|
setIsOpen(false);
|
|
981
|
-
setSearchQuery(
|
|
995
|
+
setSearchQuery("");
|
|
982
996
|
};
|
|
983
997
|
const handleClose = () => {
|
|
984
998
|
setIsOpen(false);
|
|
985
|
-
setSearchQuery(
|
|
999
|
+
setSearchQuery("");
|
|
986
1000
|
};
|
|
987
1001
|
// Render option button (shared between desktop and mobile)
|
|
988
1002
|
const renderOption = (option, isSelected, mobile = false) => (jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, className: `
|
|
989
1003
|
w-full flex items-center justify-between px-4 transition-colors
|
|
990
1004
|
${mobile ? optionSizeClasses.lg : optionSizeClasses[effectiveSize]}
|
|
991
|
-
${isSelected ?
|
|
992
|
-
${option.disabled ?
|
|
993
|
-
`, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && jsx(Check, { className: `${mobile ?
|
|
1005
|
+
${isSelected ? "bg-accent-50 text-accent-900" : "text-ink-700"}
|
|
1006
|
+
${option.disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-paper-50 active:bg-paper-100 cursor-pointer"}
|
|
1007
|
+
`, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && (jsx(Check, { className: `${mobile ? "h-5 w-5" : "h-4 w-4"} text-accent-600` }))] }, option.value));
|
|
994
1008
|
// Render options list content (shared between desktop and mobile)
|
|
995
1009
|
const renderOptionsContent = (mobile = false) => {
|
|
996
1010
|
if (loading) {
|
|
997
1011
|
return (jsxs("div", { className: "px-4 py-8 flex items-center justify-center", role: "status", "aria-live": "polite", children: [jsx(Loader2, { className: "h-5 w-5 animate-spin text-ink-500" }), jsx("span", { className: "ml-2 text-sm text-ink-500", children: "Loading..." })] }));
|
|
998
1012
|
}
|
|
999
|
-
if (filteredOptions.length === 0 &&
|
|
1013
|
+
if (filteredOptions.length === 0 &&
|
|
1014
|
+
filteredGroups.length === 0 &&
|
|
1015
|
+
!showCreateOption) {
|
|
1000
1016
|
return (jsx("div", { className: "px-4 py-3 text-sm text-ink-500 text-center", role: "status", "aria-live": "polite", children: "No options found" }));
|
|
1001
1017
|
}
|
|
1002
1018
|
return (jsxs(Fragment, { children: [showCreateOption && (jsx("button", { type: "button", onClick: handleCreateOption, className: `
|
|
1003
1019
|
w-full flex items-center px-4 text-accent-700 hover:bg-accent-50 transition-colors border-b border-paper-200
|
|
1004
|
-
${mobile ?
|
|
1005
|
-
`, children: jsxs("span", { className: "font-medium", children: ["Create \"", searchQuery, "\""] }) })), useVirtualScrolling ? (jsx("div", { style: { height: totalHeight, position:
|
|
1020
|
+
${mobile ? "py-3.5 text-base" : "py-2.5 text-sm"}
|
|
1021
|
+
`, children: jsxs("span", { className: "font-medium", children: ["Create \"", searchQuery, "\""] }) })), useVirtualScrolling ? (jsx("div", { style: { height: totalHeight, position: "relative" }, children: jsx("div", { style: { transform: `translateY(${offsetY}px)` }, children: visibleItems.map((item) => {
|
|
1006
1022
|
const option = item.option;
|
|
1007
1023
|
const isSelected = option.value === value;
|
|
1008
1024
|
const key = `${item.type}-${item.groupIndex}-${item.optionIndex}-${option.value}`;
|
|
1009
|
-
return (jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, style: {
|
|
1025
|
+
return (jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, style: {
|
|
1026
|
+
height: mobile ? "56px" : `${virtualItemHeight}px`,
|
|
1027
|
+
}, className: `
|
|
1010
1028
|
w-full flex items-center justify-between px-4 transition-colors
|
|
1011
|
-
${mobile ?
|
|
1012
|
-
${isSelected ?
|
|
1013
|
-
${option.disabled ?
|
|
1014
|
-
`, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && jsx(Check, { className: `${mobile ?
|
|
1029
|
+
${mobile ? "text-base" : "text-sm"}
|
|
1030
|
+
${isSelected ? "bg-accent-50 text-accent-900" : "text-ink-700"}
|
|
1031
|
+
${option.disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-paper-50 cursor-pointer"}
|
|
1032
|
+
`, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && (jsx(Check, { className: `${mobile ? "h-5 w-5" : "h-4 w-4"} text-accent-600` }))] }, key));
|
|
1015
1033
|
}) }) })) : (jsxs(Fragment, { children: [filteredOptions.map((option) => renderOption(option, option.value === value, mobile)), filteredGroups.map((group) => (jsxs("div", { children: [jsx("div", { className: `
|
|
1016
1034
|
px-4 font-semibold text-ink-500 uppercase tracking-wider bg-paper-50 border-t border-b border-paper-200
|
|
1017
|
-
${mobile ?
|
|
1035
|
+
${mobile ? "py-2.5 text-xs" : "py-2 text-xs"}
|
|
1018
1036
|
`, children: group.label }), group.options.map((option) => renderOption(option, option.value === value, mobile))] }, group.label)))] }))] }));
|
|
1019
1037
|
};
|
|
1020
1038
|
// Native select for mobile (optional)
|
|
1021
1039
|
if (useNativeSelect) {
|
|
1022
|
-
return (jsxs("div", { className: "w-full", children: [label && (jsxs("label", { id: labelId, className: "label", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsxs("select", { ref: nativeSelectRef, value: value ||
|
|
1040
|
+
return (jsxs("div", { className: "w-full", children: [label && (jsxs("label", { id: labelId, className: "label", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsxs("select", { ref: nativeSelectRef, value: value || "", onChange: (e) => onChange?.(e.target.value), disabled: disabled, className: `
|
|
1023
1041
|
input w-full appearance-none pr-10
|
|
1024
1042
|
${sizeClasses$d[effectiveSize]}
|
|
1025
|
-
${error ?
|
|
1026
|
-
${disabled ?
|
|
1027
|
-
`, "aria-labelledby": label ? labelId : undefined, "aria-invalid": error ?
|
|
1043
|
+
${error ? "border-error-400 focus:border-error-400 focus:ring-error-400" : ""}
|
|
1044
|
+
${disabled ? "opacity-40 cursor-not-allowed" : "cursor-pointer"}
|
|
1045
|
+
`, "aria-labelledby": label ? labelId : undefined, "aria-invalid": error ? "true" : undefined, "aria-describedby": error ? errorId : helperText ? helperTextId : undefined, "aria-required": required, children: [jsx("option", { value: "", disabled: true, children: placeholder }), options.map((opt) => (jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))), groups.map((group) => (jsx("optgroup", { label: group.label, children: group.options.map((opt) => (jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }, group.label)))] }), jsx(ChevronDown, { className: "absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-500 pointer-events-none" })] }), error && (jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
|
|
1028
1046
|
}
|
|
1029
1047
|
return (jsxs("div", { className: "w-full", children: [label && (jsxs("label", { id: labelId, className: "label", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsx("div", { ref: selectRef, className: "relative", children: jsxs("button", { ref: buttonRef, type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `
|
|
1030
1048
|
input w-full flex items-center justify-between px-3
|
|
1031
1049
|
${sizeClasses$d[effectiveSize]}
|
|
1032
|
-
${error ?
|
|
1033
|
-
${disabled ?
|
|
1034
|
-
`, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? placeholder : undefined, "aria-activedescendant": activeDescendant, "aria-invalid": error ?
|
|
1050
|
+
${error ? "border-error-400 focus:border-error-400 focus:ring-error-400" : ""}
|
|
1051
|
+
${disabled ? "opacity-40 cursor-not-allowed" : "cursor-pointer"}
|
|
1052
|
+
`, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? placeholder : undefined, "aria-activedescendant": activeDescendant, "aria-invalid": error ? "true" : undefined, "aria-describedby": error ? errorId : helperText ? helperTextId : undefined, "aria-disabled": disabled, "aria-required": required, children: [jsxs("span", { className: `flex items-center gap-2 ${selectedOption ? "text-ink-800" : "text-ink-400"}`, children: [loading && (jsx(Loader2, { className: "h-4 w-4 animate-spin text-ink-500" })), !loading && selectedOption?.icon && (jsx("span", { children: selectedOption.icon })), selectedOption ? selectedOption.label : placeholder] }), jsxs("div", { className: "flex items-center gap-1", children: [clearable && value && (jsx("span", { role: "button", tabIndex: -1, onClick: (e) => {
|
|
1035
1053
|
e.stopPropagation();
|
|
1036
|
-
onChange?.(
|
|
1054
|
+
onChange?.("");
|
|
1037
1055
|
setIsOpen(false);
|
|
1038
|
-
},
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1056
|
+
}, onKeyDown: (e) => {
|
|
1057
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1058
|
+
e.preventDefault();
|
|
1059
|
+
e.stopPropagation();
|
|
1060
|
+
onChange?.("");
|
|
1061
|
+
setIsOpen(false);
|
|
1062
|
+
}
|
|
1063
|
+
}, className: "text-ink-400 hover:text-ink-600 transition-colors p-0.5 cursor-pointer inline-flex", "aria-label": "Clear selection", children: jsx(X, { className: `${effectiveSize === "lg" ? "h-5 w-5" : "h-4 w-4"}` }) })), jsx(ChevronDown, { className: `${effectiveSize === "lg" ? "h-5 w-5" : "h-4 w-4"} text-ink-500 transition-transform ${isOpen ? "rotate-180" : ""}` })] })] }) }), isOpen &&
|
|
1064
|
+
!useMobileSheet &&
|
|
1065
|
+
(usePortal ? dropdownPosition : true) &&
|
|
1066
|
+
(usePortal ? (createPortal(jsxs("div", { ref: dropdownRef, className: `fixed z-[9999] bg-white bg-subtle-grain rounded-lg shadow-lg border border-paper-200 max-h-60 overflow-hidden animate-fade-in ${dropdownPosition?.placement === "top"
|
|
1067
|
+
? "origin-bottom"
|
|
1068
|
+
: "origin-top"}`, style: {
|
|
1069
|
+
top: dropdownPosition.top,
|
|
1070
|
+
left: dropdownPosition.left,
|
|
1071
|
+
width: dropdownPosition.width,
|
|
1072
|
+
}, children: [searchable && (jsx("div", { className: "p-2 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" }), jsx("input", { ref: searchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", className: "w-full pl-9 pr-3 py-2 text-sm border border-paper-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400", role: "searchbox", "aria-label": "Search options", "aria-autocomplete": "list", "aria-controls": listboxId })] }) })), jsx("div", { ref: listRef, id: listboxId, className: "overflow-y-auto", style: {
|
|
1073
|
+
maxHeight: useVirtualScrolling ? virtualHeight : "12rem",
|
|
1074
|
+
}, onScroll: (e) => useVirtualScrolling && setScrollTop(e.currentTarget.scrollTop), role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(false) })] }), document.body)) : (
|
|
1075
|
+
// Non-portal dropdown (inline, relative positioning)
|
|
1076
|
+
jsxs("div", { ref: dropdownRef, className: "absolute z-50 mt-1 w-full bg-white bg-subtle-grain rounded-lg shadow-lg border border-paper-200 max-h-60 overflow-hidden animate-fade-in", children: [searchable && (jsx("div", { className: "p-2 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" }), jsx("input", { ref: searchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", className: "w-full pl-9 pr-3 py-2 text-sm border border-paper-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400", role: "searchbox", "aria-label": "Search options", "aria-autocomplete": "list", "aria-controls": listboxId })] }) })), jsx("div", { ref: listRef, id: listboxId, className: "overflow-y-auto", style: {
|
|
1077
|
+
maxHeight: useVirtualScrolling ? virtualHeight : "12rem",
|
|
1078
|
+
}, onScroll: (e) => useVirtualScrolling && setScrollTop(e.currentTarget.scrollTop), role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(false) })] }))), isOpen &&
|
|
1079
|
+
useMobileSheet &&
|
|
1080
|
+
createPortal(jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: (e) => e.target === e.currentTarget && handleClose(), role: "dialog", "aria-modal": "true", "aria-labelledby": label ? `mobile-${labelId}` : undefined, children: [jsx("div", { className: "absolute inset-0 bg-black/50 animate-fade-in" }), jsxs("div", { className: "relative w-full bg-white rounded-t-2xl shadow-2xl animate-slide-up max-h-[85vh] flex flex-col", style: { paddingBottom: "env(safe-area-inset-bottom)" }, children: [jsx("div", { className: "py-3 cursor-grab", children: jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) }), jsxs("div", { className: "px-4 pb-3 border-b border-paper-200 flex items-center justify-between", children: [label && (jsx("h2", { id: `mobile-${labelId}`, className: "text-lg font-semibold text-ink-900", children: label })), !label && (jsx("h2", { className: "text-lg font-semibold text-ink-900", children: placeholder })), jsx("button", { onClick: handleClose, className: "text-ink-400 hover:text-ink-600 transition-colors p-2 -mr-2", "aria-label": "Close", children: jsx(X, { className: "h-5 w-5" }) })] }), searchable && (jsx("div", { className: "p-3 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-400" }), jsx("input", { ref: mobileSearchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", inputMode: "search", enterKeyHint: "search", className: "w-full pl-12 pr-4 py-3 text-base border border-paper-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400", role: "searchbox", "aria-label": "Search options" })] }) })), jsx("div", { id: listboxId, className: "overflow-y-auto flex-1", role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(true) })] })] }), document.body), error && (jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
|
|
1045
1081
|
});
|
|
1046
|
-
Select.displayName =
|
|
1082
|
+
Select.displayName = "Select";
|
|
1047
1083
|
|
|
1048
1084
|
const MultiSelect = forwardRef(({ options, value = [], onChange, placeholder = 'Select options', searchable = false, disabled = false, label, helperText, error, maxHeight = 240, maxSelections, loading = false, 'aria-label': ariaLabel, }, ref) => {
|
|
1049
1085
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -2490,9 +2526,9 @@ TimezoneSelector.displayName = 'TimezoneSelector';
|
|
|
2490
2526
|
* />
|
|
2491
2527
|
* ```
|
|
2492
2528
|
*/
|
|
2493
|
-
const Combobox = forwardRef(({ value =
|
|
2529
|
+
const Combobox = forwardRef(({ value = "", onChange, options, onSearch, onCreateOption, label, placeholder = "Search or select...", allowCustomValue = false, loading = false, validationState, validationMessage, helperText, required = false, disabled = false, className = "", size = "md", }, ref) => {
|
|
2494
2530
|
const [isOpen, setIsOpen] = useState(false);
|
|
2495
|
-
const [searchQuery, setSearchQuery] = useState(
|
|
2531
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
2496
2532
|
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
2497
2533
|
const containerRef = useRef(null);
|
|
2498
2534
|
const inputRef = useRef(null);
|
|
@@ -2509,23 +2545,26 @@ const Combobox = forwardRef(({ value = '', onChange, options, onSearch, onCreate
|
|
|
2509
2545
|
close: () => setIsOpen(false),
|
|
2510
2546
|
}));
|
|
2511
2547
|
// Filter options based on search query
|
|
2512
|
-
const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
2548
|
+
const filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
2513
2549
|
// Get display value
|
|
2514
|
-
const selectedOption = options.find(opt => opt.value === value);
|
|
2515
|
-
const displayValue = isOpen
|
|
2550
|
+
const selectedOption = options.find((opt) => opt.value === value);
|
|
2551
|
+
const displayValue = isOpen
|
|
2552
|
+
? searchQuery
|
|
2553
|
+
: selectedOption?.label || value || "";
|
|
2516
2554
|
// Close on click outside
|
|
2517
2555
|
useEffect(() => {
|
|
2518
2556
|
const handleClickOutside = (event) => {
|
|
2519
|
-
if (containerRef.current &&
|
|
2557
|
+
if (containerRef.current &&
|
|
2558
|
+
!containerRef.current.contains(event.target)) {
|
|
2520
2559
|
setIsOpen(false);
|
|
2521
|
-
setSearchQuery(
|
|
2560
|
+
setSearchQuery("");
|
|
2522
2561
|
}
|
|
2523
2562
|
};
|
|
2524
2563
|
if (isOpen) {
|
|
2525
|
-
document.addEventListener(
|
|
2564
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2526
2565
|
}
|
|
2527
2566
|
return () => {
|
|
2528
|
-
document.removeEventListener(
|
|
2567
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
2529
2568
|
};
|
|
2530
2569
|
}, [isOpen]);
|
|
2531
2570
|
// Scroll highlighted option into view
|
|
@@ -2533,7 +2572,10 @@ const Combobox = forwardRef(({ value = '', onChange, options, onSearch, onCreate
|
|
|
2533
2572
|
if (isOpen && listRef.current) {
|
|
2534
2573
|
const highlightedElement = listRef.current.children[highlightedIndex];
|
|
2535
2574
|
if (highlightedElement) {
|
|
2536
|
-
highlightedElement.scrollIntoView({
|
|
2575
|
+
highlightedElement.scrollIntoView({
|
|
2576
|
+
block: "nearest",
|
|
2577
|
+
behavior: "smooth",
|
|
2578
|
+
});
|
|
2537
2579
|
}
|
|
2538
2580
|
}
|
|
2539
2581
|
}, [highlightedIndex, isOpen]);
|
|
@@ -2557,7 +2599,7 @@ const Combobox = forwardRef(({ value = '', onChange, options, onSearch, onCreate
|
|
|
2557
2599
|
if (option.disabled)
|
|
2558
2600
|
return;
|
|
2559
2601
|
onChange?.(option.value);
|
|
2560
|
-
setSearchQuery(
|
|
2602
|
+
setSearchQuery("");
|
|
2561
2603
|
setIsOpen(false);
|
|
2562
2604
|
inputRef.current?.blur();
|
|
2563
2605
|
};
|
|
@@ -2565,14 +2607,14 @@ const Combobox = forwardRef(({ value = '', onChange, options, onSearch, onCreate
|
|
|
2565
2607
|
const handleCreateOption = () => {
|
|
2566
2608
|
if (searchQuery.trim() && onCreateOption) {
|
|
2567
2609
|
onCreateOption(searchQuery.trim());
|
|
2568
|
-
setSearchQuery(
|
|
2610
|
+
setSearchQuery("");
|
|
2569
2611
|
setIsOpen(false);
|
|
2570
2612
|
}
|
|
2571
2613
|
};
|
|
2572
2614
|
// Handle clear
|
|
2573
2615
|
const handleClear = () => {
|
|
2574
|
-
onChange?.(
|
|
2575
|
-
setSearchQuery(
|
|
2616
|
+
onChange?.("");
|
|
2617
|
+
setSearchQuery("");
|
|
2576
2618
|
inputRef.current?.focus();
|
|
2577
2619
|
};
|
|
2578
2620
|
// Keyboard navigation
|
|
@@ -2580,22 +2622,22 @@ const Combobox = forwardRef(({ value = '', onChange, options, onSearch, onCreate
|
|
|
2580
2622
|
if (disabled)
|
|
2581
2623
|
return;
|
|
2582
2624
|
switch (e.key) {
|
|
2583
|
-
case
|
|
2625
|
+
case "ArrowDown":
|
|
2584
2626
|
e.preventDefault();
|
|
2585
2627
|
if (!isOpen) {
|
|
2586
2628
|
setIsOpen(true);
|
|
2587
2629
|
}
|
|
2588
2630
|
else {
|
|
2589
|
-
setHighlightedIndex(prev => prev < filteredOptions.length - 1 ? prev + 1 : prev);
|
|
2631
|
+
setHighlightedIndex((prev) => prev < filteredOptions.length - 1 ? prev + 1 : prev);
|
|
2590
2632
|
}
|
|
2591
2633
|
break;
|
|
2592
|
-
case
|
|
2634
|
+
case "ArrowUp":
|
|
2593
2635
|
e.preventDefault();
|
|
2594
2636
|
if (isOpen) {
|
|
2595
|
-
setHighlightedIndex(prev => (prev > 0 ? prev - 1 : 0));
|
|
2637
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0));
|
|
2596
2638
|
}
|
|
2597
2639
|
break;
|
|
2598
|
-
case
|
|
2640
|
+
case "Enter":
|
|
2599
2641
|
e.preventDefault();
|
|
2600
2642
|
if (isOpen && filteredOptions.length > 0) {
|
|
2601
2643
|
handleSelectOption(filteredOptions[highlightedIndex]);
|
|
@@ -2605,64 +2647,83 @@ const Combobox = forwardRef(({ value = '', onChange, options, onSearch, onCreate
|
|
|
2605
2647
|
setIsOpen(false);
|
|
2606
2648
|
}
|
|
2607
2649
|
break;
|
|
2608
|
-
case
|
|
2650
|
+
case "Escape":
|
|
2609
2651
|
e.preventDefault();
|
|
2610
2652
|
setIsOpen(false);
|
|
2611
|
-
setSearchQuery(
|
|
2653
|
+
setSearchQuery("");
|
|
2612
2654
|
break;
|
|
2613
|
-
case
|
|
2655
|
+
case "Tab":
|
|
2614
2656
|
setIsOpen(false);
|
|
2615
|
-
setSearchQuery(
|
|
2657
|
+
setSearchQuery("");
|
|
2616
2658
|
break;
|
|
2617
2659
|
}
|
|
2618
2660
|
};
|
|
2619
2661
|
// Size classes
|
|
2620
2662
|
const sizeClasses = {
|
|
2621
|
-
sm:
|
|
2622
|
-
md:
|
|
2623
|
-
lg:
|
|
2663
|
+
sm: "text-sm py-1.5 px-3",
|
|
2664
|
+
md: "text-sm py-2 px-3",
|
|
2665
|
+
lg: "text-base py-2.5 px-4",
|
|
2624
2666
|
};
|
|
2625
2667
|
const iconSizeClasses = {
|
|
2626
|
-
sm:
|
|
2627
|
-
md:
|
|
2628
|
-
lg:
|
|
2668
|
+
sm: "h-4 w-4",
|
|
2669
|
+
md: "h-4 w-4",
|
|
2670
|
+
lg: "h-5 w-5",
|
|
2629
2671
|
};
|
|
2630
2672
|
// Validation classes
|
|
2631
2673
|
const validationClasses = {
|
|
2632
|
-
error:
|
|
2633
|
-
success:
|
|
2634
|
-
warning:
|
|
2674
|
+
error: "border-error-500 focus:ring-error-500 focus:border-error-500",
|
|
2675
|
+
success: "border-success-500 focus:ring-success-500 focus:border-success-500",
|
|
2676
|
+
warning: "border-warning-500 focus:ring-warning-500 focus:border-warning-500",
|
|
2635
2677
|
};
|
|
2636
2678
|
const validationMessageColors = {
|
|
2637
|
-
error:
|
|
2638
|
-
success:
|
|
2639
|
-
warning:
|
|
2679
|
+
error: "text-error-600",
|
|
2680
|
+
success: "text-success-600",
|
|
2681
|
+
warning: "text-warning-600",
|
|
2640
2682
|
};
|
|
2641
2683
|
// Check if can create custom option
|
|
2642
2684
|
const canCreateOption = onCreateOption &&
|
|
2643
2685
|
searchQuery.trim() &&
|
|
2644
|
-
!filteredOptions.some(opt => opt.label.toLowerCase() === searchQuery.toLowerCase());
|
|
2645
|
-
return (jsxs("div", { className: `relative ${className}`, ref: containerRef, children: [label && (jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-700 mb-1", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsx("div", { className: "relative", children: jsxs("div", { className: "relative", children: [jsx("input", { ref: inputRef, type: "text", value: displayValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => setIsOpen(true),
|
|
2686
|
+
!filteredOptions.some((opt) => opt.label.toLowerCase() === searchQuery.toLowerCase());
|
|
2687
|
+
return (jsxs("div", { className: `relative ${className}`, ref: containerRef, children: [label && (jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-700 mb-1", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsx("div", { className: "relative", children: jsxs("div", { className: "relative", children: [jsx("input", { ref: inputRef, type: "text", value: displayValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => setIsOpen(true), onMouseDown: (e) => {
|
|
2688
|
+
// Toggle close on click when the input is already focused + open.
|
|
2689
|
+
// Without this, the second click is a no-op because onFocus only
|
|
2690
|
+
// fires on focus transition.
|
|
2691
|
+
if (isOpen && document.activeElement === inputRef.current) {
|
|
2692
|
+
e.preventDefault();
|
|
2693
|
+
setIsOpen(false);
|
|
2694
|
+
}
|
|
2695
|
+
}, placeholder: placeholder, disabled: disabled, className: `
|
|
2646
2696
|
w-full rounded-md border bg-white
|
|
2647
2697
|
${sizeClasses[size]}
|
|
2648
|
-
${validationState ? validationClasses[validationState] :
|
|
2649
|
-
${disabled ?
|
|
2698
|
+
${validationState ? validationClasses[validationState] : "border-paper-300 focus:ring-primary-500 focus:border-primary-500"}
|
|
2699
|
+
${disabled ? "bg-paper-100 text-ink-400 cursor-not-allowed" : ""}
|
|
2650
2700
|
focus:outline-none focus:ring-2
|
|
2651
2701
|
pr-20
|
|
2652
|
-
`, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ?
|
|
2702
|
+
`, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? "Combobox" : undefined, "aria-expanded": isOpen, "aria-autocomplete": "list", "aria-controls": listboxId, "aria-activedescendant": isOpen && filteredOptions.length > 0
|
|
2703
|
+
? `option-${highlightedIndex}`
|
|
2704
|
+
: undefined, "aria-invalid": validationState === "error" ? "true" : undefined, "aria-describedby": validationMessage ? descriptionId : undefined, "aria-required": required, role: "combobox" }), jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-2 gap-1", children: [loading && (jsx("div", { className: "animate-spin", children: jsx(Search, { className: `${iconSizeClasses[size]} text-ink-400` }) })), !loading && value && !disabled && (jsx("button", { type: "button", onClick: handleClear, className: "p-0.5 text-ink-400 hover:text-ink-600 focus:outline-none", "aria-label": "Clear", tabIndex: -1, children: jsx(X, { className: iconSizeClasses[size] }) })), !loading && !disabled && (jsx("button", { type: "button", onMouseDown: (e) => {
|
|
2705
|
+
// preventDefault keeps the input from losing focus on
|
|
2706
|
+
// mousedown so the toggle stays smooth. Manually re-focus
|
|
2707
|
+
// when opening so keyboard nav works immediately.
|
|
2708
|
+
e.preventDefault();
|
|
2709
|
+
setIsOpen((o) => !o);
|
|
2710
|
+
if (!isOpen) {
|
|
2711
|
+
inputRef.current?.focus();
|
|
2712
|
+
}
|
|
2713
|
+
}, className: "p-0.5 text-ink-400 hover:text-ink-600 focus:outline-none", "aria-label": isOpen ? "Close options" : "Open options", tabIndex: -1, children: jsx(ChevronDown, { className: `${iconSizeClasses[size]} transition-transform ${isOpen ? "rotate-180" : ""}` }) })), !loading && disabled && (jsx(ChevronDown, { className: `${iconSizeClasses[size]} text-ink-400 transition-transform` }))] })] }) }), validationMessage && (jsx("p", { id: descriptionId, className: `mt-1 text-xs ${validationState ? validationMessageColors[validationState] : "text-ink-500"}`, role: "alert", "aria-live": "polite", children: validationMessage })), helperText && !validationMessage && (jsx("p", { className: "mt-1 text-xs text-ink-500", children: helperText })), isOpen && (jsx("div", { className: "absolute z-50 mt-1 w-full bg-white rounded-md shadow-lg border border-paper-200 max-h-60 overflow-auto", role: "listbox", id: listboxId, "aria-label": "Available options", children: loading ? (jsx("div", { className: "px-4 py-8 text-center text-ink-500 text-sm", role: "status", "aria-live": "polite", children: "Loading..." })) : filteredOptions.length === 0 && !canCreateOption ? (jsx("div", { className: "px-4 py-8 text-center text-ink-500 text-sm", role: "status", "aria-live": "polite", children: "No options found" })) : (jsxs("ul", { ref: listRef, children: [filteredOptions.map((option, index) => {
|
|
2653
2714
|
const Icon = option.icon;
|
|
2654
2715
|
const isSelected = option.value === value;
|
|
2655
2716
|
const isHighlighted = index === highlightedIndex;
|
|
2656
2717
|
return (jsxs("li", { id: `option-${index}`, role: "option", "aria-selected": isSelected, "aria-disabled": option.disabled, onClick: () => handleSelectOption(option), onMouseEnter: () => setHighlightedIndex(index), className: `
|
|
2657
2718
|
px-3 py-2 cursor-pointer flex items-center justify-between gap-2
|
|
2658
|
-
${option.disabled ?
|
|
2659
|
-
${isHighlighted ?
|
|
2660
|
-
${isSelected ?
|
|
2719
|
+
${option.disabled ? "opacity-50 cursor-not-allowed" : ""}
|
|
2720
|
+
${isHighlighted ? "bg-primary-50" : ""}
|
|
2721
|
+
${isSelected ? "bg-primary-100 font-medium" : ""}
|
|
2661
2722
|
hover:bg-primary-50
|
|
2662
|
-
`, children: [jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [Icon && jsx(Icon, { className: `${iconSizeClasses[size]} flex-shrink-0 text-ink-600` }), jsx("span", { className: "truncate text-sm text-ink-900", children: option.label })] }), isSelected && (jsx(Check, { className: `${iconSizeClasses[size]} flex-shrink-0 text-primary-600` }))] }, option.value));
|
|
2723
|
+
`, children: [jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [Icon && (jsx(Icon, { className: `${iconSizeClasses[size]} flex-shrink-0 text-ink-600` })), jsx("span", { className: "truncate text-sm text-ink-900", children: option.label })] }), isSelected && (jsx(Check, { className: `${iconSizeClasses[size]} flex-shrink-0 text-primary-600` }))] }, option.value));
|
|
2663
2724
|
}), canCreateOption && (jsxs("li", { role: "option", onClick: handleCreateOption, className: "px-3 py-2 cursor-pointer flex items-center gap-2 border-t border-paper-200 hover:bg-primary-50 bg-success-50", children: [jsx(Plus, { className: `${iconSizeClasses[size]} text-success-600` }), jsxs("span", { className: "text-sm text-success-700 font-medium", children: ["Create \"", searchQuery, "\""] })] }))] })) }))] }));
|
|
2664
2725
|
});
|
|
2665
|
-
Combobox.displayName =
|
|
2726
|
+
Combobox.displayName = "Combobox";
|
|
2666
2727
|
|
|
2667
2728
|
/**
|
|
2668
2729
|
* FormControl wrapper component for consistent form field layout.
|