infinity-ui-elements 1.8.2 → 1.8.4

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.esm.js CHANGED
@@ -5,7 +5,7 @@ import { Slot } from '@radix-ui/react-slot';
5
5
  import { PulseLoader, ClipLoader } from 'react-spinners';
6
6
  import { clsx } from 'clsx';
7
7
  import { twMerge } from 'tailwind-merge';
8
- import { ExternalLink, Loader2, Search, X, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
8
+ import { ExternalLink, Loader2, Search, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
9
9
  import { createPortal } from 'react-dom';
10
10
  import { flexRender } from '@tanstack/react-table';
11
11
 
@@ -1868,10 +1868,10 @@ const DropdownMenu = React.forwardRef(({ items = [], customContent, sectionHeadi
1868
1868
  if (isEmpty || items.length === 0) {
1869
1869
  return (jsxs("div", { className: "flex flex-col items-center justify-center py-8 px-6 text-center", children: [emptyIcon || (jsx(Search, { className: "w-12 h-12 text-surface-ink-neutral-muted mb-4" })), jsx(Text, { as: "h3", variant: "body", size: "small", weight: "semibold", className: "text-surface-ink-neutral-normal mb-2", children: emptyTitle }), jsx(Text, { as: "p", variant: "body", size: "small", weight: "regular", className: "text-surface-ink-neutral-muted mb-3", children: emptyDescription }), emptyLinkText && (jsx(Link, { type: "anchor", color: "primary", size: "small", onClick: onEmptyLinkClick, children: emptyLinkText }))] }));
1870
1870
  }
1871
- return (jsxs("div", { className: "py-3 px-3 max-h-[400px] overflow-y-auto", children: [sectionHeading && (jsx(Text, { as: "div", variant: "body", size: "small", weight: "medium", className: "text-surface-ink-neutral-muted px-3 py-2 mb-1", children: sectionHeading })), jsx("div", { className: "flex flex-col gap-1", children: items.map((item, index) => (jsx(ListItem, { title: item.title, description: item.description, leadingIcon: item.leadingIcon, trailingIcon: item.trailingIcon, showChevron: showChevron, isDisabled: item.isDisabled, isSelected: index === focusedIndex, onClick: () => {
1871
+ return (jsxs("div", { className: "py-3 px-3 max-h-[400px] overflow-y-auto", children: [sectionHeading && (jsx(Text, { as: "div", variant: "body", size: "small", weight: "medium", className: "text-surface-ink-neutral-muted px-3 py-2 mb-1", children: sectionHeading })), jsx("div", { className: "flex flex-col gap-1", children: items.map((item, index) => (jsx(ListItem, { title: item.label, description: item.description, leadingIcon: item.leadingIcon, trailingIcon: item.trailingIcon, showChevron: showChevron, isDisabled: item.isDisabled, isSelected: index === focusedIndex, variant: item.variant, onClick: () => {
1872
1872
  item.onClick?.();
1873
1873
  onClose?.();
1874
- }, containerClassName: cn(index === focusedIndex && "bg-action-fill-primary-faded") }, item.id))) })] }));
1874
+ }, containerClassName: cn(index === focusedIndex && "bg-action-fill-primary-faded") }, item.value))) })] }));
1875
1875
  };
1876
1876
  const widthClass = width === "full" ? "w-full" : width === "auto" ? "w-auto" : "";
1877
1877
  const footerVisible = showFooter ?? !disableFooter;
@@ -2213,7 +2213,7 @@ const Modal = React.forwardRef(({ isOpen, onClose, title, description, footer, c
2213
2213
  };
2214
2214
  // Handle escape key
2215
2215
  React.useEffect(() => {
2216
- if (!isOpen || !closeOnEscape)
2216
+ if (!isOpen || !closeOnEscape || !onClose)
2217
2217
  return;
2218
2218
  const handleEscape = (e) => {
2219
2219
  if (e.key === "Escape") {
@@ -2237,7 +2237,7 @@ const Modal = React.forwardRef(({ isOpen, onClose, title, description, footer, c
2237
2237
  }, [isOpen]);
2238
2238
  // Handle overlay click
2239
2239
  const handleOverlayClick = (e) => {
2240
- if (closeOnOverlayClick && e.target === e.currentTarget) {
2240
+ if (closeOnOverlayClick && e.target === e.currentTarget && onClose) {
2241
2241
  onClose();
2242
2242
  }
2243
2243
  };
@@ -2245,7 +2245,7 @@ const Modal = React.forwardRef(({ isOpen, onClose, title, description, footer, c
2245
2245
  if (!isOpen)
2246
2246
  return null;
2247
2247
  const hasHeader = title || description;
2248
- return (jsxs("div", { className: cn("fixed inset-0 z-50 flex items-center justify-center p-4", className), role: "dialog", "aria-modal": "true", "aria-label": ariaLabel || title, "aria-describedby": ariaDescribedBy, children: [jsx("div", { className: cn("absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity", overlayClassName), onClick: handleOverlayClick, "aria-hidden": "true" }), jsxs("div", { ref: contentRef, className: cn("relative w-full bg-white rounded-large shadow-xl transition-all", "flex flex-col max-h-[90vh]", sizeConfig[size], contentClassName), children: [hasHeader && (jsxs("div", { className: cn("flex items-start justify-between gap-4 px-6 pt-6", !description && "pb-4", description && "pb-2", headerClassName), children: [jsxs("div", { className: "flex-1", children: [title && (jsx(Text, { as: "h2", variant: "body", size: "large", weight: "semibold", color: "default", children: title })), description && (jsx(Text, { as: "p", variant: "body", size: "small", weight: "regular", color: "subtle", className: "mt-1", children: description }))] }), showCloseButton && (jsx("button", { type: "button", onClick: onClose, className: cn("shrink-0 rounded-medium p-1.5 transition-colors", "text-surface-ink-neutral-muted hover:text-surface-ink-neutral-default", "hover:bg-surface-fill-neutral-faded focus:outline-none focus:ring-2", "focus:ring-action-outline-primary-default focus:ring-offset-2"), "aria-label": "Close modal", children: jsx(X, { className: "h-5 w-5" }) }))] })), !hasHeader && showCloseButton && (jsx("div", { className: "absolute top-4 right-4 z-10", children: jsx("button", { type: "button", onClick: onClose, className: cn("shrink-0 rounded-medium p-1.5 transition-colors", "text-surface-ink-neutral-muted hover:text-surface-ink-neutral-default", "hover:bg-surface-fill-neutral-faded focus:outline-none focus:ring-2", "focus:ring-action-outline-primary-default focus:ring-offset-2"), "aria-label": "Close modal", children: jsx(X, { className: "h-5 w-5" }) }) })), jsx("div", { className: cn("flex-1 overflow-y-auto px-6", hasHeader ? "py-4" : "pt-6 pb-4", !footer && "pb-6", bodyClassName), children: children }), footer && (jsxs("div", { className: "flex flex-col", children: [jsx(Divider, { thickness: "thin", variant: "muted" }), jsx("div", { className: cn("flex items-center justify-end gap-3 px-6 py-4", footerClassName), children: footer })] }))] })] }));
2248
+ return (jsxs("div", { className: cn("fixed inset-0 z-50 flex items-center justify-center p-4", className), role: "dialog", "aria-modal": "true", "aria-label": ariaLabel || title, "aria-describedby": ariaDescribedBy, children: [jsx("div", { className: cn("absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity", overlayClassName), onClick: handleOverlayClick, "aria-hidden": "true" }), jsxs("div", { ref: contentRef, className: cn("relative w-full bg-white rounded-large shadow-xl transition-all", "flex flex-col max-h-[90vh]", sizeConfig[size], contentClassName), children: [hasHeader && (jsxs("div", { className: cn("flex items-start justify-between gap-4 px-6 pt-6", !description && "pb-4", description && "pb-2", headerClassName), children: [jsxs("div", { className: "flex-1", children: [title && (jsx(Text, { as: "h2", variant: "body", size: "large", weight: "semibold", color: "default", children: title })), description && (jsx(Text, { as: "p", variant: "body", size: "small", weight: "regular", color: "subtle", className: "mt-1", children: description }))] }), showCloseButton && onClose && (jsx(IconButton, { icon: "close", onClick: onClose, color: "neutral", size: "small", "aria-label": "Close modal", className: "shrink-0" }))] })), !hasHeader && showCloseButton && onClose && (jsx("div", { className: "absolute top-4 right-4 z-10", children: jsx(IconButton, { icon: "close", onClick: onClose, color: "neutral", size: "small", "aria-label": "Close modal" }) })), jsx("div", { className: cn("flex-1 overflow-y-auto px-6", hasHeader ? "py-4" : "pt-6 pb-4", !footer && "pb-6", bodyClassName), children: children }), footer && (jsxs("div", { className: "flex flex-col", children: [jsx(Divider, { thickness: "thin", variant: "muted" }), jsx("div", { className: cn("flex items-center justify-end gap-3 px-6 py-4", footerClassName), children: footer })] }))] })] }));
2249
2249
  });
2250
2250
  Modal.displayName = "Modal";
2251
2251
 
@@ -2885,10 +2885,10 @@ TextField.displayName = "TextField";
2885
2885
 
2886
2886
  const defaultFilter = (item, query) => {
2887
2887
  const searchQuery = query.toLowerCase();
2888
- return (item.title.toLowerCase().includes(searchQuery) ||
2888
+ return (item.label.toLowerCase().includes(searchQuery) ||
2889
2889
  (item.description?.toLowerCase().includes(searchQuery) ?? false));
2890
2890
  };
2891
- const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHeading, isLoading = false, emptyTitle = "No Search Results Found", emptyDescription = "Add description of what the user can search for here.", emptyLinkText = "Link to support site", onEmptyLinkClick, primaryButtonText = "Primary", secondaryButtonText = "Secondary", onPrimaryClick, onSecondaryClick, dropdownWidth = "full", showChevron = false, emptyIcon, disableFooter = false, footerLayout = "horizontal", onSearchChange, onItemSelect, filterFunction = defaultFilter, searchValue: controlledSearchValue, defaultSearchValue = "", dropdownClassName, minSearchLength = 0, showOnFocus = true, containerClassName, ...textFieldProps }, ref) => {
2891
+ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHeading, isLoading = false, emptyTitle = "No Search Results Found", emptyDescription = "Add description of what the user can search for here.", emptyLinkText = "Link to support site", onEmptyLinkClick, primaryButtonText, secondaryButtonText, onPrimaryClick, onSecondaryClick, dropdownWidth = "full", showChevron = false, emptyIcon, disableFooter = false, footerLayout = "horizontal", onSearchChange, onItemSelect, filterFunction = defaultFilter, searchValue: controlledSearchValue, defaultSearchValue = "", dropdownClassName, minSearchLength = 0, showOnFocus = true, showAddNew = false, containerClassName, ...textFieldProps }, ref) => {
2892
2892
  const [uncontrolledSearchValue, setUncontrolledSearchValue] = React.useState(defaultSearchValue);
2893
2893
  const [isOpen, setIsOpen] = React.useState(false);
2894
2894
  const [focusedIndex, setFocusedIndex] = React.useState(-1);
@@ -2937,7 +2937,7 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
2937
2937
  const handleItemSelect = (item) => {
2938
2938
  onItemSelect?.(item);
2939
2939
  if (controlledSearchValue === undefined) {
2940
- setUncontrolledSearchValue(item.title);
2940
+ setUncontrolledSearchValue(item.label);
2941
2941
  }
2942
2942
  setIsOpen(false);
2943
2943
  inputRef.current?.focus();
@@ -2948,6 +2948,40 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
2948
2948
  return items;
2949
2949
  return items.filter((item) => filterFunction(item, searchValue));
2950
2950
  }, [items, searchValue, filterFunction]);
2951
+ // Add "Add New" option if showAddNew is true and no items match
2952
+ const itemsWithAddNew = React.useMemo(() => {
2953
+ if (!showAddNew || !searchValue || filteredItems.length > 0) {
2954
+ return filteredItems;
2955
+ }
2956
+ const addNewItem = {
2957
+ value: searchValue,
2958
+ label: `+ Add ${searchValue}`,
2959
+ variant: "primary",
2960
+ onClick: () => {
2961
+ const newItem = {
2962
+ value: searchValue,
2963
+ label: searchValue,
2964
+ };
2965
+ onItemSelect?.(newItem);
2966
+ if (controlledSearchValue === undefined) {
2967
+ setUncontrolledSearchValue(searchValue);
2968
+ }
2969
+ setIsOpen(false);
2970
+ inputRef.current?.focus();
2971
+ },
2972
+ };
2973
+ return [addNewItem];
2974
+ }, [
2975
+ showAddNew,
2976
+ searchValue,
2977
+ filteredItems,
2978
+ onItemSelect,
2979
+ controlledSearchValue,
2980
+ ]);
2981
+ // Reset focused index when items change
2982
+ React.useEffect(() => {
2983
+ setFocusedIndex(-1);
2984
+ }, [itemsWithAddNew.length]);
2951
2985
  // Close dropdown when clicking outside
2952
2986
  React.useEffect(() => {
2953
2987
  const handleClickOutside = (event) => {
@@ -2977,7 +3011,7 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
2977
3011
  switch (e.key) {
2978
3012
  case "ArrowDown":
2979
3013
  e.preventDefault();
2980
- setFocusedIndex((prev) => prev < filteredItems.length - 1 ? prev + 1 : prev);
3014
+ setFocusedIndex((prev) => prev < itemsWithAddNew.length - 1 ? prev + 1 : prev);
2981
3015
  break;
2982
3016
  case "ArrowUp":
2983
3017
  e.preventDefault();
@@ -2985,8 +3019,8 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
2985
3019
  break;
2986
3020
  case "Enter":
2987
3021
  e.preventDefault();
2988
- if (focusedIndex >= 0 && filteredItems[focusedIndex]) {
2989
- handleItemSelect(filteredItems[focusedIndex]);
3022
+ if (focusedIndex >= 0 && itemsWithAddNew[focusedIndex]) {
3023
+ handleItemSelect(itemsWithAddNew[focusedIndex]);
2990
3024
  }
2991
3025
  break;
2992
3026
  case "Escape":
@@ -2997,9 +3031,10 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
2997
3031
  }
2998
3032
  };
2999
3033
  // Update items with onClick handlers that call handleItemSelect
3000
- const itemsWithHandlers = filteredItems.map((item) => ({
3034
+ // Only add onClick if it doesn't already exist (for Add New items)
3035
+ const itemsWithHandlers = itemsWithAddNew.map((item) => ({
3001
3036
  ...item,
3002
- onClick: () => handleItemSelect(item),
3037
+ onClick: item.onClick || (() => handleItemSelect(item)),
3003
3038
  }));
3004
3039
  const showDropdown = isOpen && searchValue.length >= minSearchLength;
3005
3040
  const dropdownMenu = showDropdown && (jsx("div", { ref: menuRef, style: {
@@ -3008,7 +3043,9 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
3008
3043
  left: `${position.left}px`,
3009
3044
  width: dropdownWidth === "full" ? `${position.width}px` : "auto",
3010
3045
  zIndex: 9999,
3011
- }, children: jsx(DropdownMenu, { items: itemsWithHandlers, sectionHeading: sectionHeading, isLoading: isLoading, isEmpty: filteredItems.length === 0, emptyTitle: emptyTitle, emptyDescription: emptyDescription, emptyLinkText: emptyLinkText, onEmptyLinkClick: onEmptyLinkClick, primaryButtonText: primaryButtonText, secondaryButtonText: secondaryButtonText, onPrimaryClick: onPrimaryClick, onSecondaryClick: onSecondaryClick, showChevron: showChevron, emptyIcon: emptyIcon, disableFooter: disableFooter, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: dropdownWidth === "full" ? "full" : "auto" }) }));
3046
+ }, children: jsx(DropdownMenu, { items: itemsWithHandlers, sectionHeading: sectionHeading, isLoading: isLoading, isEmpty: itemsWithAddNew.length === 0 && !showAddNew, emptyTitle: emptyTitle, emptyDescription: emptyDescription, emptyLinkText: emptyLinkText, onEmptyLinkClick: onEmptyLinkClick, primaryButtonText: primaryButtonText, secondaryButtonText: secondaryButtonText, onPrimaryClick: onPrimaryClick, onSecondaryClick: onSecondaryClick, showChevron: showChevron, emptyIcon: emptyIcon, disableFooter: disableFooter, showFooter: (primaryButtonText || secondaryButtonText) && !disableFooter
3047
+ ? true
3048
+ : false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: dropdownWidth === "full" ? "full" : "auto" }) }));
3012
3049
  return (jsxs(Fragment, { children: [jsx("div", { ref: dropdownRef, className: cn("relative", containerClassName), children: jsx(TextField, { ref: inputRef, value: searchValue, onChange: handleSearchChange, onFocus: handleFocus, onKeyDown: handleKeyDown, containerClassName: "mb-0", ...textFieldProps }) }), typeof document !== "undefined" &&
3013
3050
  dropdownMenu &&
3014
3051
  createPortal(dropdownMenu, document.body)] }));