infinity-ui-elements 1.14.1 → 1.14.3

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
@@ -1410,13 +1410,102 @@ const Divider = React.forwardRef(({ className, orientation = "horizontal", thick
1410
1410
  });
1411
1411
  Divider.displayName = "Divider";
1412
1412
 
1413
- const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, footer, children, variant = "default", showCloseButton = true, showDragHandle = true, closeOnOverlayClick = true, closeOnEscape = true, closeOnSwipeDown = true, maxHeight = "90vh", className, contentClassName, headerClassName, bodyClassName, footerClassName, overlayClassName, ariaLabel, ariaDescribedBy, }, ref) => {
1413
+ // Detect iOS
1414
+ const isIOS = () => {
1415
+ if (typeof window === "undefined" || typeof navigator === "undefined")
1416
+ return false;
1417
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
1418
+ (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
1419
+ };
1420
+ // Detect Android
1421
+ const isAndroid = () => {
1422
+ if (typeof navigator === "undefined")
1423
+ return false;
1424
+ return /Android/.test(navigator.userAgent);
1425
+ };
1426
+ // Get visual viewport height (accounts for keyboard)
1427
+ const getVisualViewportHeight = () => {
1428
+ if (typeof window === "undefined")
1429
+ return 0;
1430
+ // Use visualViewport API if available (better keyboard detection)
1431
+ if (window.visualViewport) {
1432
+ return window.visualViewport.height;
1433
+ }
1434
+ return window.innerHeight;
1435
+ };
1436
+ // Get safe area inset bottom (for devices with home indicator)
1437
+ const getSafeAreaInsetBottom = () => {
1438
+ if (typeof window === "undefined" || typeof document === "undefined")
1439
+ return 0;
1440
+ const root = document.documentElement;
1441
+ const safeAreaInset = getComputedStyle(root).getPropertyValue("--safe-area-inset-bottom");
1442
+ if (safeAreaInset) {
1443
+ return parseInt(safeAreaInset, 10) || 0;
1444
+ }
1445
+ // Fallback: try to read env() value
1446
+ const testDiv = document.createElement("div");
1447
+ testDiv.style.paddingBottom = "env(safe-area-inset-bottom, 0px)";
1448
+ document.body.appendChild(testDiv);
1449
+ const paddingBottom = parseInt(getComputedStyle(testDiv).paddingBottom, 10) || 0;
1450
+ document.body.removeChild(testDiv);
1451
+ return paddingBottom;
1452
+ };
1453
+ const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, footer, children, variant = "default", showCloseButton = true, showDragHandle = true, closeOnOverlayClick = true, closeOnEscape = true, closeOnSwipeDown = true, maxHeight = "90vh", className, contentClassName, headerClassName, bodyClassName, footerClassName, overlayClassName, ariaLabel, ariaDescribedBy, adjustForKeyboard = true, scrollToFocusedInput = true, stickyContent, stickyContentClassName, }, ref) => {
1414
1454
  const sheetRef = React.useRef(null);
1415
1455
  const contentRef = ref || sheetRef;
1416
1456
  const bodyRef = React.useRef(null);
1457
+ const dragHandleRef = React.useRef(null);
1458
+ const overlayRef = React.useRef(null);
1417
1459
  const [isClosing, setIsClosing] = React.useState(false);
1418
1460
  const [touchStart, setTouchStart] = React.useState(null);
1419
1461
  const [touchEnd, setTouchEnd] = React.useState(null);
1462
+ const [touchStartX, setTouchStartX] = React.useState(null);
1463
+ const [isSwipeGesture, setIsSwipeGesture] = React.useState(false);
1464
+ const [keyboardHeight, setKeyboardHeight] = React.useState(0);
1465
+ const [viewportHeight, setViewportHeight] = React.useState(typeof window !== "undefined" ? window.innerHeight : 0);
1466
+ const [safeAreaBottom, setSafeAreaBottom] = React.useState(0);
1467
+ // Track if user started dragging from the drag handle
1468
+ const isDraggingFromHandle = React.useRef(false);
1469
+ // Store original body styles for restoration
1470
+ const originalBodyStyles = React.useRef(null);
1471
+ // Track scroll position before locking
1472
+ const scrollPosition = React.useRef(0);
1473
+ // Initialize safe area inset
1474
+ React.useEffect(() => {
1475
+ setSafeAreaBottom(getSafeAreaInsetBottom());
1476
+ }, []);
1477
+ // Handle Visual Viewport API for keyboard detection
1478
+ React.useEffect(() => {
1479
+ if (!isOpen || !adjustForKeyboard)
1480
+ return;
1481
+ if (typeof window === "undefined")
1482
+ return;
1483
+ const updateViewport = () => {
1484
+ const newHeight = getVisualViewportHeight();
1485
+ const fullHeight = window.innerHeight;
1486
+ const newKeyboardHeight = Math.max(0, fullHeight - newHeight);
1487
+ setViewportHeight(newHeight);
1488
+ setKeyboardHeight(newKeyboardHeight);
1489
+ };
1490
+ // Use visualViewport API if available
1491
+ if (window.visualViewport) {
1492
+ window.visualViewport.addEventListener("resize", updateViewport);
1493
+ window.visualViewport.addEventListener("scroll", updateViewport);
1494
+ updateViewport();
1495
+ return () => {
1496
+ window.visualViewport?.removeEventListener("resize", updateViewport);
1497
+ window.visualViewport?.removeEventListener("scroll", updateViewport);
1498
+ };
1499
+ }
1500
+ else {
1501
+ // Fallback for browsers without visualViewport API
1502
+ window.addEventListener("resize", updateViewport);
1503
+ updateViewport();
1504
+ return () => {
1505
+ window.removeEventListener("resize", updateViewport);
1506
+ };
1507
+ }
1508
+ }, [isOpen, adjustForKeyboard]);
1420
1509
  // Handle escape key
1421
1510
  React.useEffect(() => {
1422
1511
  if (!isOpen || !closeOnEscape || !onClose)
@@ -1429,80 +1518,245 @@ const BottomSheet = React.forwardRef(({ isOpen, onClose, title, description, foo
1429
1518
  document.addEventListener("keydown", handleEscape);
1430
1519
  return () => document.removeEventListener("keydown", handleEscape);
1431
1520
  }, [isOpen, closeOnEscape, onClose]);
1432
- // Prevent body scroll when bottom sheet is open
1521
+ // Robust body scroll lock for iOS and Android
1433
1522
  React.useEffect(() => {
1434
- if (isOpen) {
1435
- document.body.style.overflow = "hidden";
1436
- }
1437
- else {
1438
- document.body.style.overflow = "";
1439
- }
1523
+ if (!isOpen)
1524
+ return;
1525
+ const iOS = isIOS();
1526
+ // Store current scroll position
1527
+ scrollPosition.current = window.pageYOffset || document.documentElement.scrollTop;
1528
+ // Store original styles
1529
+ originalBodyStyles.current = {
1530
+ overflow: document.body.style.overflow,
1531
+ position: document.body.style.position,
1532
+ top: document.body.style.top,
1533
+ left: document.body.style.left,
1534
+ right: document.body.style.right,
1535
+ width: document.body.style.width,
1536
+ height: document.body.style.height,
1537
+ };
1538
+ // Apply scroll lock
1539
+ document.body.style.overflow = "hidden";
1540
+ // iOS requires position fixed to prevent background scroll
1541
+ if (iOS) {
1542
+ document.body.style.position = "fixed";
1543
+ document.body.style.top = `-${scrollPosition.current}px`;
1544
+ document.body.style.left = "0";
1545
+ document.body.style.right = "0";
1546
+ document.body.style.width = "100%";
1547
+ }
1548
+ // Prevent touchmove on overlay to stop iOS rubber-banding
1549
+ const preventTouchMove = (e) => {
1550
+ const target = e.target;
1551
+ // Allow scrolling inside the body content
1552
+ if (bodyRef.current?.contains(target)) {
1553
+ const scrollTop = bodyRef.current.scrollTop;
1554
+ const scrollHeight = bodyRef.current.scrollHeight;
1555
+ const clientHeight = bodyRef.current.clientHeight;
1556
+ // Check if at scroll boundaries
1557
+ const isAtTop = scrollTop <= 0;
1558
+ const isAtBottom = scrollTop + clientHeight >= scrollHeight;
1559
+ // Only prevent default if at boundaries and trying to scroll further
1560
+ if ((isAtTop && e.touches[0].clientY > (touchStart || 0)) ||
1561
+ (isAtBottom && e.touches[0].clientY < (touchStart || 0))) {
1562
+ // Allow the swipe-to-close gesture
1563
+ if (!isDraggingFromHandle.current) {
1564
+ e.preventDefault();
1565
+ }
1566
+ }
1567
+ return;
1568
+ }
1569
+ // Prevent scroll on overlay and other areas
1570
+ e.preventDefault();
1571
+ };
1572
+ document.addEventListener("touchmove", preventTouchMove, { passive: false });
1440
1573
  return () => {
1441
- document.body.style.overflow = "";
1574
+ document.removeEventListener("touchmove", preventTouchMove);
1575
+ // Restore original styles
1576
+ if (originalBodyStyles.current) {
1577
+ document.body.style.overflow = originalBodyStyles.current.overflow;
1578
+ document.body.style.position = originalBodyStyles.current.position;
1579
+ document.body.style.top = originalBodyStyles.current.top;
1580
+ document.body.style.left = originalBodyStyles.current.left;
1581
+ document.body.style.right = originalBodyStyles.current.right;
1582
+ document.body.style.width = originalBodyStyles.current.width;
1583
+ }
1584
+ // Restore scroll position for iOS
1585
+ if (iOS) {
1586
+ window.scrollTo(0, scrollPosition.current);
1587
+ }
1442
1588
  };
1443
1589
  }, [isOpen]);
1444
1590
  // Reset closing state when opening
1445
1591
  React.useEffect(() => {
1446
1592
  if (isOpen) {
1447
1593
  setIsClosing(false);
1594
+ setKeyboardHeight(0);
1448
1595
  }
1449
1596
  }, [isOpen]);
1450
- // Reset scroll position when content changes (e.g., after filtering)
1597
+ // Scroll focused input into view when keyboard opens
1451
1598
  React.useEffect(() => {
1452
- if (isOpen && bodyRef.current) {
1453
- // Use requestAnimationFrame to ensure DOM has updated
1454
- requestAnimationFrame(() => {
1455
- if (bodyRef.current) {
1456
- bodyRef.current.scrollTop = 0;
1457
- }
1458
- });
1459
- }
1599
+ if (!isOpen || !scrollToFocusedInput || !adjustForKeyboard)
1600
+ return;
1601
+ const handleFocusIn = (e) => {
1602
+ const target = e.target;
1603
+ if (!target || !bodyRef.current)
1604
+ return;
1605
+ // Check if target is an input/textarea inside the body
1606
+ if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
1607
+ bodyRef.current.contains(target)) {
1608
+ // Wait for keyboard to open
1609
+ setTimeout(() => {
1610
+ // Scroll the input into view within the body container
1611
+ const targetRect = target.getBoundingClientRect();
1612
+ const bodyRect = bodyRef.current?.getBoundingClientRect();
1613
+ if (!bodyRect)
1614
+ return;
1615
+ // Calculate if input is below visible area (considering keyboard)
1616
+ const visibleBottom = viewportHeight - keyboardHeight - 20; // 20px buffer
1617
+ if (targetRect.bottom > visibleBottom) {
1618
+ // Scroll to bring input into view
1619
+ const scrollAmount = targetRect.bottom - visibleBottom + 40;
1620
+ bodyRef.current?.scrollBy({
1621
+ top: scrollAmount,
1622
+ behavior: "smooth",
1623
+ });
1624
+ }
1625
+ }, 100);
1626
+ }
1627
+ };
1628
+ document.addEventListener("focusin", handleFocusIn);
1629
+ return () => document.removeEventListener("focusin", handleFocusIn);
1630
+ }, [isOpen, scrollToFocusedInput, adjustForKeyboard, viewportHeight, keyboardHeight]);
1631
+ // Reset scroll to top when content changes (for filtered results)
1632
+ React.useEffect(() => {
1633
+ if (!isOpen || !bodyRef.current)
1634
+ return;
1635
+ // Use requestAnimationFrame to ensure DOM has updated
1636
+ requestAnimationFrame(() => {
1637
+ if (bodyRef.current) {
1638
+ bodyRef.current.scrollTop = 0;
1639
+ }
1640
+ });
1460
1641
  }, [children, isOpen]);
1461
1642
  // Handle close with animation
1462
- const handleClose = () => {
1643
+ const handleClose = React.useCallback(() => {
1463
1644
  if (!onClose)
1464
1645
  return;
1646
+ // Blur any focused input to dismiss keyboard
1647
+ if (document.activeElement instanceof HTMLElement) {
1648
+ document.activeElement.blur();
1649
+ }
1465
1650
  setIsClosing(true);
1466
1651
  // Wait for animation to complete before calling onClose
1467
1652
  setTimeout(() => {
1468
1653
  onClose();
1469
1654
  setIsClosing(false);
1470
1655
  }, 300); // Match animation duration
1471
- };
1656
+ }, [onClose]);
1472
1657
  // Handle overlay click
1473
1658
  const handleOverlayClick = (e) => {
1474
1659
  if (closeOnOverlayClick && e.target === e.currentTarget) {
1475
1660
  handleClose();
1476
1661
  }
1477
1662
  };
1478
- // Handle touch events for swipe down
1663
+ // Touch handling for swipe-to-close (only from drag handle or when scrolled to top)
1479
1664
  const handleTouchStart = (e) => {
1480
1665
  if (!closeOnSwipeDown)
1481
1666
  return;
1667
+ const touch = e.targetTouches[0];
1482
1668
  setTouchEnd(null);
1483
- setTouchStart(e.targetTouches[0].clientY);
1669
+ setTouchStart(touch.clientY);
1670
+ setTouchStartX(touch.clientX);
1671
+ setIsSwipeGesture(false);
1672
+ // Check if drag started from the drag handle
1673
+ isDraggingFromHandle.current = dragHandleRef.current?.contains(e.target) || false;
1484
1674
  };
1485
1675
  const handleTouchMove = (e) => {
1486
- if (!closeOnSwipeDown)
1676
+ if (!closeOnSwipeDown || touchStart === null)
1487
1677
  return;
1488
- setTouchEnd(e.targetTouches[0].clientY);
1678
+ const touch = e.targetTouches[0];
1679
+ const currentY = touch.clientY;
1680
+ const currentX = touch.clientX;
1681
+ setTouchEnd(currentY);
1682
+ // Determine if this is a vertical swipe gesture (vs horizontal scroll)
1683
+ if (!isSwipeGesture && touchStartX !== null) {
1684
+ const deltaY = Math.abs(currentY - touchStart);
1685
+ const deltaX = Math.abs(currentX - touchStartX);
1686
+ // If vertical movement is greater, it's a swipe gesture
1687
+ if (deltaY > 10 || deltaX > 10) {
1688
+ setIsSwipeGesture(deltaY > deltaX);
1689
+ }
1690
+ }
1489
1691
  };
1490
1692
  const handleTouchEnd = () => {
1491
- if (!closeOnSwipeDown || !touchStart || !touchEnd)
1693
+ if (!closeOnSwipeDown || touchStart === null || touchEnd === null) {
1694
+ resetTouchState();
1492
1695
  return;
1696
+ }
1493
1697
  const distance = touchEnd - touchStart;
1494
- const isSwipeDown = distance > 100; // Minimum swipe distance
1495
- if (isSwipeDown) {
1698
+ const isSwipeDown = distance > 80; // Minimum swipe distance
1699
+ // Only close if:
1700
+ // 1. Swiped from drag handle, OR
1701
+ // 2. Body is scrolled to top and user swiped down
1702
+ const bodyScrolledToTop = (bodyRef.current?.scrollTop || 0) <= 0;
1703
+ const shouldClose = isSwipeDown && isSwipeGesture && (isDraggingFromHandle.current || bodyScrolledToTop);
1704
+ if (shouldClose) {
1496
1705
  handleClose();
1497
1706
  }
1707
+ resetTouchState();
1708
+ };
1709
+ const resetTouchState = () => {
1498
1710
  setTouchStart(null);
1499
1711
  setTouchEnd(null);
1712
+ setTouchStartX(null);
1713
+ setIsSwipeGesture(false);
1714
+ isDraggingFromHandle.current = false;
1500
1715
  };
1716
+ // Handle Android back button
1717
+ React.useEffect(() => {
1718
+ if (!isOpen || !isAndroid())
1719
+ return;
1720
+ const handlePopState = (e) => {
1721
+ e.preventDefault();
1722
+ handleClose();
1723
+ };
1724
+ // Push a dummy state so back button can be caught
1725
+ window.history.pushState({ bottomSheet: true }, "");
1726
+ window.addEventListener("popstate", handlePopState);
1727
+ return () => {
1728
+ window.removeEventListener("popstate", handlePopState);
1729
+ // Clean up the dummy state if still present
1730
+ if (window.history.state?.bottomSheet) {
1731
+ window.history.back();
1732
+ }
1733
+ };
1734
+ }, [isOpen, handleClose]);
1501
1735
  // Don't render if not open and not closing
1502
1736
  if (!isOpen && !isClosing)
1503
1737
  return null;
1504
1738
  const hasHeader = title || description;
1505
- return (jsxs("div", { className: cn("fixed inset-0 z-9999 flex items-end justify-center", className), role: "dialog", "aria-modal": "true", "aria-label": ariaLabel || title, "aria-describedby": ariaDescribedBy, children: [jsx("div", { className: cn("absolute inset-0 z-0 bg-black/50 backdrop-blur-sm transition-opacity duration-300", isClosing ? "opacity-0" : "opacity-100", overlayClassName), onClick: handleOverlayClick, "aria-hidden": "true" }), jsxs("div", { ref: contentRef, className: cn("relative z-10 w-full transition-transform duration-300", "flex flex-col overflow-hidden", variant === "default" && "bg-white rounded-t-2xl shadow-xl", isClosing ? "animate-slide-out-bottom" : "animate-slide-in-bottom", contentClassName), style: { maxHeight }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [showDragHandle && variant === "default" && (jsx("div", { className: "flex justify-center pt-3 pb-2", children: jsx("div", { className: "w-10 h-1 bg-neutral-300 rounded-full" }) })), hasHeader && (jsxs("div", { className: cn("flex items-start justify-between gap-4", variant === "default" && "px-6", variant === "default" && !showDragHandle && "pt-6", variant === "default" && showDragHandle && "pt-2", variant === "default" && !description && "pb-4", variant === "default" && 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: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet", className: "shrink-0" }))] })), !hasHeader && showCloseButton && onClose && (jsx("div", { className: cn("absolute z-10", variant === "default" && "top-4 right-4"), children: jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet" }) })), jsx("div", { ref: bodyRef, className: cn("flex-1 overflow-y-auto", variant === "default" && "px-6", variant === "default" && hasHeader && "py-4", variant === "default" && !hasHeader && !showDragHandle && "pt-6 pb-4", variant === "default" && !hasHeader && showDragHandle && "pt-2 pb-4", variant === "default" && !footer && "pb-6", bodyClassName), children: children }), footer && (jsxs("div", { className: "flex flex-col", children: [variant === "default" && (jsx(Divider, { thickness: "thin", variant: "muted" })), jsx("div", { className: cn("flex items-center justify-end gap-3", variant === "default" && "px-6 py-4", footerClassName), children: footer })] }))] })] }));
1739
+ // Calculate dynamic max height based on keyboard
1740
+ const dynamicMaxHeight = keyboardHeight > 0
1741
+ ? `calc(${viewportHeight}px - ${safeAreaBottom}px - 20px)`
1742
+ : maxHeight;
1743
+ return (jsxs("div", { className: cn("fixed inset-0 z-9999 flex items-end justify-center",
1744
+ // Use visual viewport height on mobile when keyboard is open
1745
+ keyboardHeight > 0 && "items-start pt-[env(safe-area-inset-top)]", className), style: {
1746
+ // Use visual viewport height when keyboard is open
1747
+ height: keyboardHeight > 0 ? `${viewportHeight}px` : "100%",
1748
+ }, role: "dialog", "aria-modal": "true", "aria-label": ariaLabel || title, "aria-describedby": ariaDescribedBy, children: [jsx("div", { ref: overlayRef, className: cn("absolute inset-0 z-0 bg-black/50 backdrop-blur-sm transition-opacity duration-300", isClosing ? "opacity-0" : "opacity-100", overlayClassName), onClick: handleOverlayClick, "aria-hidden": "true" }), jsxs("div", { ref: contentRef, className: cn("relative z-10 w-full transition-transform duration-300", "flex flex-col overflow-hidden", variant === "default" && "bg-white rounded-t-2xl shadow-xl", isClosing ? "animate-slide-out-bottom" : "animate-slide-in-bottom",
1749
+ // Ensure bottom sheet is at the bottom when keyboard is open
1750
+ keyboardHeight > 0 && "mt-auto", contentClassName), style: {
1751
+ maxHeight: dynamicMaxHeight,
1752
+ // Add safe area padding at the bottom for devices with home indicator
1753
+ paddingBottom: footer ? 0 : `max(env(safe-area-inset-bottom, 0px), ${safeAreaBottom}px)`,
1754
+ }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [showDragHandle && variant === "default" && (jsx("div", { ref: dragHandleRef, className: "flex justify-center pt-3 pb-2 cursor-grab active:cursor-grabbing touch-none", children: jsx("div", { className: "w-10 h-1 bg-neutral-300 rounded-full" }) })), hasHeader && (jsxs("div", { className: cn("flex items-start justify-between gap-4 shrink-0", variant === "default" && "px-6", variant === "default" && !showDragHandle && "pt-6", variant === "default" && showDragHandle && "pt-2", variant === "default" && !description && "pb-4", variant === "default" && description && "pb-2", headerClassName), children: [jsxs("div", { className: "flex-1 min-w-0", 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: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet", className: "shrink-0" }))] })), !hasHeader && showCloseButton && onClose && (jsx("div", { className: cn("absolute z-10", variant === "default" && "top-4 right-4"), children: jsx(IconButton, { icon: "close", onClick: handleClose, color: "neutral", size: "small", "aria-label": "Close bottom sheet" }) })), stickyContent && (jsx("div", { className: cn("shrink-0", variant === "default" && "px-6 pb-4", variant === "default" && !hasHeader && !showDragHandle && "pt-4", variant === "default" && !hasHeader && showDragHandle && "pt-2", stickyContentClassName), children: stickyContent })), jsx("div", { ref: bodyRef, className: cn("flex-1 overflow-y-auto overscroll-contain",
1755
+ // Smooth scrolling and momentum scroll for iOS
1756
+ "-webkit-overflow-scrolling-touch", variant === "default" && "px-6", variant === "default" && hasHeader && !stickyContent && "py-4", variant === "default" && hasHeader && stickyContent && "pb-4", variant === "default" && !hasHeader && !showDragHandle && !stickyContent && "pt-6 pb-4", variant === "default" && !hasHeader && showDragHandle && !stickyContent && "pt-2 pb-4", variant === "default" && stickyContent && "pt-0 pb-4", variant === "default" && !footer && "pb-6", bodyClassName), children: children }), footer && (jsxs("div", { className: "flex flex-col shrink-0", style: {
1757
+ // Add safe area padding to footer
1758
+ paddingBottom: `max(env(safe-area-inset-bottom, 0px), ${safeAreaBottom}px)`,
1759
+ }, children: [variant === "default" && (jsx(Divider, { thickness: "thin", variant: "muted" })), jsx("div", { className: cn("flex items-center justify-end gap-3", variant === "default" && "px-6 py-4", footerClassName), children: footer })] }))] })] }));
1506
1760
  });
1507
1761
  BottomSheet.displayName = "BottomSheet";
1508
1762
 
@@ -4613,8 +4867,10 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
4613
4867
  const renderDropdownContent = () => (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
4614
4868
  ? true
4615
4869
  : false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: isMobile ? "full" : (dropdownWidth === "full" ? "full" : "auto"), maxHeight: `${position.maxHeight}px`, unstyled: isMobile }));
4870
+ // Search field component for mobile BottomSheet
4871
+ const mobileSearchField = (jsx(TextField, { value: searchValue, onChange: handleSearchChange, onKeyDown: handleKeyDown, containerClassName: "mb-0", placeholder: textFieldProps.placeholder || "Search...", autoFocus: true, ...textFieldProps }));
4616
4872
  // Mobile: BottomSheet, Desktop: Regular Dropdown
4617
- const dropdownMenu = showDropdown && (isMobile ? (jsxs(BottomSheet, { isOpen: isOpen, onClose: () => setIsOpen(false), title: sectionHeading, variant: "default", showDragHandle: true, closeOnOverlayClick: true, closeOnEscape: true, closeOnSwipeDown: true, children: [jsx("div", { className: "mb-4", children: jsx(TextField, { value: searchValue, onChange: handleSearchChange, onKeyDown: handleKeyDown, containerClassName: "mb-0", placeholder: textFieldProps.placeholder || "Search...", autoFocus: true, ...textFieldProps }) }), renderDropdownContent()] })) : (jsx("div", { ref: menuRef, style: {
4873
+ const dropdownMenu = showDropdown && (isMobile ? (jsx(BottomSheet, { isOpen: isOpen, onClose: () => setIsOpen(false), title: sectionHeading, variant: "default", showDragHandle: true, closeOnOverlayClick: true, closeOnEscape: true, closeOnSwipeDown: true, adjustForKeyboard: true, scrollToFocusedInput: true, maxHeight: "85vh", stickyContent: mobileSearchField, children: renderDropdownContent() })) : (jsx("div", { ref: menuRef, style: {
4618
4874
  position: "fixed",
4619
4875
  ...(position.top !== undefined && { top: `${position.top}px` }),
4620
4876
  ...(position.bottom !== undefined && {