aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
446 lines (443 loc) • 15.3 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { GlassInput } from '../input/GlassInput.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { forwardRef, useState, useRef, useEffect, useCallback } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { GlassButton, IconButton } from '../button/GlassButton.js';
import { GlassBadge } from '../data-display/GlassBadge.js';
import { GlassProgress } from '../data-display/GlassProgress.js';
/**
* GlassFileUpload component
* File upload with drag-and-drop functionality and glassmorphism styling
*/
const GlassFileUpload = /*#__PURE__*/forwardRef(({
accept,
multiple = false,
maxSize = 10 * 1024 * 1024,
// 10MB
maxFiles = 10,
variant = "default",
size = "md",
disabled = false,
files = [],
onChange,
onUpload,
onRemove,
onPreview,
showPreviews = true,
showProgress = true,
instruction = "Drag and drop files here, or click to select",
helperText,
error,
autoUpload = false,
renderFile,
children,
className,
...props
}, ref) => {
const [internalFiles, setInternalFiles] = useState(files);
const [isDragOver, setIsDragOver] = useState(false);
const [isDragActive, setIsDragActive] = useState(false);
const fileInputRef = useRef(null);
const dropZoneRef = useRef(null);
const sizeClasses = {
sm: "glass-p-4 glass-text-sm",
md: "glass-p-6 glass-text-base",
lg: "p-8 glass-text-lg"
};
const variantClasses = {
default: "min-h-32",
compact: "min-h-20",
minimal: "min-h-16",
grid: "min-h-40"
};
// Sync internal files with props
useEffect(() => {
setInternalFiles(files);
}, [files]);
// Format file size
const formatFileSize = bytes => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
// Validate file
const validateFile = file => {
if (maxSize && file.size > maxSize) {
return `File size must be less than ${formatFileSize(maxSize)}`;
}
if (accept) {
const acceptedTypes = accept.split(",").map(type => type.trim());
const isAccepted = acceptedTypes.some(type => {
if (type.startsWith(".")) {
return file.name.toLowerCase().endsWith(type.toLowerCase());
}
return file.type.match(type.replace("*", ".*"));
});
if (!isAccepted) {
return `File type not accepted. Accepted types: ${accept}`;
}
}
return null;
};
// Create file object
const createFileObject = file => {
const id = Math.random().toString(36).substr(2, 9);
const validationError = validateFile(file);
return {
id,
file,
name: file.name,
size: file.size,
type: file.type,
status: validationError ? "error" : "pending",
error: validationError || undefined,
progress: 0
};
};
// Handle file selection
const handleFiles = useCallback(async fileList => {
if (disabled) return;
const newFiles = Array.from(fileList).map(createFileObject);
// Check max files limit
if (maxFiles && internalFiles.length + newFiles.length > maxFiles) {
console.warn(`Maximum ${maxFiles} files allowed`);
return;
}
const updatedFiles = multiple ? [...internalFiles, ...newFiles] : newFiles;
setInternalFiles(updatedFiles);
onChange?.(updatedFiles);
// Auto upload valid files
if (autoUpload && onUpload) {
for (const fileObj of newFiles) {
if (fileObj.status === "pending") {
await handleUpload(fileObj.id);
}
}
}
}, [disabled, maxFiles, internalFiles, multiple, onChange, autoUpload, onUpload]);
// Handle file upload
const handleUpload = async fileId => {
if (!onUpload) return;
const fileIndex = internalFiles.findIndex(f => f.id === fileId);
if (fileIndex === -1) return;
const fileObj = internalFiles[fileIndex];
// Update status to uploading
const updatedFiles = [...internalFiles];
updatedFiles[fileIndex] = {
...fileObj,
status: "uploading",
progress: 0
};
setInternalFiles(updatedFiles);
onChange?.(updatedFiles);
try {
// Simulate upload progress
const progressInterval = setInterval(() => {
setInternalFiles(current => {
const newFiles = [...current];
const currentFile = newFiles.find(f => f.id === fileId);
if (currentFile && currentFile.status === "uploading") {
currentFile.progress = Math.min((currentFile.progress || 0) + 10, 90);
}
return newFiles;
});
}, 200);
const result = await onUpload(fileObj.file);
clearInterval(progressInterval);
// Update file with result
const finalFiles = [...internalFiles];
const finalIndex = finalFiles.findIndex(f => f.id === fileId);
if (finalIndex !== -1) {
finalFiles[finalIndex] = {
...finalFiles[finalIndex],
status: "completed",
progress: 100,
url: result?.url
};
setInternalFiles(finalFiles);
onChange?.(finalFiles);
}
} catch (error) {
// Update file with error
const errorFiles = [...internalFiles];
const errorIndex = errorFiles.findIndex(f => f.id === fileId);
if (errorIndex !== -1) {
errorFiles[errorIndex] = {
...errorFiles[errorIndex],
status: "error",
error: error instanceof Error ? error.message : "Upload failed"
};
setInternalFiles(errorFiles);
onChange?.(errorFiles);
}
}
};
// Handle file removal
const handleRemove = fileId => {
const updatedFiles = internalFiles.filter(f => f.id !== fileId);
setInternalFiles(updatedFiles);
onChange?.(updatedFiles);
onRemove?.(fileId);
};
// Drag and drop handlers
const handleDragEnter = e => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(true);
};
const handleDragLeave = e => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(false);
setIsDragOver(false);
};
const handleDragOver = e => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(true);
};
const handleDrop = e => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(false);
setIsDragOver(false);
const {
files: droppedFiles
} = e.dataTransfer;
if (droppedFiles && droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
};
// Click to select files
const handleClick = () => {
if (!disabled) {
fileInputRef.current?.click();
}
};
// File input change
const handleInputChange = e => {
const {
files
} = e.target;
if (files && files.length > 0) {
handleFiles(files);
}
// Reset input value to allow selecting the same file again
e.target.value = "";
};
// Get file icon
const getFileIcon = type => {
if (type.startsWith("image/")) {
return jsx("svg", {
"data-glass-component": true,
className: 'w-5 h-5',
fill: "none",
stroke: "currentColor",
viewBox: "0 0 24 24",
children: jsx("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
})
});
}
if (type.includes("pdf")) {
return jsx("svg", {
className: 'w-5 h-5',
fill: "none",
stroke: "currentColor",
viewBox: "0 0 24 24",
children: jsx("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
})
});
}
return jsx("svg", {
className: 'w-5 h-5',
fill: "none",
stroke: "currentColor",
viewBox: "0 0 24 24",
children: jsx("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
})
});
};
// Render file item
const renderFileItem = (file, index) => {
if (renderFile) {
return renderFile(file, index);
}
return jsx(MotionFramer, {
preset: "slideDown",
children: jsx(OptimizedGlassCore, {
intent: "neutral",
elevation: "level1",
intensity: "medium",
depth: 2,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
className: cn("glass-p-3 border border-border/20", file.status === "error" && "border-destructive/50 bg-destructive/5"),
children: jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3",
children: [jsx("div", {
className: "glass-flex-shrink-0",
children: showPreviews && file.type.startsWith("image/") && file.preview ? jsx("img", {
src: file.preview,
alt: file.name,
className: 'w-10 h-10 object-cover glass-radius-md'
}) : jsx("div", {
className: 'w-10 h-10 glass-radius-md glass-surface-subtle glass-flex glass-items-center glass-justify-center',
children: getFileIcon(file.type)
})
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [jsx("p", {
className: 'glass-text-sm font-medium text-primary truncate',
children: file.name
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2 glass-mt-1",
children: [jsx("span", {
className: "glass-text-xs glass-text-secondary",
children: formatFileSize(file.size)
}), jsx(GlassBadge, {
variant: file.status === "completed" ? "success" : file.status === "error" ? "error" : file.status === "uploading" ? "primary" : "outline",
size: "xs",
children: file.status
})]
}), file.status === "uploading" && showProgress && jsx(GlassProgress, {
value: file.progress,
size: "xs",
className: "glass-mt-2"
}), file.error && jsx("p", {
className: 'glass-text-xs text-destructive glass-mt-1',
children: file.error
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-1",
children: [file.status === "pending" && onUpload && jsx(IconButton, {
icon: "\u2191",
variant: "ghost",
size: "sm",
onClick: e => handleUpload(file.id),
"aria-label": "Upload file"
}), file.status === "completed" && onPreview && jsx(IconButton, {
icon: "\uD83D\uDC41",
variant: "ghost",
size: "sm",
onClick: e => onPreview(file),
"aria-label": "Preview file"
}), jsx(IconButton, {
icon: "\u00D7",
variant: "ghost",
size: "sm",
onClick: e => handleRemove(file.id),
"aria-label": "Remove file"
})]
})]
})
})
}, file.id);
};
return jsxs("div", {
ref: ref,
className: cn("w-full", className),
...props,
children: [jsx(GlassInput, {
ref: fileInputRef,
type: "file",
accept: accept,
multiple: multiple,
onChange: handleInputChange,
className: 'hidden',
disabled: disabled
}), jsx(OptimizedGlassCore, {
variant: "frosted",
elevation: isDragOver ? "level2" : "level1",
intensity: "medium",
depth: 2,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
ref: dropZoneRef,
className: cn("relative border-2 border-dashed cursor-pointer transition-all", "hover:border-primary/50 focus:border-primary focus:outline-none", sizeClasses[size], variantClasses[variant], {
"border-primary bg-primary/5": isDragOver,
"border-border/30": !isDragOver && !error,
"border-destructive bg-destructive/5": error,
"opacity-50 cursor-not-allowed": disabled
}),
onDragEnter: handleDragEnter,
onDragLeave: handleDragLeave,
onDragOver: handleDragOver,
onDrop: handleDrop,
onClick: handleClick,
tabIndex: disabled ? -1 : 0,
role: "button",
"aria-label": "File upload area",
children: children || jsxs("div", {
className: 'glass-flex glass-flex-col glass-items-center glass-justify-center text-center',
children: [jsx("svg", {
className: cn("mx-auto mb-3 glass-text-secondary", size === "sm" ? "w-8 h-8" : size === "lg" ? "w-12 h-12" : "w-10 h-10"),
fill: "none",
stroke: "currentColor",
viewBox: "0 0 24 24",
children: jsx("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
})
}), jsx("p", {
className: 'text-primary font-medium mb-1',
children: instruction
}), helperText && jsx("p", {
className: "glass-text-sm glass-text-secondary",
children: helperText
}), maxSize && jsxs("p", {
className: "glass-text-xs glass-text-secondary glass-mt-2",
children: ["Max file size: ", formatFileSize(maxSize)]
})]
})
}), error && jsx("p", {
className: 'glass-text-sm text-destructive glass-mt-2',
children: error
}), internalFiles.length > 0 && jsx("div", {
className: cn("glass-mt-4 glass-gap-2", {
"grid grid-cols-2 md:grid-cols-3 glass-gap-3": variant === "grid"
}),
children: internalFiles.map((file, index) => renderFileItem(file, index))
}), internalFiles.some(f => f.status === "pending") && onUpload && !autoUpload && jsx("div", {
className: "glass-mt-4 glass-flex glass-justify-end",
children: jsx(GlassButton, {
variant: "default",
size: "sm",
onClick: e => {
internalFiles.filter(f => f.status === "pending").forEach(f => handleUpload(f.id));
},
disabled: disabled,
children: "Upload All"
})
})]
});
});
GlassFileUpload.displayName = "GlassFileUpload";
export { GlassFileUpload };
//# sourceMappingURL=GlassFileUpload.js.map