infinity-ui-elements 1.8.12 → 1.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/SelectTextField/SelectTextField.d.ts +73 -0
- package/dist/components/SelectTextField/SelectTextField.d.ts.map +1 -0
- package/dist/components/SelectTextField/SelectTextField.stories.d.ts +21 -0
- package/dist/components/SelectTextField/SelectTextField.stories.d.ts.map +1 -0
- package/dist/components/SelectTextField/index.d.ts +3 -0
- package/dist/components/SelectTextField/index.d.ts.map +1 -0
- package/dist/components/UploadBox/UploadBox.d.ts +49 -0
- package/dist/components/UploadBox/UploadBox.d.ts.map +1 -0
- package/dist/components/UploadBox/UploadBox.stories.d.ts +29 -0
- package/dist/components/UploadBox/UploadBox.stories.d.ts.map +1 -0
- package/dist/components/UploadBox/index.d.ts +3 -0
- package/dist/components/UploadBox/index.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +562 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +565 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/icons.d.ts +2 -0
- package/dist/lib/icons.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -61,6 +61,14 @@ const iconRegistry = {
|
|
|
61
61
|
close: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
62
62
|
<path d="M18 6L6 18M6 6L18 18" stroke="#081416" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
63
63
|
</svg>`,
|
|
64
|
+
upload: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
65
|
+
<path d="M4 19H20V12H22V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V12H4V19ZM13 9V16H11V9H6L12 3L18 9H13Z" fill="#081416"/>
|
|
66
|
+
</svg>
|
|
67
|
+
`,
|
|
68
|
+
file: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
69
|
+
<path d="M19.1111 21H4.88889C4.39797 21 4 20.5971 4 20.1V3.9C4 3.40295 4.39797 3 4.88889 3H19.1111C19.602 3 20 3.40295 20 3.9V20.1C20 20.5971 19.602 21 19.1111 21ZM18.2222 19.2V4.8H5.77778V19.2H18.2222ZM8.44444 9.3H15.5556V11.1H8.44444V9.3ZM8.44444 12.9H15.5556V14.7H8.44444V12.9Z" fill="#081416"/>
|
|
70
|
+
</svg>
|
|
71
|
+
`,
|
|
64
72
|
};
|
|
65
73
|
const Icon = ({ name, size = 24, className = "", style = {}, ...props }) => {
|
|
66
74
|
const svgContent = iconRegistry[name];
|
|
@@ -3421,6 +3429,208 @@ const Skeleton = React.forwardRef(({ className, containerClassName, containerSty
|
|
|
3421
3429
|
});
|
|
3422
3430
|
Skeleton.displayName = "Skeleton";
|
|
3423
3431
|
|
|
3432
|
+
const selectTriggerVariants = cva("flex items-center gap-1 transition-all font-display font-size-100 leading-100", {
|
|
3433
|
+
variants: {
|
|
3434
|
+
size: {
|
|
3435
|
+
small: "px-2 text-xs",
|
|
3436
|
+
medium: "px-2 text-sm",
|
|
3437
|
+
large: "px-3 text-base",
|
|
3438
|
+
},
|
|
3439
|
+
validationState: {
|
|
3440
|
+
none: "",
|
|
3441
|
+
positive: "",
|
|
3442
|
+
negative: "",
|
|
3443
|
+
},
|
|
3444
|
+
isDisabled: {
|
|
3445
|
+
true: "opacity-60 cursor-not-allowed",
|
|
3446
|
+
false: "cursor-pointer hover:opacity-80",
|
|
3447
|
+
},
|
|
3448
|
+
},
|
|
3449
|
+
defaultVariants: {
|
|
3450
|
+
size: "medium",
|
|
3451
|
+
validationState: "none",
|
|
3452
|
+
isDisabled: false,
|
|
3453
|
+
},
|
|
3454
|
+
});
|
|
3455
|
+
const SelectTextField = React.forwardRef(({ textValue: controlledTextValue, defaultTextValue, onTextChange, selectOptions = [], selectValue: controlledSelectValue, defaultSelectValue, onSelectChange, selectPlaceholder = "Select", selectTriggerClassName, selectMenuClassName, selectMenuWidth = "auto", selectSectionHeading, selectEmptyTitle = "No options available", selectEmptyDescription = "There are no options to select from.", selectEmptyIcon, label, helperText, errorText, successText, validationState = "none", isDisabled = false, isRequired = false, isOptional = false, size = "medium", containerClassName, labelClassName, inputClassName, className, ...textFieldProps }, ref) => {
|
|
3456
|
+
const [uncontrolledTextValue, setUncontrolledTextValue] = React.useState(defaultTextValue || "");
|
|
3457
|
+
const [uncontrolledSelectValue, setUncontrolledSelectValue] = React.useState(defaultSelectValue);
|
|
3458
|
+
const [isSelectOpen, setIsSelectOpen] = React.useState(false);
|
|
3459
|
+
const [dropdownPlacement, setDropdownPlacement] = React.useState("bottom");
|
|
3460
|
+
const selectRef = React.useRef(null);
|
|
3461
|
+
const dropdownContainerRef = React.useRef(null);
|
|
3462
|
+
const componentRef = React.useRef(null);
|
|
3463
|
+
const textValue = controlledTextValue !== undefined
|
|
3464
|
+
? controlledTextValue
|
|
3465
|
+
: uncontrolledTextValue;
|
|
3466
|
+
const selectValue = controlledSelectValue !== undefined
|
|
3467
|
+
? controlledSelectValue
|
|
3468
|
+
: uncontrolledSelectValue;
|
|
3469
|
+
// Find the selected option
|
|
3470
|
+
const selectedOption = selectOptions.find((opt) => opt.value === selectValue);
|
|
3471
|
+
// Determine which helper text to show
|
|
3472
|
+
const displayHelperText = errorText || successText || helperText;
|
|
3473
|
+
const currentValidationState = errorText
|
|
3474
|
+
? "negative"
|
|
3475
|
+
: successText
|
|
3476
|
+
? "positive"
|
|
3477
|
+
: validationState;
|
|
3478
|
+
const handleTextChange = (e) => {
|
|
3479
|
+
const newValue = e.target.value;
|
|
3480
|
+
if (onTextChange) {
|
|
3481
|
+
onTextChange(newValue);
|
|
3482
|
+
}
|
|
3483
|
+
else {
|
|
3484
|
+
setUncontrolledTextValue(newValue);
|
|
3485
|
+
}
|
|
3486
|
+
};
|
|
3487
|
+
const handleSelectOpenChange = (newOpen) => {
|
|
3488
|
+
if (!isDisabled) {
|
|
3489
|
+
setIsSelectOpen(newOpen);
|
|
3490
|
+
}
|
|
3491
|
+
};
|
|
3492
|
+
const toggleSelectOpen = () => {
|
|
3493
|
+
handleSelectOpenChange(!isSelectOpen);
|
|
3494
|
+
};
|
|
3495
|
+
const handleSelect = (option) => {
|
|
3496
|
+
if (controlledSelectValue === undefined) {
|
|
3497
|
+
setUncontrolledSelectValue(option.value);
|
|
3498
|
+
}
|
|
3499
|
+
onSelectChange?.(option.value, option);
|
|
3500
|
+
setIsSelectOpen(false);
|
|
3501
|
+
};
|
|
3502
|
+
const updateDropdownPlacement = React.useCallback(() => {
|
|
3503
|
+
if (typeof window === "undefined")
|
|
3504
|
+
return;
|
|
3505
|
+
const trigger = selectRef.current;
|
|
3506
|
+
if (!trigger)
|
|
3507
|
+
return;
|
|
3508
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
3509
|
+
const spaceBelow = window.innerHeight - triggerRect.bottom;
|
|
3510
|
+
const spaceAbove = triggerRect.top;
|
|
3511
|
+
const dropdownHeight = dropdownContainerRef.current
|
|
3512
|
+
? dropdownContainerRef.current.offsetHeight
|
|
3513
|
+
: 0;
|
|
3514
|
+
if (dropdownHeight === 0) {
|
|
3515
|
+
setDropdownPlacement(spaceBelow >= spaceAbove ? "bottom" : "top");
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
|
|
3519
|
+
setDropdownPlacement("bottom");
|
|
3520
|
+
}
|
|
3521
|
+
else {
|
|
3522
|
+
setDropdownPlacement("top");
|
|
3523
|
+
}
|
|
3524
|
+
}, []);
|
|
3525
|
+
const attachDropdownListeners = React.useCallback(() => {
|
|
3526
|
+
if (!isSelectOpen)
|
|
3527
|
+
return;
|
|
3528
|
+
if (typeof window === "undefined")
|
|
3529
|
+
return;
|
|
3530
|
+
let rafId = requestAnimationFrame(updateDropdownPlacement);
|
|
3531
|
+
const handleUpdate = () => updateDropdownPlacement();
|
|
3532
|
+
window.addEventListener("resize", handleUpdate);
|
|
3533
|
+
window.addEventListener("scroll", handleUpdate, true);
|
|
3534
|
+
return () => {
|
|
3535
|
+
cancelAnimationFrame(rafId);
|
|
3536
|
+
window.removeEventListener("resize", handleUpdate);
|
|
3537
|
+
window.removeEventListener("scroll", handleUpdate, true);
|
|
3538
|
+
};
|
|
3539
|
+
}, [isSelectOpen, updateDropdownPlacement]);
|
|
3540
|
+
React.useEffect(() => {
|
|
3541
|
+
const detach = attachDropdownListeners();
|
|
3542
|
+
return () => {
|
|
3543
|
+
detach?.();
|
|
3544
|
+
};
|
|
3545
|
+
}, [attachDropdownListeners]);
|
|
3546
|
+
React.useEffect(() => {
|
|
3547
|
+
if (isSelectOpen) {
|
|
3548
|
+
updateDropdownPlacement();
|
|
3549
|
+
}
|
|
3550
|
+
}, [isSelectOpen, selectOptions.length, updateDropdownPlacement]);
|
|
3551
|
+
// Close dropdown when clicking outside
|
|
3552
|
+
React.useEffect(() => {
|
|
3553
|
+
const handleClickOutside = (event) => {
|
|
3554
|
+
const target = event.target;
|
|
3555
|
+
if (selectRef.current &&
|
|
3556
|
+
!selectRef.current.contains(target) &&
|
|
3557
|
+
dropdownContainerRef.current &&
|
|
3558
|
+
!dropdownContainerRef.current.contains(target)) {
|
|
3559
|
+
handleSelectOpenChange(false);
|
|
3560
|
+
}
|
|
3561
|
+
};
|
|
3562
|
+
if (isSelectOpen) {
|
|
3563
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
3564
|
+
return () => {
|
|
3565
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
}, [isSelectOpen]);
|
|
3569
|
+
// Close on escape key
|
|
3570
|
+
React.useEffect(() => {
|
|
3571
|
+
const handleEscape = (event) => {
|
|
3572
|
+
if (event.key === "Escape") {
|
|
3573
|
+
handleSelectOpenChange(false);
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
if (isSelectOpen) {
|
|
3577
|
+
document.addEventListener("keydown", handleEscape);
|
|
3578
|
+
return () => {
|
|
3579
|
+
document.removeEventListener("keydown", handleEscape);
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
}, [isSelectOpen]);
|
|
3583
|
+
// Transform options to dropdown menu items
|
|
3584
|
+
const menuItems = selectOptions.map((option) => ({
|
|
3585
|
+
value: option.value,
|
|
3586
|
+
label: option.label ?? String(option.value),
|
|
3587
|
+
description: option.description,
|
|
3588
|
+
leadingIcon: option.leadingIcon,
|
|
3589
|
+
trailingIcon: option.trailingIcon,
|
|
3590
|
+
isDisabled: option.isDisabled,
|
|
3591
|
+
variant: option.variant,
|
|
3592
|
+
onClick: () => handleSelect(option),
|
|
3593
|
+
}));
|
|
3594
|
+
const widthStyle = selectMenuWidth === "full"
|
|
3595
|
+
? "100%"
|
|
3596
|
+
: selectMenuWidth === "auto"
|
|
3597
|
+
? "auto"
|
|
3598
|
+
: selectMenuWidth;
|
|
3599
|
+
const sizeConfig = {
|
|
3600
|
+
small: {
|
|
3601
|
+
gap: "gap-2",
|
|
3602
|
+
},
|
|
3603
|
+
medium: {
|
|
3604
|
+
gap: "gap-2",
|
|
3605
|
+
},
|
|
3606
|
+
large: {
|
|
3607
|
+
gap: "gap-3",
|
|
3608
|
+
},
|
|
3609
|
+
};
|
|
3610
|
+
// Create the select suffix component
|
|
3611
|
+
const selectSuffix = (jsxs("div", { className: "relative flex items-center h-full", children: [jsxs("div", { ref: selectRef, className: cn(selectTriggerVariants({
|
|
3612
|
+
size,
|
|
3613
|
+
validationState: currentValidationState,
|
|
3614
|
+
isDisabled,
|
|
3615
|
+
}), "border-l border-action-outline-neutral-faded pl-2 ml-2 h-full flex items-center", selectTriggerClassName), onClick: !isDisabled ? toggleSelectOpen : undefined, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isSelectOpen, "aria-disabled": isDisabled, children: [jsx("span", { className: cn("text-left truncate max-w-[120px] whitespace-nowrap", !selectedOption && "text-surface-ink-neutral-muted", isDisabled && "text-surface-ink-neutral-disabled"), children: selectedOption?.label || selectPlaceholder }), jsx(ChevronDown, { className: cn("shrink-0 transition-transform", size === "small"
|
|
3616
|
+
? "w-3 h-3"
|
|
3617
|
+
: size === "medium"
|
|
3618
|
+
? "w-3.5 h-3.5"
|
|
3619
|
+
: "w-4 h-4", isDisabled
|
|
3620
|
+
? "text-surface-ink-neutral-disabled"
|
|
3621
|
+
: currentValidationState === "positive"
|
|
3622
|
+
? "text-feedback-ink-positive-intense"
|
|
3623
|
+
: currentValidationState === "negative"
|
|
3624
|
+
? "text-feedback-ink-negative-subtle"
|
|
3625
|
+
: "text-surface-ink-neutral-muted", isSelectOpen && "transform rotate-180") })] }), isSelectOpen && !isDisabled && (jsx("div", { ref: dropdownContainerRef, className: cn("absolute z-50 right-0", dropdownPlacement === "bottom"
|
|
3626
|
+
? "top-full mt-1"
|
|
3627
|
+
: "bottom-full mb-1"), children: jsx(DropdownMenu, { items: menuItems, sectionHeading: selectSectionHeading, isEmpty: selectOptions.length === 0, emptyTitle: selectEmptyTitle, emptyDescription: selectEmptyDescription, emptyIcon: selectEmptyIcon, disableFooter: true, onClose: () => handleSelectOpenChange(false), className: selectMenuClassName, width: widthStyle }) }))] }));
|
|
3628
|
+
return (jsxs("div", { ref: componentRef, className: cn("w-full flex flex-col", sizeConfig[size].gap, containerClassName), children: [label && (jsx(FormHeader, { label: label, size: size, isRequired: isRequired, isOptional: isOptional, infoHeading: textFieldProps.infoHeading, infoDescription: textFieldProps.infoDescription, LinkComponent: textFieldProps.LinkComponent, linkText: textFieldProps.linkText, linkHref: textFieldProps.linkHref, onLinkClick: textFieldProps.onLinkClick, htmlFor: textFieldProps.id, className: "mb-2", labelClassName: labelClassName })), jsx(TextField, { ref: ref, value: textValue, onChange: handleTextChange, suffix: selectSuffix, size: size, validationState: currentValidationState, isDisabled: isDisabled, isRequired: isRequired, isOptional: isOptional, containerClassName: "gap-0", className: className, inputClassName: inputClassName, ...textFieldProps }), jsx(FormFooter, { helperText: displayHelperText, validationState: currentValidationState === "none"
|
|
3629
|
+
? "default"
|
|
3630
|
+
: currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" })] }));
|
|
3631
|
+
});
|
|
3632
|
+
SelectTextField.displayName = "SelectTextField";
|
|
3633
|
+
|
|
3424
3634
|
const switchVariants = cva("relative inline-flex items-center shrink-0 cursor-pointer rounded-full transition-all duration-200", {
|
|
3425
3635
|
variants: {
|
|
3426
3636
|
size: {
|
|
@@ -4013,5 +4223,356 @@ const TextArea = React.forwardRef(({ label, helperText, errorText, successText,
|
|
|
4013
4223
|
});
|
|
4014
4224
|
TextArea.displayName = "TextArea";
|
|
4015
4225
|
|
|
4016
|
-
|
|
4226
|
+
const uploadBoxVariants = cva("relative flex flex-col items-center justify-center border-2 border-dashed rounded-large transition-all font-display", {
|
|
4227
|
+
variants: {
|
|
4228
|
+
size: {
|
|
4229
|
+
small: "min-h-[120px] p-4 gap-2",
|
|
4230
|
+
medium: "min-h-[160px] p-6 gap-3",
|
|
4231
|
+
large: "min-h-[200px] p-8 gap-4",
|
|
4232
|
+
},
|
|
4233
|
+
validationState: {
|
|
4234
|
+
none: `
|
|
4235
|
+
border-action-outline-neutral-faded
|
|
4236
|
+
hover:border-action-outline-primary-hover
|
|
4237
|
+
focus-within:border-action-outline-primary-hover
|
|
4238
|
+
focus-within:ring-2
|
|
4239
|
+
ring-action-outline-primary-faded-hover
|
|
4240
|
+
bg-surface-fill-neutral-intense`,
|
|
4241
|
+
positive: `
|
|
4242
|
+
border-action-outline-positive-default
|
|
4243
|
+
focus-within:border-action-outline-positive-hover
|
|
4244
|
+
focus-within:ring-2
|
|
4245
|
+
ring-action-outline-positive-faded-hover
|
|
4246
|
+
bg-surface-fill-neutral-intense`,
|
|
4247
|
+
negative: `
|
|
4248
|
+
border-action-outline-negative-default
|
|
4249
|
+
focus-within:border-action-outline-negative-hover
|
|
4250
|
+
focus-within:ring-2
|
|
4251
|
+
ring-action-outline-negative-faded-hover
|
|
4252
|
+
bg-surface-fill-neutral-intense`,
|
|
4253
|
+
},
|
|
4254
|
+
isDisabled: {
|
|
4255
|
+
true: `
|
|
4256
|
+
border-[var(--border-width-thinner)]
|
|
4257
|
+
hover:border-action-outline-neutral-disabled
|
|
4258
|
+
border-action-outline-neutral-disabled
|
|
4259
|
+
bg-surface-fill-neutral-intense
|
|
4260
|
+
cursor-not-allowed
|
|
4261
|
+
opacity-60`,
|
|
4262
|
+
false: "cursor-pointer",
|
|
4263
|
+
},
|
|
4264
|
+
isDragging: {
|
|
4265
|
+
true: "border-action-outline-primary-hover bg-action-fill-primary-faded",
|
|
4266
|
+
false: "",
|
|
4267
|
+
},
|
|
4268
|
+
},
|
|
4269
|
+
defaultVariants: {
|
|
4270
|
+
size: "medium",
|
|
4271
|
+
validationState: "none",
|
|
4272
|
+
isDisabled: false,
|
|
4273
|
+
isDragging: false,
|
|
4274
|
+
},
|
|
4275
|
+
});
|
|
4276
|
+
const formatFileSize = (bytes) => {
|
|
4277
|
+
if (bytes === 0)
|
|
4278
|
+
return "0 Bytes";
|
|
4279
|
+
const k = 1024;
|
|
4280
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
4281
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
4282
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
|
4283
|
+
};
|
|
4284
|
+
const isImageFile = (file, url) => {
|
|
4285
|
+
if (file) {
|
|
4286
|
+
return file.type.startsWith("image/");
|
|
4287
|
+
}
|
|
4288
|
+
if (url) {
|
|
4289
|
+
const lowerUrl = url.toLowerCase();
|
|
4290
|
+
return (lowerUrl.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?.*)?$/i) !== null ||
|
|
4291
|
+
url.match(/data:image\//) !== null);
|
|
4292
|
+
}
|
|
4293
|
+
return false;
|
|
4294
|
+
};
|
|
4295
|
+
const isPdfFile = (file, url) => {
|
|
4296
|
+
if (file) {
|
|
4297
|
+
return file.type === "application/pdf";
|
|
4298
|
+
}
|
|
4299
|
+
if (url) {
|
|
4300
|
+
return url.toLowerCase().match(/\.pdf(\?.*)?$/i) !== null;
|
|
4301
|
+
}
|
|
4302
|
+
return false;
|
|
4303
|
+
};
|
|
4304
|
+
const getFileNameFromUrl = (url) => {
|
|
4305
|
+
try {
|
|
4306
|
+
const urlObj = new URL(url);
|
|
4307
|
+
const pathname = urlObj.pathname;
|
|
4308
|
+
const fileName = pathname.split("/").pop() || "file";
|
|
4309
|
+
return decodeURIComponent(fileName);
|
|
4310
|
+
}
|
|
4311
|
+
catch {
|
|
4312
|
+
// If URL parsing fails, try to extract from the string
|
|
4313
|
+
const parts = url.split("/");
|
|
4314
|
+
const lastPart = parts[parts.length - 1] || "file";
|
|
4315
|
+
return decodeURIComponent(lastPart.split("?")[0]);
|
|
4316
|
+
}
|
|
4317
|
+
};
|
|
4318
|
+
// Component to handle image preview with error fallback
|
|
4319
|
+
const ImagePreview = ({ src, alt }) => {
|
|
4320
|
+
const [hasError, setHasError] = React.useState(false);
|
|
4321
|
+
if (hasError) {
|
|
4322
|
+
return (jsx("div", { className: "shrink-0 w-16 h-16 rounded-medium flex items-center justify-center bg-surface-fill-neutral-subtle", children: jsx(Icon, { name: "file", size: 24, className: "text-surface-ink-neutral-subtle" }) }));
|
|
4323
|
+
}
|
|
4324
|
+
return (jsx("div", { className: "shrink-0 w-16 h-16 rounded-medium overflow-hidden ", children: jsx("img", { src: src, alt: alt, className: "w-full h-full object-cover", onError: () => setHasError(true) }) }));
|
|
4325
|
+
};
|
|
4326
|
+
const UploadBox = React.forwardRef(({ label, helperText, errorText, successText, size = "medium", validationState = "none", isDisabled = false, isRequired = false, isOptional = false, accept, multiple = false, maxSize, maxFiles, value, onChange, onFileRemove, containerClassName, labelClassName, uploadAreaClassName, previewClassName, infoHeading, infoDescription, LinkComponent, linkText, linkHref, onLinkClick, uploadText, dragText, showPreview = true, className, ...props }, ref) => {
|
|
4327
|
+
const fileInputRef = React.useRef(null);
|
|
4328
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
4329
|
+
const [uploadedFiles, setUploadedFiles] = React.useState([]);
|
|
4330
|
+
const [error, setError] = React.useState(null);
|
|
4331
|
+
// Initialize uploaded files from value prop
|
|
4332
|
+
React.useEffect(() => {
|
|
4333
|
+
if (value) {
|
|
4334
|
+
const filesArray = Array.isArray(value) ? value : [value];
|
|
4335
|
+
const processedFiles = filesArray.map((item) => {
|
|
4336
|
+
if (item instanceof File) {
|
|
4337
|
+
const uploadedFile = {
|
|
4338
|
+
file: item,
|
|
4339
|
+
name: item.name,
|
|
4340
|
+
size: item.size,
|
|
4341
|
+
id: `${item.name}-${item.size}-${item.lastModified}`,
|
|
4342
|
+
};
|
|
4343
|
+
if (isImageFile(item) && showPreview) {
|
|
4344
|
+
uploadedFile.preview = URL.createObjectURL(item);
|
|
4345
|
+
}
|
|
4346
|
+
return uploadedFile;
|
|
4347
|
+
}
|
|
4348
|
+
else if (typeof item === "string") {
|
|
4349
|
+
// It's a URL string
|
|
4350
|
+
const uploadedFile = {
|
|
4351
|
+
url: item,
|
|
4352
|
+
name: getFileNameFromUrl(item),
|
|
4353
|
+
id: `url-${item}-${Date.now()}`,
|
|
4354
|
+
};
|
|
4355
|
+
// Always set preview for URLs when showPreview is true
|
|
4356
|
+
// The rendering logic will determine if it's an image or PDF
|
|
4357
|
+
if (showPreview) {
|
|
4358
|
+
uploadedFile.preview = item;
|
|
4359
|
+
}
|
|
4360
|
+
return uploadedFile;
|
|
4361
|
+
}
|
|
4362
|
+
else {
|
|
4363
|
+
// It's already an UploadedFile
|
|
4364
|
+
const uploadedFile = item;
|
|
4365
|
+
// If it has a URL but no preview set, set it
|
|
4366
|
+
if (uploadedFile.url && !uploadedFile.preview && showPreview) {
|
|
4367
|
+
uploadedFile.preview = uploadedFile.url;
|
|
4368
|
+
}
|
|
4369
|
+
return uploadedFile;
|
|
4370
|
+
}
|
|
4371
|
+
});
|
|
4372
|
+
setUploadedFiles(processedFiles);
|
|
4373
|
+
}
|
|
4374
|
+
else {
|
|
4375
|
+
setUploadedFiles([]);
|
|
4376
|
+
}
|
|
4377
|
+
}, [value, showPreview]);
|
|
4378
|
+
// Cleanup preview URLs (only revoke object URLs, not regular URLs)
|
|
4379
|
+
React.useEffect(() => {
|
|
4380
|
+
return () => {
|
|
4381
|
+
uploadedFiles.forEach((uploadedFile) => {
|
|
4382
|
+
// Only revoke object URLs (created with URL.createObjectURL)
|
|
4383
|
+
// Regular URLs (http/https) should not be revoked
|
|
4384
|
+
if (uploadedFile.preview &&
|
|
4385
|
+
uploadedFile.preview.startsWith("blob:")) {
|
|
4386
|
+
URL.revokeObjectURL(uploadedFile.preview);
|
|
4387
|
+
}
|
|
4388
|
+
});
|
|
4389
|
+
};
|
|
4390
|
+
}, [uploadedFiles]);
|
|
4391
|
+
const processFiles = (files) => {
|
|
4392
|
+
const fileArray = Array.from(files);
|
|
4393
|
+
const validFiles = [];
|
|
4394
|
+
let currentError = null;
|
|
4395
|
+
// Check max files limit
|
|
4396
|
+
if (maxFiles && uploadedFiles.length + fileArray.length > maxFiles) {
|
|
4397
|
+
currentError = `Maximum ${maxFiles} file(s) allowed`;
|
|
4398
|
+
setError(currentError);
|
|
4399
|
+
return validFiles;
|
|
4400
|
+
}
|
|
4401
|
+
fileArray.forEach((file) => {
|
|
4402
|
+
// Check file size
|
|
4403
|
+
if (maxSize && file.size > maxSize) {
|
|
4404
|
+
currentError = `File "${file.name}" exceeds maximum size of ${formatFileSize(maxSize)}`;
|
|
4405
|
+
setError(currentError);
|
|
4406
|
+
return;
|
|
4407
|
+
}
|
|
4408
|
+
// Check if file type is accepted
|
|
4409
|
+
if (accept) {
|
|
4410
|
+
const acceptedTypes = accept.split(",").map((type) => type.trim());
|
|
4411
|
+
const isAccepted = acceptedTypes.some((type) => {
|
|
4412
|
+
if (type.startsWith(".")) {
|
|
4413
|
+
return file.name.toLowerCase().endsWith(type.toLowerCase());
|
|
4414
|
+
}
|
|
4415
|
+
if (type.includes("/*")) {
|
|
4416
|
+
const baseType = type.split("/")[0];
|
|
4417
|
+
return file.type.startsWith(baseType + "/");
|
|
4418
|
+
}
|
|
4419
|
+
return file.type === type;
|
|
4420
|
+
});
|
|
4421
|
+
if (!isAccepted) {
|
|
4422
|
+
currentError = `File type "${file.type}" is not accepted`;
|
|
4423
|
+
setError(currentError);
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
validFiles.push(file);
|
|
4428
|
+
});
|
|
4429
|
+
if (currentError) {
|
|
4430
|
+
setError(currentError);
|
|
4431
|
+
}
|
|
4432
|
+
else {
|
|
4433
|
+
setError(null);
|
|
4434
|
+
}
|
|
4435
|
+
return validFiles;
|
|
4436
|
+
};
|
|
4437
|
+
const handleFiles = (files) => {
|
|
4438
|
+
if (files.length === 0)
|
|
4439
|
+
return;
|
|
4440
|
+
const newUploadedFiles = files.map((file) => {
|
|
4441
|
+
const uploadedFile = {
|
|
4442
|
+
file,
|
|
4443
|
+
id: `${file.name}-${file.size}-${file.lastModified}-${Date.now()}`,
|
|
4444
|
+
};
|
|
4445
|
+
if (isImageFile(file) && showPreview) {
|
|
4446
|
+
uploadedFile.preview = URL.createObjectURL(file);
|
|
4447
|
+
}
|
|
4448
|
+
return uploadedFile;
|
|
4449
|
+
});
|
|
4450
|
+
const updatedFiles = multiple
|
|
4451
|
+
? [...uploadedFiles, ...newUploadedFiles]
|
|
4452
|
+
: newUploadedFiles;
|
|
4453
|
+
setUploadedFiles(updatedFiles);
|
|
4454
|
+
// Call onChange with File objects only (filter out URL-only items)
|
|
4455
|
+
if (onChange) {
|
|
4456
|
+
const filesToReturn = updatedFiles
|
|
4457
|
+
.map((uf) => uf.file)
|
|
4458
|
+
.filter((file) => file !== undefined);
|
|
4459
|
+
onChange(multiple ? filesToReturn : filesToReturn[0] || null);
|
|
4460
|
+
}
|
|
4461
|
+
};
|
|
4462
|
+
const handleFileInputChange = (e) => {
|
|
4463
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
4464
|
+
const validFiles = processFiles(e.target.files);
|
|
4465
|
+
if (validFiles.length > 0) {
|
|
4466
|
+
handleFiles(validFiles);
|
|
4467
|
+
}
|
|
4468
|
+
// Reset input value to allow selecting the same file again
|
|
4469
|
+
e.target.value = "";
|
|
4470
|
+
}
|
|
4471
|
+
};
|
|
4472
|
+
const handleDragOver = (e) => {
|
|
4473
|
+
e.preventDefault();
|
|
4474
|
+
e.stopPropagation();
|
|
4475
|
+
if (!isDisabled) {
|
|
4476
|
+
setIsDragging(true);
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
const handleDragLeave = (e) => {
|
|
4480
|
+
e.preventDefault();
|
|
4481
|
+
e.stopPropagation();
|
|
4482
|
+
setIsDragging(false);
|
|
4483
|
+
};
|
|
4484
|
+
const handleDrop = (e) => {
|
|
4485
|
+
e.preventDefault();
|
|
4486
|
+
e.stopPropagation();
|
|
4487
|
+
setIsDragging(false);
|
|
4488
|
+
if (isDisabled)
|
|
4489
|
+
return;
|
|
4490
|
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
4491
|
+
const validFiles = processFiles(e.dataTransfer.files);
|
|
4492
|
+
if (validFiles.length > 0) {
|
|
4493
|
+
handleFiles(validFiles);
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4497
|
+
const handleClick = () => {
|
|
4498
|
+
if (!isDisabled && fileInputRef.current) {
|
|
4499
|
+
fileInputRef.current.click();
|
|
4500
|
+
}
|
|
4501
|
+
};
|
|
4502
|
+
const handleRemoveFile = (e, uploadedFile) => {
|
|
4503
|
+
e.stopPropagation();
|
|
4504
|
+
if (isDisabled)
|
|
4505
|
+
return;
|
|
4506
|
+
// Revoke preview URL if exists
|
|
4507
|
+
if (uploadedFile.preview) {
|
|
4508
|
+
URL.revokeObjectURL(uploadedFile.preview);
|
|
4509
|
+
}
|
|
4510
|
+
const updatedFiles = uploadedFiles.filter((f) => f.id !== uploadedFile.id);
|
|
4511
|
+
setUploadedFiles(updatedFiles);
|
|
4512
|
+
if (onFileRemove) {
|
|
4513
|
+
onFileRemove(uploadedFile);
|
|
4514
|
+
}
|
|
4515
|
+
if (onChange) {
|
|
4516
|
+
// Only return File objects (filter out URL-only items)
|
|
4517
|
+
const filesToReturn = updatedFiles
|
|
4518
|
+
.map((uf) => uf.file)
|
|
4519
|
+
.filter((file) => file !== undefined);
|
|
4520
|
+
onChange(multiple ? filesToReturn : filesToReturn[0] || null);
|
|
4521
|
+
}
|
|
4522
|
+
};
|
|
4523
|
+
// Determine which helper text to show
|
|
4524
|
+
const displayHelperText = errorText || successText || helperText || error;
|
|
4525
|
+
const currentValidationState = errorText
|
|
4526
|
+
? "negative"
|
|
4527
|
+
: successText
|
|
4528
|
+
? "positive"
|
|
4529
|
+
: validationState;
|
|
4530
|
+
const sizeConfig = {
|
|
4531
|
+
small: {
|
|
4532
|
+
gap: "gap-2",
|
|
4533
|
+
iconSize: 16,
|
|
4534
|
+
textSize: "text-body-small-medium",
|
|
4535
|
+
},
|
|
4536
|
+
medium: {
|
|
4537
|
+
gap: "gap-2",
|
|
4538
|
+
iconSize: 16,
|
|
4539
|
+
textSize: "text-body-small-medium",
|
|
4540
|
+
},
|
|
4541
|
+
large: {
|
|
4542
|
+
gap: "gap-3",
|
|
4543
|
+
iconSize: 16,
|
|
4544
|
+
textSize: "text-body-small-medium",
|
|
4545
|
+
},
|
|
4546
|
+
};
|
|
4547
|
+
const config = sizeConfig[size];
|
|
4548
|
+
const defaultUploadText = "Click to upload or drag and drop";
|
|
4549
|
+
const defaultDragText = "Drop files here";
|
|
4550
|
+
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(uploadBoxVariants({
|
|
4551
|
+
size,
|
|
4552
|
+
validationState: currentValidationState,
|
|
4553
|
+
isDisabled,
|
|
4554
|
+
isDragging,
|
|
4555
|
+
}), uploadAreaClassName, className), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: handleClick, children: [jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: multiple, disabled: isDisabled, onChange: handleFileInputChange, className: "hidden", "aria-label": label || "File upload" }), uploadedFiles.length === 0 ? (jsxs("div", { className: "flex flex-col items-center justify-center gap-2 text-center", children: [jsx("div", { className: "flex items-center justify-center w-8 h-8 rounded-full bg-surface-fill-neutral-subtle", children: jsx(Icon, { name: "upload", size: config.iconSize }) }), jsxs("div", { className: "flex flex-col gap-1", children: [jsx(Text, { variant: "body", size: "small", weight: "medium", color: isDisabled ? "disabled" : "default", children: isDragging
|
|
4556
|
+
? dragText || defaultDragText
|
|
4557
|
+
: uploadText || defaultUploadText }), accept && (jsxs(Text, { variant: "caption", size: "small", color: isDisabled ? "disabled" : "muted", children: ["Accepted: ", accept] })), maxSize && (jsxs(Text, { variant: "caption", size: "small", color: isDisabled ? "disabled" : "muted", children: ["Max size: ", formatFileSize(maxSize)] }))] })] })) : (jsxs("div", { className: cn("w-full flex flex-col gap-3", previewClassName), children: [uploadedFiles.map((uploadedFile) => {
|
|
4558
|
+
const { file, url, preview, name, size, id } = uploadedFile;
|
|
4559
|
+
const fileName = name ||
|
|
4560
|
+
file?.name ||
|
|
4561
|
+
(url ? getFileNameFromUrl(url) : "Unknown file");
|
|
4562
|
+
const fileSize = size || file?.size;
|
|
4563
|
+
const canPreview = showPreview && preview;
|
|
4564
|
+
const isImage = isImageFile(file, url);
|
|
4565
|
+
const isPdf = isPdfFile(file, url);
|
|
4566
|
+
// For URLs, try to show as image if not explicitly a PDF
|
|
4567
|
+
// If it's a URL without extension, assume it might be an image
|
|
4568
|
+
// Always try to show URLs as images unless they're explicitly PDFs
|
|
4569
|
+
const shouldShowAsImage = canPreview && preview && (isImage || (url && !isPdf));
|
|
4570
|
+
return (jsxs("div", { className: "flex items-start gap-3 p-3 border border-surface-outline-neutral-muted rounded-large bg-surface-fill-neutral-intense", children: [shouldShowAsImage ? (jsx(ImagePreview, { src: preview, alt: fileName })) : canPreview && isPdf ? (jsx("div", { className: "shrink-0 w-16 h-16 rounded-medium border border-action-outline-neutral-faded flex items-center justify-center bg-surface-fill-neutral-subtle", children: jsx(Icon, { name: "file", size: 24, className: "text-surface-ink-neutral-subtle" }) })) : (jsx("div", { className: "shrink-0 w-16 h-16 rounded-medium border border-action-outline-neutral-faded flex items-center justify-center bg-surface-fill-neutral-subtle", children: jsx(Icon, { name: "file", size: 24, className: "text-surface-ink-neutral-subtle" }) })), jsxs("div", { className: "flex-1 min-w-0 flex flex-col gap-1", children: [jsx("div", { className: "flex items-center gap-2", children: jsx(Text, { variant: "body", size: "small", weight: "medium", color: isDisabled ? "disabled" : "default", as: "span", className: "truncate", title: fileName, children: fileName }) }), fileSize !== undefined && (jsx(Text, { variant: "caption", size: "small", color: isDisabled ? "disabled" : "muted", as: "span", children: formatFileSize(fileSize) }))] }), !isDisabled && (jsx(IconButton, { icon: "close", size: "xsmall", onClick: (e) => handleRemoveFile(e, uploadedFile), "aria-label": `Remove ${fileName}` }))] }, id));
|
|
4571
|
+
}), multiple && !isDisabled && (jsx(Button, { variant: "tertiary", color: "primary", size: "small", onClick: handleClick, leadingIcon: jsx(Icon, { name: "add", size: 16 }), className: "mt-2 w-fit", children: "Add more files" }))] }))] }), jsx(FormFooter, { helperText: displayHelperText || undefined, validationState: currentValidationState === "none"
|
|
4572
|
+
? "default"
|
|
4573
|
+
: currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" })] }));
|
|
4574
|
+
});
|
|
4575
|
+
UploadBox.displayName = "UploadBox";
|
|
4576
|
+
|
|
4577
|
+
export { Alert, Amount, Avatar, AvatarCell, Badge, Button, ButtonGroup, Checkbox, Counter, DatePicker, Divider, Dropdown, DropdownMenu, FormFooter, FormHeader, Icon, IconButton, IconCell, Link, ListItem, Modal, NumberCell, Pagination, Radio, SearchableDropdown, Select, SelectTextField, Skeleton, SlotCell, SpacerCell, Switch, TabItem, Table, TableDetailPanel, Tabs, Text, TextArea, TextField, Tooltip, UploadBox, alertVariants, avatarVariants, badgeVariants, buttonGroupVariants, buttonVariants, checkboxVariants, cn, counterVariants, datePickerVariants, dropdownVariants, getAvailableIcons, hasIcon, iconButtonVariants, iconRegistry, linkVariants, listItemVariants, paginationVariants, radioVariants, selectTriggerVariants, selectVariants, switchVariants, tableCellVariants, tableHeaderVariants, tableVariants, textAreaVariants, textFieldVariants, tooltipVariants, uploadBoxVariants };
|
|
4017
4578
|
//# sourceMappingURL=index.esm.js.map
|