aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
532 lines (529 loc) • 20.5 kB
JavaScript
'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