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