infinity-ui-elements 1.8.13 → 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/index.js CHANGED
@@ -82,6 +82,14 @@ const iconRegistry = {
82
82
  close: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
83
83
  <path d="M18 6L6 18M6 6L18 18" stroke="#081416" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
84
84
  </svg>`,
85
+ upload: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
86
+ <path d="M4 19H20V12H22V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V12H4V19ZM13 9V16H11V9H6L12 3L18 9H13Z" fill="#081416"/>
87
+ </svg>
88
+ `,
89
+ file: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
90
+ <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"/>
91
+ </svg>
92
+ `,
85
93
  };
86
94
  const Icon = ({ name, size = 24, className = "", style = {}, ...props }) => {
87
95
  const svgContent = iconRegistry[name];
@@ -4236,6 +4244,357 @@ const TextArea = React__namespace.forwardRef(({ label, helperText, errorText, su
4236
4244
  });
4237
4245
  TextArea.displayName = "TextArea";
4238
4246
 
4247
+ const uploadBoxVariants = classVarianceAuthority.cva("relative flex flex-col items-center justify-center border-2 border-dashed rounded-large transition-all font-display", {
4248
+ variants: {
4249
+ size: {
4250
+ small: "min-h-[120px] p-4 gap-2",
4251
+ medium: "min-h-[160px] p-6 gap-3",
4252
+ large: "min-h-[200px] p-8 gap-4",
4253
+ },
4254
+ validationState: {
4255
+ none: `
4256
+ border-action-outline-neutral-faded
4257
+ hover:border-action-outline-primary-hover
4258
+ focus-within:border-action-outline-primary-hover
4259
+ focus-within:ring-2
4260
+ ring-action-outline-primary-faded-hover
4261
+ bg-surface-fill-neutral-intense`,
4262
+ positive: `
4263
+ border-action-outline-positive-default
4264
+ focus-within:border-action-outline-positive-hover
4265
+ focus-within:ring-2
4266
+ ring-action-outline-positive-faded-hover
4267
+ bg-surface-fill-neutral-intense`,
4268
+ negative: `
4269
+ border-action-outline-negative-default
4270
+ focus-within:border-action-outline-negative-hover
4271
+ focus-within:ring-2
4272
+ ring-action-outline-negative-faded-hover
4273
+ bg-surface-fill-neutral-intense`,
4274
+ },
4275
+ isDisabled: {
4276
+ true: `
4277
+ border-[var(--border-width-thinner)]
4278
+ hover:border-action-outline-neutral-disabled
4279
+ border-action-outline-neutral-disabled
4280
+ bg-surface-fill-neutral-intense
4281
+ cursor-not-allowed
4282
+ opacity-60`,
4283
+ false: "cursor-pointer",
4284
+ },
4285
+ isDragging: {
4286
+ true: "border-action-outline-primary-hover bg-action-fill-primary-faded",
4287
+ false: "",
4288
+ },
4289
+ },
4290
+ defaultVariants: {
4291
+ size: "medium",
4292
+ validationState: "none",
4293
+ isDisabled: false,
4294
+ isDragging: false,
4295
+ },
4296
+ });
4297
+ const formatFileSize = (bytes) => {
4298
+ if (bytes === 0)
4299
+ return "0 Bytes";
4300
+ const k = 1024;
4301
+ const sizes = ["Bytes", "KB", "MB", "GB"];
4302
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
4303
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
4304
+ };
4305
+ const isImageFile = (file, url) => {
4306
+ if (file) {
4307
+ return file.type.startsWith("image/");
4308
+ }
4309
+ if (url) {
4310
+ const lowerUrl = url.toLowerCase();
4311
+ return (lowerUrl.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?.*)?$/i) !== null ||
4312
+ url.match(/data:image\//) !== null);
4313
+ }
4314
+ return false;
4315
+ };
4316
+ const isPdfFile = (file, url) => {
4317
+ if (file) {
4318
+ return file.type === "application/pdf";
4319
+ }
4320
+ if (url) {
4321
+ return url.toLowerCase().match(/\.pdf(\?.*)?$/i) !== null;
4322
+ }
4323
+ return false;
4324
+ };
4325
+ const getFileNameFromUrl = (url) => {
4326
+ try {
4327
+ const urlObj = new URL(url);
4328
+ const pathname = urlObj.pathname;
4329
+ const fileName = pathname.split("/").pop() || "file";
4330
+ return decodeURIComponent(fileName);
4331
+ }
4332
+ catch {
4333
+ // If URL parsing fails, try to extract from the string
4334
+ const parts = url.split("/");
4335
+ const lastPart = parts[parts.length - 1] || "file";
4336
+ return decodeURIComponent(lastPart.split("?")[0]);
4337
+ }
4338
+ };
4339
+ // Component to handle image preview with error fallback
4340
+ const ImagePreview = ({ src, alt }) => {
4341
+ const [hasError, setHasError] = React__namespace.useState(false);
4342
+ if (hasError) {
4343
+ return (jsxRuntime.jsx("div", { className: "shrink-0 w-16 h-16 rounded-medium flex items-center justify-center bg-surface-fill-neutral-subtle", children: jsxRuntime.jsx(Icon, { name: "file", size: 24, className: "text-surface-ink-neutral-subtle" }) }));
4344
+ }
4345
+ return (jsxRuntime.jsx("div", { className: "shrink-0 w-16 h-16 rounded-medium overflow-hidden ", children: jsxRuntime.jsx("img", { src: src, alt: alt, className: "w-full h-full object-cover", onError: () => setHasError(true) }) }));
4346
+ };
4347
+ const UploadBox = React__namespace.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) => {
4348
+ const fileInputRef = React__namespace.useRef(null);
4349
+ const [isDragging, setIsDragging] = React__namespace.useState(false);
4350
+ const [uploadedFiles, setUploadedFiles] = React__namespace.useState([]);
4351
+ const [error, setError] = React__namespace.useState(null);
4352
+ // Initialize uploaded files from value prop
4353
+ React__namespace.useEffect(() => {
4354
+ if (value) {
4355
+ const filesArray = Array.isArray(value) ? value : [value];
4356
+ const processedFiles = filesArray.map((item) => {
4357
+ if (item instanceof File) {
4358
+ const uploadedFile = {
4359
+ file: item,
4360
+ name: item.name,
4361
+ size: item.size,
4362
+ id: `${item.name}-${item.size}-${item.lastModified}`,
4363
+ };
4364
+ if (isImageFile(item) && showPreview) {
4365
+ uploadedFile.preview = URL.createObjectURL(item);
4366
+ }
4367
+ return uploadedFile;
4368
+ }
4369
+ else if (typeof item === "string") {
4370
+ // It's a URL string
4371
+ const uploadedFile = {
4372
+ url: item,
4373
+ name: getFileNameFromUrl(item),
4374
+ id: `url-${item}-${Date.now()}`,
4375
+ };
4376
+ // Always set preview for URLs when showPreview is true
4377
+ // The rendering logic will determine if it's an image or PDF
4378
+ if (showPreview) {
4379
+ uploadedFile.preview = item;
4380
+ }
4381
+ return uploadedFile;
4382
+ }
4383
+ else {
4384
+ // It's already an UploadedFile
4385
+ const uploadedFile = item;
4386
+ // If it has a URL but no preview set, set it
4387
+ if (uploadedFile.url && !uploadedFile.preview && showPreview) {
4388
+ uploadedFile.preview = uploadedFile.url;
4389
+ }
4390
+ return uploadedFile;
4391
+ }
4392
+ });
4393
+ setUploadedFiles(processedFiles);
4394
+ }
4395
+ else {
4396
+ setUploadedFiles([]);
4397
+ }
4398
+ }, [value, showPreview]);
4399
+ // Cleanup preview URLs (only revoke object URLs, not regular URLs)
4400
+ React__namespace.useEffect(() => {
4401
+ return () => {
4402
+ uploadedFiles.forEach((uploadedFile) => {
4403
+ // Only revoke object URLs (created with URL.createObjectURL)
4404
+ // Regular URLs (http/https) should not be revoked
4405
+ if (uploadedFile.preview &&
4406
+ uploadedFile.preview.startsWith("blob:")) {
4407
+ URL.revokeObjectURL(uploadedFile.preview);
4408
+ }
4409
+ });
4410
+ };
4411
+ }, [uploadedFiles]);
4412
+ const processFiles = (files) => {
4413
+ const fileArray = Array.from(files);
4414
+ const validFiles = [];
4415
+ let currentError = null;
4416
+ // Check max files limit
4417
+ if (maxFiles && uploadedFiles.length + fileArray.length > maxFiles) {
4418
+ currentError = `Maximum ${maxFiles} file(s) allowed`;
4419
+ setError(currentError);
4420
+ return validFiles;
4421
+ }
4422
+ fileArray.forEach((file) => {
4423
+ // Check file size
4424
+ if (maxSize && file.size > maxSize) {
4425
+ currentError = `File "${file.name}" exceeds maximum size of ${formatFileSize(maxSize)}`;
4426
+ setError(currentError);
4427
+ return;
4428
+ }
4429
+ // Check if file type is accepted
4430
+ if (accept) {
4431
+ const acceptedTypes = accept.split(",").map((type) => type.trim());
4432
+ const isAccepted = acceptedTypes.some((type) => {
4433
+ if (type.startsWith(".")) {
4434
+ return file.name.toLowerCase().endsWith(type.toLowerCase());
4435
+ }
4436
+ if (type.includes("/*")) {
4437
+ const baseType = type.split("/")[0];
4438
+ return file.type.startsWith(baseType + "/");
4439
+ }
4440
+ return file.type === type;
4441
+ });
4442
+ if (!isAccepted) {
4443
+ currentError = `File type "${file.type}" is not accepted`;
4444
+ setError(currentError);
4445
+ return;
4446
+ }
4447
+ }
4448
+ validFiles.push(file);
4449
+ });
4450
+ if (currentError) {
4451
+ setError(currentError);
4452
+ }
4453
+ else {
4454
+ setError(null);
4455
+ }
4456
+ return validFiles;
4457
+ };
4458
+ const handleFiles = (files) => {
4459
+ if (files.length === 0)
4460
+ return;
4461
+ const newUploadedFiles = files.map((file) => {
4462
+ const uploadedFile = {
4463
+ file,
4464
+ id: `${file.name}-${file.size}-${file.lastModified}-${Date.now()}`,
4465
+ };
4466
+ if (isImageFile(file) && showPreview) {
4467
+ uploadedFile.preview = URL.createObjectURL(file);
4468
+ }
4469
+ return uploadedFile;
4470
+ });
4471
+ const updatedFiles = multiple
4472
+ ? [...uploadedFiles, ...newUploadedFiles]
4473
+ : newUploadedFiles;
4474
+ setUploadedFiles(updatedFiles);
4475
+ // Call onChange with File objects only (filter out URL-only items)
4476
+ if (onChange) {
4477
+ const filesToReturn = updatedFiles
4478
+ .map((uf) => uf.file)
4479
+ .filter((file) => file !== undefined);
4480
+ onChange(multiple ? filesToReturn : filesToReturn[0] || null);
4481
+ }
4482
+ };
4483
+ const handleFileInputChange = (e) => {
4484
+ if (e.target.files && e.target.files.length > 0) {
4485
+ const validFiles = processFiles(e.target.files);
4486
+ if (validFiles.length > 0) {
4487
+ handleFiles(validFiles);
4488
+ }
4489
+ // Reset input value to allow selecting the same file again
4490
+ e.target.value = "";
4491
+ }
4492
+ };
4493
+ const handleDragOver = (e) => {
4494
+ e.preventDefault();
4495
+ e.stopPropagation();
4496
+ if (!isDisabled) {
4497
+ setIsDragging(true);
4498
+ }
4499
+ };
4500
+ const handleDragLeave = (e) => {
4501
+ e.preventDefault();
4502
+ e.stopPropagation();
4503
+ setIsDragging(false);
4504
+ };
4505
+ const handleDrop = (e) => {
4506
+ e.preventDefault();
4507
+ e.stopPropagation();
4508
+ setIsDragging(false);
4509
+ if (isDisabled)
4510
+ return;
4511
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
4512
+ const validFiles = processFiles(e.dataTransfer.files);
4513
+ if (validFiles.length > 0) {
4514
+ handleFiles(validFiles);
4515
+ }
4516
+ }
4517
+ };
4518
+ const handleClick = () => {
4519
+ if (!isDisabled && fileInputRef.current) {
4520
+ fileInputRef.current.click();
4521
+ }
4522
+ };
4523
+ const handleRemoveFile = (e, uploadedFile) => {
4524
+ e.stopPropagation();
4525
+ if (isDisabled)
4526
+ return;
4527
+ // Revoke preview URL if exists
4528
+ if (uploadedFile.preview) {
4529
+ URL.revokeObjectURL(uploadedFile.preview);
4530
+ }
4531
+ const updatedFiles = uploadedFiles.filter((f) => f.id !== uploadedFile.id);
4532
+ setUploadedFiles(updatedFiles);
4533
+ if (onFileRemove) {
4534
+ onFileRemove(uploadedFile);
4535
+ }
4536
+ if (onChange) {
4537
+ // Only return File objects (filter out URL-only items)
4538
+ const filesToReturn = updatedFiles
4539
+ .map((uf) => uf.file)
4540
+ .filter((file) => file !== undefined);
4541
+ onChange(multiple ? filesToReturn : filesToReturn[0] || null);
4542
+ }
4543
+ };
4544
+ // Determine which helper text to show
4545
+ const displayHelperText = errorText || successText || helperText || error;
4546
+ const currentValidationState = errorText
4547
+ ? "negative"
4548
+ : successText
4549
+ ? "positive"
4550
+ : validationState;
4551
+ const sizeConfig = {
4552
+ small: {
4553
+ gap: "gap-2",
4554
+ iconSize: 16,
4555
+ textSize: "text-body-small-medium",
4556
+ },
4557
+ medium: {
4558
+ gap: "gap-2",
4559
+ iconSize: 16,
4560
+ textSize: "text-body-small-medium",
4561
+ },
4562
+ large: {
4563
+ gap: "gap-3",
4564
+ iconSize: 16,
4565
+ textSize: "text-body-small-medium",
4566
+ },
4567
+ };
4568
+ const config = sizeConfig[size];
4569
+ const defaultUploadText = "Click to upload or drag and drop";
4570
+ const defaultDragText = "Drop files here";
4571
+ return (jsxRuntime.jsxs("div", { ref: ref, className: cn("w-full flex flex-col", config.gap, containerClassName), ...props, children: [label && (jsxRuntime.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 })), jsxRuntime.jsxs("div", { className: cn(uploadBoxVariants({
4572
+ size,
4573
+ validationState: currentValidationState,
4574
+ isDisabled,
4575
+ isDragging,
4576
+ }), uploadAreaClassName, className), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: handleClick, children: [jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: multiple, disabled: isDisabled, onChange: handleFileInputChange, className: "hidden", "aria-label": label || "File upload" }), uploadedFiles.length === 0 ? (jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center gap-2 text-center", children: [jsxRuntime.jsx("div", { className: "flex items-center justify-center w-8 h-8 rounded-full bg-surface-fill-neutral-subtle", children: jsxRuntime.jsx(Icon, { name: "upload", size: config.iconSize }) }), jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [jsxRuntime.jsx(Text, { variant: "body", size: "small", weight: "medium", color: isDisabled ? "disabled" : "default", children: isDragging
4577
+ ? dragText || defaultDragText
4578
+ : uploadText || defaultUploadText }), accept && (jsxRuntime.jsxs(Text, { variant: "caption", size: "small", color: isDisabled ? "disabled" : "muted", children: ["Accepted: ", accept] })), maxSize && (jsxRuntime.jsxs(Text, { variant: "caption", size: "small", color: isDisabled ? "disabled" : "muted", children: ["Max size: ", formatFileSize(maxSize)] }))] })] })) : (jsxRuntime.jsxs("div", { className: cn("w-full flex flex-col gap-3", previewClassName), children: [uploadedFiles.map((uploadedFile) => {
4579
+ const { file, url, preview, name, size, id } = uploadedFile;
4580
+ const fileName = name ||
4581
+ file?.name ||
4582
+ (url ? getFileNameFromUrl(url) : "Unknown file");
4583
+ const fileSize = size || file?.size;
4584
+ const canPreview = showPreview && preview;
4585
+ const isImage = isImageFile(file, url);
4586
+ const isPdf = isPdfFile(file, url);
4587
+ // For URLs, try to show as image if not explicitly a PDF
4588
+ // If it's a URL without extension, assume it might be an image
4589
+ // Always try to show URLs as images unless they're explicitly PDFs
4590
+ const shouldShowAsImage = canPreview && preview && (isImage || (url && !isPdf));
4591
+ return (jsxRuntime.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 ? (jsxRuntime.jsx(ImagePreview, { src: preview, alt: fileName })) : canPreview && isPdf ? (jsxRuntime.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: jsxRuntime.jsx(Icon, { name: "file", size: 24, className: "text-surface-ink-neutral-subtle" }) })) : (jsxRuntime.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: jsxRuntime.jsx(Icon, { name: "file", size: 24, className: "text-surface-ink-neutral-subtle" }) })), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0 flex flex-col gap-1", children: [jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: jsxRuntime.jsx(Text, { variant: "body", size: "small", weight: "medium", color: isDisabled ? "disabled" : "default", as: "span", className: "truncate", title: fileName, children: fileName }) }), fileSize !== undefined && (jsxRuntime.jsx(Text, { variant: "caption", size: "small", color: isDisabled ? "disabled" : "muted", as: "span", children: formatFileSize(fileSize) }))] }), !isDisabled && (jsxRuntime.jsx(IconButton, { icon: "close", size: "xsmall", onClick: (e) => handleRemoveFile(e, uploadedFile), "aria-label": `Remove ${fileName}` }))] }, id));
4592
+ }), multiple && !isDisabled && (jsxRuntime.jsx(Button, { variant: "tertiary", color: "primary", size: "small", onClick: handleClick, leadingIcon: jsxRuntime.jsx(Icon, { name: "add", size: 16 }), className: "mt-2 w-fit", children: "Add more files" }))] }))] }), jsxRuntime.jsx(FormFooter, { helperText: displayHelperText || undefined, validationState: currentValidationState === "none"
4593
+ ? "default"
4594
+ : currentValidationState, size: size, isDisabled: isDisabled, className: "mt-1" })] }));
4595
+ });
4596
+ UploadBox.displayName = "UploadBox";
4597
+
4239
4598
  exports.Alert = Alert;
4240
4599
  exports.Amount = Amount;
4241
4600
  exports.Avatar = Avatar;
@@ -4275,6 +4634,7 @@ exports.Text = Text;
4275
4634
  exports.TextArea = TextArea;
4276
4635
  exports.TextField = TextField;
4277
4636
  exports.Tooltip = Tooltip;
4637
+ exports.UploadBox = UploadBox;
4278
4638
  exports.alertVariants = alertVariants;
4279
4639
  exports.avatarVariants = avatarVariants;
4280
4640
  exports.badgeVariants = badgeVariants;
@@ -4302,4 +4662,5 @@ exports.tableVariants = tableVariants;
4302
4662
  exports.textAreaVariants = textAreaVariants;
4303
4663
  exports.textFieldVariants = textFieldVariants;
4304
4664
  exports.tooltipVariants = tooltipVariants;
4665
+ exports.uploadBoxVariants = uploadBoxVariants;
4305
4666
  //# sourceMappingURL=index.js.map