UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

378 lines (375 loc) 13 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useState, useRef, useCallback } from 'react'; import { GlassCore } from '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import '../../primitives/motion/MotionFramer.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { useA11yId } from '../../utils/a11y.js'; const GlassFileUpload = ({ accept, maxSize = 10 * 1024 * 1024, // 10MB default maxFiles = 5, multiple = true, onUpload, onChange, disabled = false, uploadText = "Drag and drop files here, or click to browse", browseText = "Browse Files", showPreview = true, validator, uploadUrl, className, ...props }) => { const [files, setFiles] = useState([]); const [isDragOver, setIsDragOver] = useState(false); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); const inputId = useA11yId("file-upload-input"); // File validation const validateFile = useCallback(file => { // Custom validator first if (validator) { const customError = validator(file); if (customError) return customError; } // Size validation if (file.size > maxSize) { return `File size exceeds ${formatFileSize(maxSize)} limit`; } // Type validation if (accept) { const acceptedTypes = accept.split(",").map(type => type.trim()); const isValidType = acceptedTypes.some(type => { if (type.startsWith(".")) { return file.name.toLowerCase().endsWith(type.toLowerCase()); } else if (type.includes("/*")) { return file.type.startsWith(type.split("/")[0]); } else { return file.type === type; } }); if (!isValidType) { return `File type not supported. Accepted types: ${accept}`; } } return null; }, [accept, maxSize, validator]); // Handle file selection const handleFiles = useCallback(newFiles => { const fileArray = Array.from(newFiles); // Check max files limit if (files.length + fileArray.length > maxFiles) { alert(`Maximum ${maxFiles} files allowed`); return; } const validFiles = []; const invalidFiles = []; fileArray.forEach(file => { const error = validateFile(file); if (error) { invalidFiles.push(`${file.name}: ${error}`); } else { validFiles.push({ file, id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, progress: 0, status: "uploading" }); } }); if (invalidFiles.length > 0) { alert(`Invalid files:\n${invalidFiles.join("\n")}`); } if (validFiles.length > 0) { const updatedFiles = [...files, ...validFiles]; setFiles(updatedFiles); onChange?.(updatedFiles); // Start upload if handler provided if (onUpload || uploadUrl) { uploadFiles(validFiles); } else { // Mark as completed if no upload handler const completedFiles = validFiles.map(f => ({ ...f, status: "completed", progress: 100 })); setFiles(prev => prev.map(f => completedFiles.find(cf => cf.id === f.id) || f)); } } }, [files, maxFiles, validateFile, onChange, onUpload, uploadUrl]); // Upload files const uploadFiles = async filesToUpload => { setIsUploading(true); for (const uploadedFile of filesToUpload) { try { if (onUpload) { // Custom upload handler await onUpload([uploadedFile.file]); updateFileStatus(uploadedFile.id, "completed", 100); } else if (uploadUrl) { // Upload to URL endpoint await uploadFileToUrl(uploadedFile); } } catch (error) { updateFileStatus(uploadedFile.id, "error", 0, error instanceof Error ? error.message : "Upload failed"); } } setIsUploading(false); }; // Upload file to URL const uploadFileToUrl = async uploadedFile => { const formData = new FormData(); formData.append("file", uploadedFile.file); const xhr = new XMLHttpRequest(); return new Promise((resolve, reject) => { xhr.upload.onprogress = e => { if (e.lengthComputable) { const progress = Math.round(e.loaded / e.total * 100); updateFileProgress(uploadedFile.id, progress); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { updateFileStatus(uploadedFile.id, "completed", 100); resolve(); } else { reject(new Error(`Upload failed with status ${xhr.status}`)); } }; xhr.onerror = () => reject(new Error("Network error")); xhr.open("POST", uploadUrl); xhr.send(formData); }); }; // Update file progress const updateFileProgress = (id, progress) => { setFiles(prev => prev.map(f => f.id === id ? { ...f, progress } : f)); }; // Update file status const updateFileStatus = (id, status, progress, error) => { setFiles(prev => prev.map(f => f.id === id ? { ...f, status, progress: progress ?? f.progress, error } : f)); }; // Remove file const removeFile = id => { const updatedFiles = files.filter(f => f.id !== id); setFiles(updatedFiles); onChange?.(updatedFiles); }; // Drag and drop handlers const handleDragOver = useCallback(e => { e.preventDefault(); e.stopPropagation(); if (!disabled) { setIsDragOver(true); } }, [disabled]); const handleDragLeave = useCallback(e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []); const handleDrop = useCallback(e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); if (disabled) return; const droppedFiles = e.dataTransfer.files; if (droppedFiles.length > 0) { handleFiles(droppedFiles); } }, [disabled, handleFiles]); // File input click handler const handleClick = () => { if (!disabled) { fileInputRef.current?.click(); } }; // File input change handler const handleFileInputChange = e => { const selectedFiles = e.target.files; if (selectedFiles) { handleFiles(selectedFiles); } // Reset input value if (fileInputRef.current) { fileInputRef.current.value = ""; } }; // 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]; }; // Get file icon const getFileIcon = file => { const type = file.type.toLowerCase(); if (type.startsWith("image/")) return "🖼️"; if (type.startsWith("video/")) return "🎥"; if (type.startsWith("audio/")) return "🎵"; if (type.includes("pdf")) return "📄"; if (type.includes("word") || type.includes("document")) return "📝"; if (type.includes("excel") || type.includes("spreadsheet")) return "📊"; if (type.includes("powerpoint") || type.includes("presentation")) return "📽️"; if (type.includes("zip") || type.includes("archive")) return "📦"; return "📄"; }; // Render file preview const renderFilePreview = uploadedFile => { const { file, progress, status, error } = uploadedFile; return jsxs("div", { "data-glass-component": true, role: "listitem", className: "glass-flex glass-items-center glass-p-3 glass-surface-subtle glass-radius-lg glass-border", children: [jsx("div", { className: 'glass-text-2xl mr-3', "aria-hidden": "true", children: getFileIcon(file) }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsx("p", { className: 'glass-text-sm font-medium text-primary truncate', children: file.name }), jsx("button", { onClick: () => removeFile(uploadedFile.id), className: 'ml-2 text-primary hover:text-primary glass-text-sm', disabled: status === "uploading", "aria-label": `Remove ${file.name}`, children: "\u2715" })] }), jsxs("p", { className: "glass-text-xs glass-text-secondary", children: [formatFileSize(file.size), status === "completed" && jsx("span", { className: 'text-primary ml-2', role: "status", children: "\u2713 Completed" }), status === "error" && jsxs("span", { className: 'text-primary ml-2', role: "alert", children: ["\u2717 ", error] })] }), status === "uploading" && jsxs("div", { className: 'mt-2', role: "progressbar", "aria-valuenow": progress, "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": `Uploading ${file.name}`, children: [jsx("div", { className: 'glass-w-full glass-surface-subtle glass-radius-full h-1', children: jsx("div", { className: 'glass-surface-blue h-1 glass-radius-full transition-all duration-300', style: { width: `${progress}%` } }) }), jsxs("p", { className: 'glass-text-xs glass-text-secondary mt-1', children: [progress, "%"] })] })] })] }, uploadedFile.id); }; return jsxs("div", { className: cn("w-full", className), ...props, children: [jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: multiple, onChange: handleFileInputChange, className: 'hidden glass-touch-target glass-contrast-guard', disabled: disabled, "aria-label": "File upload input", id: inputId }), jsx("label", { htmlFor: inputId, role: "button", "aria-label": uploadText, "aria-describedby": "file-upload-instructions", "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, className: "block cursor-pointer", onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: e => { if (disabled) { e.preventDefault(); } }, onKeyDown: e => { if ((e.key === "Enter" || e.key === " ") && !disabled) { e.preventDefault(); handleClick(); } }, children: jsx(GlassCore, { role: "presentation", className: cn("border-2 border-dashed glass-p-8 text-center transition-all duration-200", isDragOver && !disabled && "border-primary bg-primary/5", disabled ? "opacity-50 cursor-not-allowed" : "hover:border-primary hover:bg-primary/5", files.length > 0 ? "glass-radius-t-xl" : "glass-radius-xl"), children: jsxs("div", { className: "glass-flex glass-flex-col glass-items-center glass-gap-4 pointer-events-none", children: [jsx("div", { className: 'glass-text-4xl opacity-60', children: isUploading ? "⏳" : "📁" }), jsxs("div", { children: [jsx("p", { className: 'glass-text-lg font-medium text-primary mb-2', children: isUploading ? "Uploading..." : uploadText }), jsxs("p", { id: "file-upload-instructions", className: "glass-text-secondary", children: [accept && `Accepted formats: ${accept}`, maxSize && ` • Max size: ${formatFileSize(maxSize)}`, multiple && ` • Max files: ${maxFiles}`] })] }), !isUploading && jsx("span", { className: 'glass-px-4 glass-py-2 glass-surface-primary text-primary-foreground glass-radius-lg transition-colors font-medium inline-flex', "aria-hidden": "true", children: browseText })] }) }) }), showPreview && files.length > 0 && jsxs(GlassCore, { className: 'glass-border-t-0 glass-radius-b-xl glass-p-4 space-y-3', role: "region", "aria-label": "Uploaded files list", children: [jsxs("h4", { id: "uploaded-files-heading", className: 'font-medium text-primary mb-3', children: ["Uploaded Files (", files.length, "/", maxFiles, ")"] }), jsx("div", { role: "list", "aria-labelledby": "uploaded-files-heading", children: files.map(renderFilePreview) })] })] }); }; GlassFileUpload.displayName = "GlassFileUpload"; export { GlassFileUpload }; //# sourceMappingURL=GlassFileUpload.js.map