UNPKG

aura-glass

Version:

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

532 lines (529 loc) 20.5 kB
'use client'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { AnimatePresence, motion } from 'framer-motion'; import { Folder, FileText, Code, Archive, Music, Video, Image, Home, ArrowUp, RefreshCw, Plus, Upload, List, Grid, Search, Check, X, MoreVertical } from 'lucide-react'; import React, { useState, useRef, useMemo, 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 '../../primitives/motion/MotionFramer.js'; import { GlassButton } from '../button/GlassButton.js'; import { GlassInput } from '../input/GlassInput.js'; import { GlassBreadcrumb, GlassBreadcrumbItem } from '../navigation/GlassBreadcrumb.js'; const GlassFileExplorer = /*#__PURE__*/React.forwardRef(({ currentPath, files, onNavigate, onFileSelect, onFileOpen, onFileDelete, onFileRename, onFileMove, onFileCopy, onFolderCreate, onFileUpload, onRefresh, selectedFiles = [], onSelectionChange, loading = false, viewMode = "list", onViewModeChange, showHiddenFiles = false, onShowHiddenFilesChange, searchQuery = "", onSearchChange, className, showToolbar = true, showBreadcrumb = true, showSearch = true, allowMultiSelect = true, allowDragDrop = true, variant = "default", size = "md", elevation = "medium", ...props }, ref) => { const prefersReducedMotion = useReducedMotion(); const [draggedItem, setDraggedItem] = useState(null); const [dragOverItem, setDragOverItem] = useState(null); const [renamingFile, setRenamingFile] = useState(null); const [newName, setNewName] = useState(""); const [creatingFolder, setCreatingFolder] = useState(false); const [newFolderName, setNewFolderName] = useState(""); const fileInputRef = useRef(null); const pathSegments = useMemo(() => { return currentPath.split("/").filter(Boolean); }, [currentPath]); const filteredFiles = useMemo(() => { let filtered = files; // Filter by search query if (searchQuery) { filtered = filtered.filter(file => file.name.toLowerCase().includes(searchQuery.toLowerCase())); } // Filter hidden files if (!showHiddenFiles) { filtered = filtered.filter(file => !file.name.startsWith(".")); } return filtered; }, [files, searchQuery, showHiddenFiles]); const sortedFiles = useMemo(() => { return [...filteredFiles].sort((a, b) => { // Folders first, then files if (a.type !== b.type) { return a.type === "folder" ? -1 : 1; } // Alphabetical sort return a.name.localeCompare(b.name); }); }, [filteredFiles]); const handleFileClick = useCallback((file, event) => { if (renamingFile === file.id) return; if (allowMultiSelect && (event.ctrlKey || event.metaKey)) { // Multi-select const newSelection = selectedFiles.includes(file.id) ? selectedFiles.filter(id => id !== file.id) : [...selectedFiles, file.id]; onSelectionChange?.(newSelection); } else if (allowMultiSelect && event.shiftKey && selectedFiles.length > 0) { // Range select const currentIndex = sortedFiles.findIndex(f => f.id === selectedFiles[0]); const targetIndex = sortedFiles.findIndex(f => f.id === file.id); const startIndex = Math.min(currentIndex, targetIndex); const endIndex = Math.max(currentIndex, targetIndex); const rangeSelection = sortedFiles.slice(startIndex, endIndex + 1).map(f => f.id); onSelectionChange?.(rangeSelection); } else { // Single select onSelectionChange?.([file.id]); onFileSelect?.(file); if (file.type === "folder") { onNavigate(file.path); } else { onFileOpen?.(file); } } }, [renamingFile, allowMultiSelect, selectedFiles, onSelectionChange, onFileSelect, onNavigate, onFileOpen, sortedFiles]); const handleBreadcrumbClick = useCallback(segmentIndex => { const newPath = "/" + pathSegments.slice(0, segmentIndex + 1).join("/"); onNavigate(newPath); }, [pathSegments, onNavigate]); const handleContextMenuAction = useCallback((action, file) => { switch (action) { case "open": if (file.type === "folder") { onNavigate(file.path); } else { onFileOpen?.(file); } break; case "rename": setRenamingFile(file.id); setNewName(file.name); break; case "delete": onFileDelete?.(file.id); break; } }, [onNavigate, onFileOpen, onFileDelete]); const handleRenameSubmit = useCallback(() => { if (renamingFile && newName.trim()) { onFileRename?.(renamingFile, newName.trim()); setRenamingFile(null); setNewName(""); } }, [renamingFile, newName, onFileRename]); const handleCreateFolder = useCallback(() => { if (newFolderName.trim()) { onFolderCreate?.(currentPath, newFolderName.trim()); setCreatingFolder(false); setNewFolderName(""); } }, [newFolderName, currentPath, onFolderCreate]); const handleFileUpload = useCallback(event => { const fileList = event.target.files; if (fileList && fileList.length > 0) { onFileUpload?.(fileList, currentPath); } // Reset input if (fileInputRef.current) { fileInputRef.current.value = ""; } }, [currentPath, onFileUpload]); const handleDragStart = useCallback((event, file) => { if (!allowDragDrop) return; setDraggedItem(file); // Create a lightweight glass ghost for drag image const ghost = document.createElement("div"); ghost.className = "pointer-events-none px-3 py-1.5 glass-radius-lg glass-surface-dark/40 ring-1 ring-white/10 text-primary text-sm glass-glass-backdrop-blur-md shadow-xl glass-contrast-guard"; ghost.textContent = file.name; document.body.appendChild(ghost); event.dataTransfer.setDragImage(ghost, 10, 10); // Remove ghost after drag starts (browser clones the node) setTimeout(() => ghost.remove(), 0); }, [allowDragDrop]); const handleDragOver = useCallback((event, fileId) => { if (allowDragDrop) { event.preventDefault(); setDragOverItem(fileId || null); } }, [allowDragDrop]); const handleDrop = useCallback((event, targetFile) => { if (allowDragDrop && draggedItem && targetFile?.type === "folder") { event.preventDefault(); onFileMove?.(draggedItem.id, targetFile.path); setDraggedItem(null); setDragOverItem(null); } }, [allowDragDrop, draggedItem, onFileMove]); const getFileIcon = useCallback(file => { if (file.type === "folder") { return jsx(Folder, { className: 'w-5 h-5 text-primary' }); } const ext = file.extension?.toLowerCase(); switch (ext) { case "jpg": case "jpeg": case "png": case "gif": case "webp": return jsx(Image, { className: 'w-5 h-5 text-primary' }); case "mp4": case "avi": case "mov": case "mkv": return jsx(Video, { className: 'w-5 h-5 text-primary' }); case "mp3": case "wav": case "flac": return jsx(Music, { className: 'w-5 h-5 text-pink-400' }); case "zip": case "rar": case "7z": return jsx(Archive, { className: 'w-5 h-5 text-primary' }); case "js": case "ts": case "jsx": case "tsx": case "py": case "java": case "cpp": case "c": case "php": case "html": case "css": return jsx(Code, { className: 'w-5 h-5 text-primary' }); default: return jsx(FileText, { className: 'w-5 h-5 glass-text-secondary' }); } }, []); const formatFileSize = useCallback(bytes => { if (!bytes) return ""; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + " " + sizes[i]; }, []); const formatDate = useCallback(date => { return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }, []); const sizeClasses = { sm: "glass-text-sm", md: "glass-text-base", lg: "glass-text-lg" }; const variantClasses = { default: "glass-p-6", compact: "glass-p-4", minimal: "glass-p-2" }; const elevationClasses = { low: "glass-backdrop-blur-md bg-white/10 border border-white/20", medium: "glass-backdrop-blur-md bg-white/20 border border-white/30 shadow-lg", high: "glass-backdrop-blur-md bg-white/30 border border-white/40 shadow-2xl" }; return jsxs("div", { ref: ref, className: cn("glass-radius-xl", elevationClasses[elevation], variantClasses[variant], sizeClasses[size], className), ...props, children: [showToolbar && jsxs("div", { className: 'glass-flex glass-items-center glass-justify-between mb-4 pb-4 glass-border-b glass-border-white/20', children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => onNavigate("/"), disabled: currentPath === "/", className: 'text-primary/70 hover:text-primary', children: jsx(Home, { className: 'w-4 h-4' }) }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => { const parentPath = currentPath.split("/").slice(0, -1).join("/") || "/"; onNavigate(parentPath); }, disabled: currentPath === "/", className: 'text-primary/70 hover:text-primary', children: jsx(ArrowUp, { className: 'w-4 h-4' }) }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: onRefresh, className: 'text-primary/70 hover:text-primary', children: jsx(RefreshCw, { className: 'w-4 h-4' }) })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [onFolderCreate && jsxs(GlassButton, { variant: "ghost", size: "sm", onClick: e => setCreatingFolder(true), className: 'text-primary/70 hover:text-primary', children: [jsx(Plus, { className: 'w-4 h-4 glass-mr-1' }), "New Folder"] }), onFileUpload && jsxs(Fragment, { children: [jsxs(GlassButton, { variant: "ghost", size: "sm", onClick: e => fileInputRef.current?.click(), className: 'text-primary/70 hover:text-primary', children: [jsx(Upload, { className: 'w-4 h-4 glass-mr-1' }), "Upload"] }), jsx("input", { ref: fileInputRef, type: "file", multiple: true, onChange: handleFileUpload, className: 'hidden glass-touch-target glass-contrast-guard' })] }), jsxs("div", { className: "glass-flex glass-items-center glass-border glass-border-white/20 glass-radius-lg", children: [jsx(GlassButton, { variant: viewMode === "list" ? "secondary" : "ghost", size: "sm", onClick: e => onViewModeChange?.("list"), className: "glass-radius-r-none glass-border-r glass-border-white/20", children: jsx(List, { className: 'w-4 h-4' }) }), jsx(GlassButton, { variant: viewMode === "grid" ? "secondary" : "ghost", size: "sm", onClick: e => onViewModeChange?.("grid"), className: "glass-radius-l-none", children: jsx(Grid, { className: 'w-4 h-4' }) })] })] })] }), showBreadcrumb && jsx("div", { className: 'mb-4', children: jsxs(GlassBreadcrumb, { children: [jsx(GlassBreadcrumbItem, { children: jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => onNavigate("/"), children: "Root" }) }), pathSegments.map((segment, index) => jsx(GlassBreadcrumbItem, { children: jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => handleBreadcrumbClick(index), children: segment }) }, segment))] }) }), showSearch && jsx("div", { className: 'mb-4', children: jsx(GlassInput, { placeholder: "Search files...", value: searchQuery, onChange: e => onSearchChange?.(e.target.value), leftIcon: jsx(Search, { className: 'w-4 h-4' }) }) }), jsx("div", { className: 'glass-flex-1 overflow-auto', children: loading ? jsx("div", { className: "glass-flex glass-items-center glass-justify-center glass-py-8", children: jsx("div", { className: 'w-8 h-8 glass-border-2 glass-border-white/30 glass-border-t-white glass-radius-full animate-spin' }) }) : jsx("div", { className: cn(viewMode === "grid" ? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 glass-gap-4" : "glass-gap-1"), children: jsx(AnimatePresence, { children: sortedFiles.map(file => jsx(motion.div, { initial: { opacity: 0, scale: 0.95 }, animate: prefersReducedMotion ? {} : { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.95 }, className: cn("relative group", selectedFiles.includes(file.id) && "ring-2 ring-blue-400", dragOverItem === file.id && "ring-2 ring-green-400"), children: jsxs("div", { className: 'relative', children: [jsx(OptimizedGlassCore, { elevation: "level1", interactive: true, className: cn("cursor-pointer glass-radius-lg transition-all duration-200", viewMode === "grid" ? "glass-p-3" : "flex items-center glass-gap-3 glass-p-3", selectedFiles.includes(file.id) && "ring-1 ring-blue-400/40", draggedItem?.id === file.id && "glass-lift scale-[1.02] ring-1 ring-white/15 shadow-xl", dragOverItem === file.id && "glass-pulse-ring"), onClick: e => handleFileClick(file, e), draggable: allowDragDrop, onDragStart: e => handleDragStart(e, file), onDragOver: e => handleDragOver(e, file.id), onDragEnd: () => { setDraggedItem(null); setDragOverItem(null); }, onDrop: e => handleDrop(e, file), children: viewMode === "grid" ? jsxs("div", { className: 'glass-flex glass-flex-col glass-items-center text-center', children: [file.thumbnail ? jsx("img", { src: file.thumbnail, alt: file.name, className: 'w-12 h-12 object-cover glass-radius-md mb-2' }) : jsx("div", { className: 'w-12 h-12 glass-flex glass-items-center glass-justify-center mb-2', children: getFileIcon(file) }), jsx("div", { className: 'glass-text-sm text-primary font-medium truncate glass-w-full', children: file.name }), file.type === "file" && file.size && jsx("div", { className: 'glass-text-xs text-primary/60', children: formatFileSize(file.size) })] }) : jsxs(Fragment, { children: [jsx("div", { className: "glass-flex-shrink-0", children: file.thumbnail ? jsx("img", { src: file.thumbnail, alt: file.name, className: 'w-8 h-8 object-cover glass-radius-md' }) : getFileIcon(file) }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [renamingFile === file.id ? jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx(GlassInput, { value: newName, onChange: e => setNewName(e.target.value), onKeyDown: e => { if (e.key === "Enter") handleRenameSubmit(); if (e.key === "Escape") setRenamingFile(null); }, autoFocus: true, className: "glass-flex-1 glass-pulse-ring" }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handleRenameSubmit, children: jsx(Check, { className: 'w-4 h-4' }) }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setRenamingFile(null), children: jsx(X, { className: 'w-4 h-4' }) })] }) : jsx("div", { className: 'font-medium text-primary truncate', children: file.name }), jsxs("div", { className: 'glass-text-sm text-primary/60', children: [file.type === "file" ? formatFileSize(file.size) : "Folder", file.modifiedAt && ` • ${formatDate(file.modifiedAt)}`] })] })] }) }), jsx(GlassButton, { variant: "ghost", size: "sm", className: 'absolute glass-top-2 right-2 w-6 h-6 glass-p-0 opacity-0 group-hover:opacity-100 hover:glass-surface-subtle/20', onClick: e => { e.stopPropagation(); // Simple action - could be expanded to show a menu handleContextMenuAction("open", file); }, children: jsx(MoreVertical, { className: 'w-3 h-3' }) })] }) }, file.id)) }) }) }), creatingFolder && jsx("div", { className: 'fixed inset-0 glass-surface-dark/50 glass-glass-backdrop-blur-md z-50 glass-flex glass-items-center glass-justify-center glass-contrast-guard', children: jsxs(OptimizedGlassCore, { elevation: "level2", className: 'glass-radius-lg glass-p-6 max-w-md glass-w-full glass-mx-4', children: [jsx("h3", { className: 'glass-text-lg font-semibold text-primary mb-4', children: "Create New Folder" }), jsx(GlassInput, { placeholder: "Folder name...", value: newFolderName, onChange: e => setNewFolderName(e.target.value), onKeyDown: e => e.key === "Enter" && handleCreateFolder(), className: 'mb-4', autoFocus: true }), jsxs("div", { className: "glass-flex glass-gap-2", children: [jsx(GlassButton, { variant: "ghost", onClick: e => setCreatingFolder(false), className: "glass-flex-1", children: "Cancel" }), jsx(GlassButton, { variant: "primary", onClick: handleCreateFolder, disabled: !newFolderName.trim(), className: "glass-flex-1", children: "Create" })] })] }) })] }); }); GlassFileExplorer.displayName = "GlassFileExplorer"; export { GlassFileExplorer }; //# sourceMappingURL=GlassFileExplorer.js.map