infinity-ui-elements 1.9.27 → 1.13.0

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
@@ -3117,79 +3117,29 @@ const DateRangePicker = React.forwardRef(({ className, value: controlledValue, d
3117
3117
  });
3118
3118
  DateRangePicker.displayName = "DateRangePicker";
3119
3119
 
3120
- const DropdownMenu = React.forwardRef(({ items = [], customContent, customContentClassName, sectionHeading, isLoading = false, isEmpty = 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, showChevron = false, emptyIcon, disableFooter = false, showFooter, footerLayout = "horizontal", onClose, focusedIndex = -1, className, width = "auto", maxHeight = "400px", unstyled = false, constrainToViewport = true, }, ref) => {
3121
- const menuRef = React.useRef(null);
3122
- const [viewportConstraints, setViewportConstraints] = React.useState({ maxHeight: null, maxWidth: null });
3123
- const setRef = React.useCallback((el) => {
3124
- menuRef.current = el;
3125
- if (typeof ref === "function")
3126
- ref(el);
3127
- else if (ref)
3128
- ref.current = el;
3129
- }, [ref]);
3130
- React.useEffect(() => {
3131
- if (!constrainToViewport || !menuRef.current)
3132
- return;
3133
- const VIEWPORT_PADDING = 8;
3134
- const updateConstraints = () => {
3135
- const el = menuRef.current;
3136
- if (!el)
3137
- return;
3138
- const rect = el.getBoundingClientRect();
3139
- const viewportHeight = window.innerHeight;
3140
- const viewportWidth = window.innerWidth;
3141
- const spaceBelow = viewportHeight - rect.bottom - VIEWPORT_PADDING;
3142
- const spaceRight = viewportWidth - rect.left - VIEWPORT_PADDING;
3143
- setViewportConstraints({
3144
- maxHeight: Math.max(0, Math.floor(spaceBelow)),
3145
- maxWidth: rect.right > viewportWidth - VIEWPORT_PADDING
3146
- ? Math.max(0, Math.floor(spaceRight))
3147
- : null,
3148
- });
3149
- };
3150
- const runAfterPaint = () => {
3151
- requestAnimationFrame(() => {
3152
- requestAnimationFrame(updateConstraints);
3153
- });
3154
- };
3155
- runAfterPaint();
3156
- window.addEventListener("resize", runAfterPaint);
3157
- window.addEventListener("scroll", runAfterPaint, true);
3158
- return () => {
3159
- window.removeEventListener("resize", runAfterPaint);
3160
- window.removeEventListener("scroll", runAfterPaint, true);
3161
- };
3162
- }, [constrainToViewport]);
3163
- const parsedMaxHeight = parseInt(String(maxHeight), 10) || 400;
3164
- const effectiveMaxHeight = viewportConstraints.maxHeight !== null
3165
- ? `${Math.min(parsedMaxHeight, viewportConstraints.maxHeight)}px`
3166
- : maxHeight;
3167
- const effectiveMaxWidth = viewportConstraints.maxWidth !== null
3168
- ? `${viewportConstraints.maxWidth}px`
3169
- : undefined;
3120
+ const DropdownMenu = React.forwardRef(({ items = [], customContent, customContentClassName, sectionHeading, isLoading = false, isEmpty = 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, showChevron = false, emptyIcon, disableFooter = false, showFooter, footerLayout = "horizontal", onClose, focusedIndex = -1, className, width = "auto", maxHeight = "400px", unstyled = false, }, ref) => {
3170
3121
  const renderContent = () => {
3171
3122
  if (isLoading) {
3172
3123
  return (jsx("div", { className: "flex flex-col items-center justify-center py-12 px-6", children: jsx(Loader2, { className: "w-12 h-12 text-action-ink-primary-normal mb-4 animate-spin" }) }));
3173
3124
  }
3174
3125
  if (customContent) {
3175
- return (jsxs("div", { className: `py-3 px-3 overflow-y-auto ${customContentClassName}`, style: { maxHeight: effectiveMaxHeight, maxWidth: effectiveMaxWidth }, children: [sectionHeading && (jsx(Text, { as: "div", variant: "body", size: "small", weight: "medium", className: "text-body-small-medium text-surface-ink-neutral-muted px-3 py-2 mb-1", children: sectionHeading })), jsx("div", { children: customContent })] }));
3126
+ return (jsxs("div", { className: `py-3 px-3 overflow-y-auto ${customContentClassName}`, style: { maxHeight }, children: [sectionHeading && (jsx(Text, { as: "div", variant: "body", size: "small", weight: "medium", className: "text-body-small-medium text-surface-ink-neutral-muted px-3 py-2 mb-1", children: sectionHeading })), jsx("div", { children: customContent })] }));
3176
3127
  }
3177
3128
  if (isEmpty || items.length === 0) {
3178
3129
  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 }))] }));
3179
3130
  }
3180
- return (jsxs("div", { className: `py-3 px-3 overflow-y-auto`, style: { maxHeight: effectiveMaxHeight, maxWidth: effectiveMaxWidth }, 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: () => {
3131
+ return (jsxs("div", { className: `py-3 px-3 overflow-y-auto`, style: { maxHeight }, 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: () => {
3181
3132
  item.onClick?.();
3182
3133
  onClose?.();
3183
3134
  }, containerClassName: cn(index === focusedIndex && "bg-action-fill-primary-faded") }, item.value))) })] }));
3184
3135
  };
3185
3136
  const widthClass = width === "full" ? "w-full" : width === "auto" ? "w-auto" : "";
3186
3137
  const footerVisible = showFooter ?? !disableFooter;
3187
- return (jsxs("div", { ref: setRef, className: cn(!unstyled && "bg-white rounded-large overflow-hidden", unstyled && "w-full", widthClass, className), style: {
3138
+ return (jsxs("div", { ref: ref, className: cn(!unstyled && "bg-white rounded-large overflow-hidden", unstyled && "w-full", widthClass, className), style: {
3188
3139
  ...(!unstyled && {
3189
3140
  boxShadow: "0 1px 2px rgba(25, 25, 30, 0.1), 0 2px 6px rgba(25, 25, 30, 0.06)",
3190
3141
  }),
3191
3142
  ...(width !== "full" && width !== "auto" ? { width } : {}),
3192
- ...(effectiveMaxWidth ? { maxWidth: effectiveMaxWidth } : {}),
3193
3143
  }, children: [renderContent(), footerVisible && (jsxs("div", { className: "flex flex-col", children: [jsx(Divider, { thickness: "thin", variant: "muted" }), jsxs("div", { className: cn("flex gap-3 p-4", footerLayout === "vertical"
3194
3144
  ? "flex-col"
3195
3145
  : "items-center flex-row"), children: [onSecondaryClick && (jsx(Button, { variant: "secondary", color: "primary", size: "medium", isFullWidth: true, onClick: onSecondaryClick, children: secondaryButtonText })), onPrimaryClick && (jsx(Button, { variant: "primary", color: "primary", size: "medium", isFullWidth: true, onClick: onPrimaryClick, children: primaryButtonText }))] })] }))] }));
@@ -3213,14 +3163,12 @@ const Dropdown = React.forwardRef(({ className, trigger, items = [], customConte
3213
3163
  const isOpen = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen;
3214
3164
  const dropdownRef = React.useRef(null);
3215
3165
  const menuRef = React.useRef(null);
3216
- const VIEWPORT_PADDING = 8;
3217
- const MENU_MAX_HEIGHT = 400;
3218
3166
  const [menuPosition, setMenuPosition] = React.useState({
3219
3167
  top: "100%",
3220
3168
  bottom: "auto",
3221
3169
  left: "0",
3222
3170
  right: "auto",
3223
- maxHeight: `${MENU_MAX_HEIGHT}px`,
3171
+ maxHeight: "400px",
3224
3172
  });
3225
3173
  // Detect if we're on mobile (< 768px)
3226
3174
  const [isMobile, setIsMobile] = React.useState(false);
@@ -3279,56 +3227,68 @@ const Dropdown = React.forwardRef(({ className, trigger, items = [], customConte
3279
3227
  const calculatePosition = () => {
3280
3228
  const triggerRect = dropdownRef.current.getBoundingClientRect();
3281
3229
  const menuElement = menuRef.current;
3282
- const currentContainerRect = dropdownRef.current.getBoundingClientRect();
3230
+ // Get menu dimensions (use a temporary measurement if needed)
3283
3231
  const menuRect = menuElement.getBoundingClientRect();
3284
- const menuHeight = menuRect.height || MENU_MAX_HEIGHT;
3285
- const menuWidth = menuRect.width || 320; // fallback for initial render (w-80 = 320px)
3232
+ const menuHeight = menuRect.height || 400; // fallback to max-height
3233
+ const menuWidth = menuRect.width;
3286
3234
  const viewportHeight = window.innerHeight;
3287
3235
  const viewportWidth = window.innerWidth;
3288
- const spaceBelow = viewportHeight - triggerRect.bottom - VIEWPORT_PADDING;
3289
- const spaceAbove = triggerRect.top - VIEWPORT_PADDING;
3236
+ const spaceBelow = viewportHeight - triggerRect.bottom;
3237
+ const spaceAbove = triggerRect.top;
3238
+ const spaceRight = viewportWidth - triggerRect.left;
3239
+ const spaceLeft = triggerRect.right;
3290
3240
  const position = {
3291
- maxHeight: `${MENU_MAX_HEIGHT}px`,
3241
+ top: "auto",
3242
+ bottom: "auto",
3243
+ left: "auto",
3244
+ right: "auto",
3245
+ maxHeight: "400px",
3292
3246
  };
3293
- // Vertical positioning: prefer below, flip above if not enough space
3247
+ // Vertical positioning
3294
3248
  if (spaceBelow >= menuHeight || spaceBelow >= spaceAbove) {
3249
+ // Position below trigger
3295
3250
  position.top = "100%";
3296
3251
  position.bottom = "auto";
3297
- position.maxHeight = `${Math.min(MENU_MAX_HEIGHT, Math.max(0, spaceBelow))}px`;
3252
+ position.maxHeight = `${Math.min(400, spaceBelow - 16)}px`;
3298
3253
  }
3299
3254
  else {
3255
+ // Position above trigger
3300
3256
  position.top = "auto";
3301
3257
  position.bottom = "100%";
3302
- position.maxHeight = `${Math.min(MENU_MAX_HEIGHT, Math.max(0, spaceAbove))}px`;
3258
+ position.maxHeight = `${Math.min(400, spaceAbove - 16)}px`;
3259
+ }
3260
+ // Horizontal positioning
3261
+ if (spaceRight >= menuWidth) {
3262
+ // Align to left edge of trigger
3263
+ position.left = "0";
3264
+ position.right = "auto";
3265
+ }
3266
+ else if (spaceLeft >= menuWidth) {
3267
+ // Align to right edge of trigger
3268
+ position.left = "auto";
3269
+ position.right = "0";
3270
+ }
3271
+ else {
3272
+ // Not enough space on either side, try to center or align based on available space
3273
+ if (triggerRect.left + menuWidth > viewportWidth) {
3274
+ position.left = "auto";
3275
+ position.right = "0";
3276
+ }
3277
+ else {
3278
+ position.left = "0";
3279
+ position.right = "auto";
3280
+ }
3303
3281
  }
3304
- // Horizontal: clamp menu left so it stays within viewport
3305
- const minMenuLeft = VIEWPORT_PADDING;
3306
- const maxMenuLeft = viewportWidth - menuWidth - VIEWPORT_PADDING;
3307
- const desiredMenuLeft = Math.max(minMenuLeft, Math.min(maxMenuLeft, triggerRect.left));
3308
- position.left = `${desiredMenuLeft - currentContainerRect.left}px`;
3309
- position.right = "auto";
3310
3282
  setMenuPosition(position);
3311
3283
  };
3312
- // Run after layout so menu dimensions are correct (double rAF for paint)
3313
- let cancelled = false;
3314
- const scheduleUpdate = () => {
3315
- requestAnimationFrame(() => {
3316
- requestAnimationFrame(() => {
3317
- if (!cancelled &&
3318
- dropdownRef.current &&
3319
- menuRef.current) {
3320
- calculatePosition();
3321
- }
3322
- });
3323
- });
3324
- };
3325
- scheduleUpdate();
3326
- const handleResize = () => scheduleUpdate();
3327
- const handleScroll = () => scheduleUpdate();
3284
+ // Calculate position after menu is rendered
3285
+ calculatePosition();
3286
+ // Recalculate on window resize or scroll
3287
+ const handleResize = () => calculatePosition();
3288
+ const handleScroll = () => calculatePosition();
3328
3289
  window.addEventListener("resize", handleResize);
3329
3290
  window.addEventListener("scroll", handleScroll, true);
3330
3291
  return () => {
3331
- cancelled = true;
3332
3292
  window.removeEventListener("resize", handleResize);
3333
3293
  window.removeEventListener("scroll", handleScroll, true);
3334
3294
  };
@@ -3353,6 +3313,188 @@ const Dropdown = React.forwardRef(({ className, trigger, items = [], customConte
3353
3313
  });
3354
3314
  Dropdown.displayName = "Dropdown";
3355
3315
 
3316
+ const fileInputVariants = cva("relative flex items-center gap-3 border rounded-large transition-all font-functional font-size-100 leading-100", {
3317
+ variants: {
3318
+ size: {
3319
+ small: "h-[28px] px-3 text-xs gap-2",
3320
+ medium: "h-[36px] px-4 text-sm gap-2",
3321
+ large: "h-[44px] px-5 text-base gap-3",
3322
+ },
3323
+ validationState: {
3324
+ none: `
3325
+ border-action-outline-neutral-faded
3326
+ focus-within:border-action-outline-primary-default
3327
+ focus-within:bg-white!
3328
+ focus-within:ring-2
3329
+ ring-surface-outline-primary-muted`,
3330
+ positive: `
3331
+ border-action-outline-positive-default
3332
+ focus-within:border-action-outline-positive-hover
3333
+ focus-within:ring-2
3334
+ ring-action-outline-positive-faded-hover`,
3335
+ negative: `border-action-outline-negative-default
3336
+ focus-within:border-action-outline-negative-hover
3337
+ focus-within:ring-2
3338
+ ring-action-outline-negative-faded-hover`,
3339
+ },
3340
+ isDisabled: {
3341
+ true: `
3342
+ border
3343
+ border-action-outline-neutral-disabled
3344
+ hover:border-action-outline-neutral-disabled
3345
+ bg-surface-fill-neutral-intense
3346
+ hover:bg-surface-fill-neutral-intense
3347
+ cursor-not-allowed`,
3348
+ false: "",
3349
+ },
3350
+ },
3351
+ defaultVariants: {
3352
+ size: "medium",
3353
+ validationState: "none",
3354
+ isDisabled: false,
3355
+ },
3356
+ });
3357
+ const formatFileSize$1 = (bytes) => {
3358
+ if (bytes === 0)
3359
+ return "0 Bytes";
3360
+ const k = 1024;
3361
+ const sizes = ["Bytes", "KB", "MB", "GB"];
3362
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3363
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
3364
+ };
3365
+ const FileInput = React.forwardRef(({ label, helperText, errorText, successText, size = "medium", validationState = "none", isDisabled = false, isRequired = false, isOptional = false, accept, multiple = false, maxSize, value, onChange, containerClassName, labelClassName, inputClassName, infoHeading, infoDescription, LinkComponent, linkText, linkHref, onLinkClick, buttonText = "Choose File", placeholderText = "No file chosen", showClearButton = true, onClear, className, ...props }, ref) => {
3366
+ const fileInputRef = React.useRef(null);
3367
+ const [selectedFiles, setSelectedFiles] = React.useState([]);
3368
+ const [error, setError] = React.useState(null);
3369
+ // Initialize from value prop
3370
+ React.useEffect(() => {
3371
+ if (value) {
3372
+ const filesArray = Array.isArray(value) ? value : [value];
3373
+ setSelectedFiles(filesArray);
3374
+ }
3375
+ else {
3376
+ setSelectedFiles([]);
3377
+ }
3378
+ }, [value]);
3379
+ const processFiles = (files) => {
3380
+ const fileArray = Array.from(files);
3381
+ const validFiles = [];
3382
+ let currentError = null;
3383
+ fileArray.forEach((file) => {
3384
+ // Check file size
3385
+ if (maxSize && file.size > maxSize) {
3386
+ currentError = `File "${file.name}" exceeds maximum size of ${formatFileSize$1(maxSize)}`;
3387
+ setError(currentError);
3388
+ return;
3389
+ }
3390
+ // Check if file type is accepted
3391
+ if (accept) {
3392
+ const acceptedTypes = accept.split(",").map((type) => type.trim());
3393
+ const isAccepted = acceptedTypes.some((type) => {
3394
+ if (type.startsWith(".")) {
3395
+ return file.name.toLowerCase().endsWith(type.toLowerCase());
3396
+ }
3397
+ if (type.includes("/*")) {
3398
+ const baseType = type.split("/")[0];
3399
+ return file.type.startsWith(baseType + "/");
3400
+ }
3401
+ return file.type === type;
3402
+ });
3403
+ if (!isAccepted) {
3404
+ currentError = `File type "${file.type}" is not accepted`;
3405
+ setError(currentError);
3406
+ return;
3407
+ }
3408
+ }
3409
+ validFiles.push(file);
3410
+ });
3411
+ if (currentError) {
3412
+ setError(currentError);
3413
+ }
3414
+ else {
3415
+ setError(null);
3416
+ }
3417
+ return validFiles;
3418
+ };
3419
+ const handleFileInputChange = (e) => {
3420
+ if (e.target.files && e.target.files.length > 0) {
3421
+ const validFiles = processFiles(e.target.files);
3422
+ if (validFiles.length > 0) {
3423
+ const newFiles = multiple
3424
+ ? [...selectedFiles, ...validFiles]
3425
+ : validFiles;
3426
+ setSelectedFiles(newFiles);
3427
+ if (onChange) {
3428
+ onChange(multiple ? newFiles : newFiles[0] || null);
3429
+ }
3430
+ }
3431
+ // Reset input value to allow selecting the same file again
3432
+ e.target.value = "";
3433
+ }
3434
+ };
3435
+ const handleButtonClick = () => {
3436
+ if (!isDisabled && fileInputRef.current) {
3437
+ fileInputRef.current.click();
3438
+ }
3439
+ };
3440
+ const handleClear = () => {
3441
+ if (isDisabled)
3442
+ return;
3443
+ setSelectedFiles([]);
3444
+ setError(null);
3445
+ // Clear the file input
3446
+ if (fileInputRef.current) {
3447
+ fileInputRef.current.value = "";
3448
+ }
3449
+ if (onClear) {
3450
+ onClear();
3451
+ }
3452
+ if (onChange) {
3453
+ onChange(null);
3454
+ }
3455
+ };
3456
+ // Determine which helper text to show
3457
+ const displayHelperText = errorText || successText || helperText || error;
3458
+ const currentValidationState = errorText
3459
+ ? "negative"
3460
+ : successText
3461
+ ? "positive"
3462
+ : validationState;
3463
+ const sizeConfig = {
3464
+ small: {
3465
+ gap: "gap-2",
3466
+ buttonSize: "xsmall",
3467
+ },
3468
+ medium: {
3469
+ gap: "gap-2",
3470
+ buttonSize: "small",
3471
+ },
3472
+ large: {
3473
+ gap: "gap-3",
3474
+ buttonSize: "small",
3475
+ },
3476
+ };
3477
+ const config = sizeConfig[size];
3478
+ const getDisplayText = () => {
3479
+ if (selectedFiles.length === 0) {
3480
+ return placeholderText;
3481
+ }
3482
+ if (selectedFiles.length === 1) {
3483
+ return selectedFiles[0].name;
3484
+ }
3485
+ return `${selectedFiles.length} file(s) selected`;
3486
+ };
3487
+ const hasFiles = selectedFiles.length > 0;
3488
+ return (jsxs("div", { ref: ref, className: cn("w-full flex flex-col", config.gap, containerClassName), ...props, children: [label && (jsx(FormHeader, { label: label, size: size, isRequired: isRequired, isOptional: isOptional, infoHeading: infoHeading, infoDescription: infoDescription, LinkComponent: LinkComponent, linkText: linkText, linkHref: linkHref, onLinkClick: onLinkClick, className: "mb-2", labelClassName: labelClassName })), jsxs("div", { className: cn(fileInputVariants({
3489
+ size,
3490
+ validationState: currentValidationState,
3491
+ isDisabled,
3492
+ }), className), children: [jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: multiple, disabled: isDisabled, onChange: handleFileInputChange, className: "hidden", "aria-label": label || "File upload" }), jsx(Link, { type: "action", color: "primary", size: config.buttonSize, onClick: handleButtonClick, isDisabled: isDisabled, className: "shrink-0", children: buttonText }), jsx("div", { className: "flex-1 min-w-0", children: jsx(Text, { variant: "body", size: "small", color: isDisabled ? "disabled" : "muted", className: "truncate", title: getDisplayText(), children: getDisplayText() }) }), showClearButton && hasFiles && !isDisabled && (jsx(IconButton, { icon: "close", size: "xsmall", onClick: handleClear, "aria-label": "Clear selected files", className: "shrink-0" }))] }), jsx(FormFooter, { helperText: displayHelperText || undefined, validationState: currentValidationState === "none"
3493
+ ? "default"
3494
+ : currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" })] }));
3495
+ });
3496
+ FileInput.displayName = "FileInput";
3497
+
3356
3498
  const Modal = React.forwardRef(({ isOpen, onClose, title, description, footer, children, variant = "default", size = "medium", showCloseButton = true, closeOnOverlayClick = true, closeOnEscape = true, className, contentClassName, headerClassName, bodyClassName, footerClassName, overlayClassName, ariaLabel, ariaDescribedBy, }, ref) => {
3357
3499
  const modalRef = React.useRef(null);
3358
3500
  const contentRef = ref || modalRef;
@@ -4170,11 +4312,12 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
4170
4312
  const inputRef = React.useRef(null);
4171
4313
  const menuRef = React.useRef(null);
4172
4314
  const VIEWPORT_PADDING = 8;
4315
+ const MENU_MAX_HEIGHT = 400;
4316
+ const MIN_SPACE_TO_OPEN_BELOW = 200;
4173
4317
  const [position, setPosition] = React.useState({
4174
- top: 0,
4175
4318
  left: 0,
4176
- width: 0,
4177
- maxHeight: 400,
4319
+ width: 200,
4320
+ maxHeight: MENU_MAX_HEIGHT,
4178
4321
  });
4179
4322
  // Detect if we're on mobile (< 768px)
4180
4323
  React.useEffect(() => {
@@ -4236,7 +4379,7 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
4236
4379
  }
4237
4380
  }
4238
4381
  }, [isOpen]);
4239
- // Update position when dropdown opens or window resizes - use viewport coords for portal
4382
+ // Update position when dropdown opens or window resizes - viewport coords, open above if not enough space below
4240
4383
  const updateMenuPosition = React.useCallback(() => {
4241
4384
  if (!dropdownRef.current)
4242
4385
  return;
@@ -4244,14 +4387,26 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
4244
4387
  const viewportHeight = window.innerHeight;
4245
4388
  const viewportWidth = window.innerWidth;
4246
4389
  const spaceBelow = viewportHeight - rect.bottom - VIEWPORT_PADDING;
4247
- const menuTop = rect.bottom + VIEWPORT_PADDING;
4248
- const menuLeft = Math.max(VIEWPORT_PADDING, Math.min(rect.left, viewportWidth - rect.width - VIEWPORT_PADDING));
4249
- setPosition({
4250
- top: menuTop,
4251
- left: menuLeft,
4252
- width: rect.width,
4253
- maxHeight: Math.max(0, Math.min(400, spaceBelow)),
4254
- });
4390
+ const spaceAbove = rect.top - VIEWPORT_PADDING;
4391
+ const openAbove = spaceBelow < MIN_SPACE_TO_OPEN_BELOW || spaceBelow < spaceAbove;
4392
+ const menuLeft = Math.max(VIEWPORT_PADDING, Math.min(rect.left, viewportWidth - Math.max(rect.width, 200) - VIEWPORT_PADDING));
4393
+ const menuWidth = Math.max(rect.width, 200);
4394
+ if (openAbove) {
4395
+ setPosition({
4396
+ bottom: viewportHeight - rect.top + VIEWPORT_PADDING,
4397
+ left: menuLeft,
4398
+ width: menuWidth,
4399
+ maxHeight: Math.max(120, Math.min(MENU_MAX_HEIGHT, spaceAbove)),
4400
+ });
4401
+ }
4402
+ else {
4403
+ setPosition({
4404
+ top: rect.bottom + VIEWPORT_PADDING,
4405
+ left: menuLeft,
4406
+ width: menuWidth,
4407
+ maxHeight: Math.max(120, Math.min(MENU_MAX_HEIGHT, spaceBelow)),
4408
+ });
4409
+ }
4255
4410
  }, []);
4256
4411
  React.useLayoutEffect(() => {
4257
4412
  if (isOpen) {
@@ -4445,11 +4600,14 @@ const SearchableDropdown = React.forwardRef(({ className, items = [], sectionHea
4445
4600
  // Render dropdown menu content
4446
4601
  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
4447
4602
  ? true
4448
- : false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: isMobile ? "full" : (dropdownWidth === "full" ? "full" : "auto"), maxHeight: `${position.maxHeight}px`, unstyled: isMobile, constrainToViewport: true }));
4603
+ : false, footerLayout: footerLayout, onClose: () => setIsOpen(false), focusedIndex: focusedIndex, className: dropdownClassName, width: isMobile ? "full" : (dropdownWidth === "full" ? "full" : "auto"), maxHeight: `${position.maxHeight}px`, unstyled: isMobile }));
4449
4604
  // Mobile: BottomSheet, Desktop: Regular Dropdown
4450
4605
  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: {
4451
4606
  position: "fixed",
4452
- top: `${position.top}px`,
4607
+ ...(position.top !== undefined && { top: `${position.top}px` }),
4608
+ ...(position.bottom !== undefined && {
4609
+ bottom: `${position.bottom}px`,
4610
+ }),
4453
4611
  left: `${position.left}px`,
4454
4612
  width: dropdownWidth === "full" ? `${position.width}px` : "auto",
4455
4613
  zIndex: isInsideModal ? 10001 : 9999,
@@ -6178,5 +6336,5 @@ const UploadBox = React.forwardRef(({ label, helperText, errorText, successText,
6178
6336
  });
6179
6337
  UploadBox.displayName = "UploadBox";
6180
6338
 
6181
- export { Alert, Amount, Avatar, AvatarCell, Badge, BottomSheet, Button, ButtonGroup, Checkbox, Counter, DatePicker, DateRangePicker, Divider, Dropdown, DropdownMenu, FormFooter, FormHeader, Icon, IconButton, IconCell, Link, ListItem, Modal, NumberCell, Pagination, Radio, RadioGroup, SearchableDropdown, Select, SelectTextField, SidePanel, Skeleton, SlotCell, SpacerCell, Switch, TabItem, Table, TableDetailPanel, Tabs, Text, TextArea, TextField, Toast, ToastProvider, Tooltip, UploadBox, alertVariants, avatarVariants, badgeVariants, buttonGroupVariants, buttonVariants, checkboxVariants, cn, counterVariants, datePickerVariants, dateRangePickerVariants, dropdownVariants, getAvailableIcons, hasIcon, iconButtonVariants, iconRegistry, linkVariants, listItemVariants, paginationVariants, radioVariants, selectTriggerVariants, switchVariants, tableCellVariants, tableHeaderVariants, tableVariants, textAreaVariants, textFieldVariants, tooltipVariants, uploadBoxVariants, useToast };
6339
+ export { Alert, Amount, Avatar, AvatarCell, Badge, BottomSheet, Button, ButtonGroup, Checkbox, Counter, DatePicker, DateRangePicker, Divider, Dropdown, DropdownMenu, FileInput, FormFooter, FormHeader, Icon, IconButton, IconCell, Link, ListItem, Modal, NumberCell, Pagination, Radio, RadioGroup, SearchableDropdown, Select, SelectTextField, SidePanel, Skeleton, SlotCell, SpacerCell, Switch, TabItem, Table, TableDetailPanel, Tabs, Text, TextArea, TextField, Toast, ToastProvider, Tooltip, UploadBox, alertVariants, avatarVariants, badgeVariants, buttonGroupVariants, buttonVariants, checkboxVariants, cn, counterVariants, datePickerVariants, dateRangePickerVariants, dropdownVariants, fileInputVariants, getAvailableIcons, hasIcon, iconButtonVariants, iconRegistry, linkVariants, listItemVariants, paginationVariants, radioVariants, selectTriggerVariants, switchVariants, tableCellVariants, tableHeaderVariants, tableVariants, textAreaVariants, textFieldVariants, tooltipVariants, uploadBoxVariants, useToast };
6182
6340
  //# sourceMappingURL=index.esm.js.map