UNPKG

aura-glass

Version:

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

446 lines (443 loc) 15.3 kB
'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