UNPKG

aura-glass

Version:

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

449 lines (446 loc) 16 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { AnimatePresence, motion } from 'framer-motion'; import { FolderOpen, Folder, FileText, Code, Archive, Music, Video, Image, Search, ChevronDown, ChevronRight, Check, X, MoreVertical } from 'lucide-react'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { GlassButton } from '../button/GlassButton.js'; import { GlassInput } from '../input/GlassInput.js'; const GlassFileTree = /*#__PURE__*/React.forwardRef(({ nodes, onNodeSelect, onNodeToggle, onNodeCreate, onNodeDelete, onNodeRename, onNodeMove, onNodeCopy, selectedNodeId, expandedNodes = [], onExpandedChange, searchQuery = "", onSearchChange, showIcons = true, showSize = false, showModified = false, allowCreate = false, allowDelete = false, allowRename = false, allowMove = false, allowCopy = false, className, maxHeight = "400px", virtualize = false, variant = "default", size = "md", elevation = "medium", ...props }, ref) => { const [renamingNode, setRenamingNode] = useState(null); const [newName, setNewName] = useState(""); const [creatingNode, setCreatingNode] = useState(null); const [newNodeName, setNewNodeName] = useState(""); const [localExpandedNodes, setLocalExpandedNodes] = useState(expandedNodes); const [draggedNode, setDraggedNode] = useState(null); const [dragOverNode, setDragOverNode] = useState(null); // Sync local expanded state with props useEffect(() => { setLocalExpandedNodes(expandedNodes); }, [expandedNodes]); const handleToggle = useCallback(node => { if (node.type !== "folder" || !node.canExpand) return; const newExpanded = localExpandedNodes.includes(node.id) ? localExpandedNodes.filter(id => id !== node.id) : [...localExpandedNodes, node.id]; setLocalExpandedNodes(newExpanded); onExpandedChange?.(newExpanded); onNodeToggle?.(node.id, !localExpandedNodes.includes(node.id)); }, [localExpandedNodes, onExpandedChange, onNodeToggle]); const handleSelect = useCallback(node => { onNodeSelect?.(node); }, [onNodeSelect]); useCallback((action, node) => { switch (action) { case "rename": if (allowRename) { setRenamingNode(node.id); setNewName(node.name); } break; case "delete": if (allowDelete) { onNodeDelete?.(node.id); } break; case "copy": if (allowCopy) { // This would typically open a dialog to select destination console.log("Copy node:", node.id); } break; case "move": if (allowMove) { // This would typically open a dialog to select destination console.log("Move node:", node.id); } break; case "newFolder": if (allowCreate && node.type === "folder") { setCreatingNode({ parentId: node.id, type: "folder" }); setNewNodeName(""); } break; case "newFile": if (allowCreate && node.type === "folder") { setCreatingNode({ parentId: node.id, type: "file" }); setNewNodeName(""); } break; } }, [allowRename, allowDelete, allowCopy, allowMove, allowCreate, onNodeDelete]); const handleRenameSubmit = useCallback(() => { if (renamingNode && newName.trim()) { onNodeRename?.(renamingNode, newName.trim()); setRenamingNode(null); setNewName(""); } }, [renamingNode, newName, onNodeRename]); const handleCreateSubmit = useCallback(() => { if (creatingNode && newNodeName.trim()) { onNodeCreate?.(creatingNode.parentId, newNodeName.trim(), creatingNode.type); setCreatingNode(null); setNewNodeName(""); } }, [creatingNode, newNodeName, onNodeCreate]); const handleDragStart = useCallback((event, node) => { setDraggedNode(node); const ghost = document.createElement("div"); ghost.className = "pointer-events-none px-2 py-1 glass-radius-md glass-surface-dark/40 ring-1 ring-white/10 text-primary text-xs glass-glass-glass-backdrop-blur-md shadow-xl glass-contrast-guard"; ghost.textContent = node.name; document.body.appendChild(ghost); event.dataTransfer.setDragImage(ghost, 8, 8); setTimeout(() => ghost.remove(), 0); }, []); const handleDragOver = useCallback((event, node) => { if (node.type === "folder" && draggedNode && draggedNode.id !== node.id) { event.preventDefault(); setDragOverNode(node.id); } }, [draggedNode]); const handleDragEnd = useCallback(() => { setDraggedNode(null); setDragOverNode(null); }, []); const handleDrop = useCallback((event, node) => { if (draggedNode && node.type === "folder" && draggedNode.id !== node.id) { event.preventDefault(); onNodeMove?.(draggedNode.id, node.id); setDraggedNode(null); setDragOverNode(null); } }, [draggedNode, onNodeMove]); const filteredNodes = useMemo(() => { if (!searchQuery) return nodes; const filterNodes = nodes => { return nodes.reduce((filtered, node) => { const matches = node.name.toLowerCase().includes(searchQuery.toLowerCase()); if (matches) { filtered.push(node); } else if (node.children && node.children.length > 0) { const filteredChildren = filterNodes(node.children); if (filteredChildren.length > 0) { filtered.push({ ...node, children: filteredChildren, isExpanded: true }); } } return filtered; }, []); }; return filterNodes(nodes); }, [nodes, searchQuery]); const getFileIcon = useCallback(node => { if (node.type === "folder") { return localExpandedNodes.includes(node.id) ? jsx(FolderOpen, { className: 'w-4 h-4 text-primary' }) : jsx(Folder, { className: 'w-4 h-4 text-primary' }); } const ext = node.extension?.toLowerCase(); switch (ext) { case "jpg": case "jpeg": case "png": case "gif": case "webp": return jsx(Image, { className: 'w-4 h-4 text-primary' }); case "mp4": case "avi": case "mov": case "mkv": return jsx(Video, { className: 'w-4 h-4 text-primary' }); case "mp3": case "wav": case "flac": return jsx(Music, { className: 'w-4 h-4 text-pink-400' }); case "zip": case "rar": case "7z": return jsx(Archive, { className: 'w-4 h-4 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-4 h-4 text-primary' }); default: return jsx(FileText, { className: 'w-4 h-4 glass-text-secondary' }); } }, [localExpandedNodes]); 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 => { if (!date) return ""; 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-4", compact: "glass-p-2", minimal: "glass-p-1" }; const elevationClasses = { low: "glass-glass-backdrop-blur-md bg-white/10 border border-white/20 glass-contrast-guard", medium: "glass-glass-backdrop-blur-md bg-white/20 border border-white/30 shadow-lg glass-contrast-guard", high: "glass-glass-backdrop-blur-md bg-white/30 border border-white/40 shadow-2xl glass-contrast-guard" }; const TreeNodeComponent = ({ node, level = 0 }) => { const prefersReducedMotion = useReducedMotion(); const isExpanded = localExpandedNodes.includes(node.id); const isSelected = selectedNodeId === node.id; const isDragOver = dragOverNode === node.id; const indent = level * 16; return jsxs("div", { children: [jsxs("div", { className: cn("flex items-center glass-gap-1 glass-py-1 glass-px-2 glass-radius-md cursor-pointer hover:bg-white/10 transition-all duration-200 group relative", "hover:-translate-y-0.5 glass-press", isSelected && "bg-blue-500/20 text-blue-300", isDragOver && "bg-green-500/20 ring-1 ring-green-400 glass-pulse-ring", renamingNode === node.id && "bg-white/5"), style: { paddingLeft: `${indent + 8}px` }, onClick: e => handleSelect(node), draggable: true, onDragStart: e => handleDragStart(e, node), onDragOver: e => handleDragOver(e, node), onDragEnd: handleDragEnd, onDrop: e => handleDrop(e, node), children: [node.type === "folder" && node.canExpand && jsx(GlassButton, { variant: "ghost", size: "sm", className: 'w-4 h-4 glass-p-0 hover:glass-surface-subtle/20 glass-focus glass-touch-target', onClick: e => { e.stopPropagation(); handleToggle(node); }, children: isExpanded ? jsx(ChevronDown, { className: 'w-3 h-3' }) : jsx(ChevronRight, { className: 'w-3 h-3' }) }), node.type === "file" && jsx("div", { className: 'w-4' }), showIcons && jsx("div", { className: "glass-flex-shrink-0", children: getFileIcon(node) }), jsx("div", { className: "glass-flex-1 glass-min-w-0", children: renamingNode === node.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") setRenamingNode(null); }, autoFocus: true, className: 'glass-flex-1 h-6 glass-text-sm glass-pulse-ring' }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handleRenameSubmit, className: 'w-6 h-6 glass-p-0 glass-focus glass-touch-target', children: jsx(Check, { className: 'w-3 h-3' }) }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setRenamingNode(null), className: 'w-6 h-6 glass-p-0 glass-focus glass-touch-target', children: jsx(X, { className: 'w-3 h-3' }) })] }) : jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("span", { className: 'truncate text-primary', children: node.name }), showSize && node.size && jsxs("span", { className: 'glass-text-xs text-primary/60', children: ["(", formatFileSize(node.size), ")"] })] }) }), showModified && node.modifiedAt && jsx("div", { className: 'glass-text-xs text-primary/60 hidden md:block', children: formatDate(node.modifiedAt) }), jsx(GlassButton, { variant: "ghost", size: "sm", className: 'w-6 h-6 glass-p-0 opacity-0 group-hover:opacity-100 hover:glass-surface-subtle/20 glass-focus glass-touch-target', onClick: e => { e.stopPropagation(); // Simple action for now console.log("Context menu for:", node.name); }, children: jsx(MoreVertical, { className: 'w-3 h-3' }) })] }), jsx(AnimatePresence, { children: isExpanded && node.children && node.children.length > 0 && jsx(motion.div, { initial: { opacity: 0, height: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1, height: "auto" }, exit: { opacity: 0, height: 0 }, className: 'overflow-hidden', children: node.children.map(child => jsx(TreeNodeComponent, { node: { ...child, level: level + 1 }, level: level + 1 }, child.id)) }) }), isExpanded && node.isLoading && jsxs("div", { className: "glass-flex glass-items-center glass-py-1", style: { paddingLeft: `${indent + 32}px` }, children: [jsx("div", { className: 'w-4 h-4 glass-border-2 glass-border-white/30 glass-border-t-white glass-radius-full animate-spin' }), jsx("span", { className: 'glass-text-sm text-primary/60 glass-ml-2', children: "Loading..." })] })] }); }; return jsxs("div", { ref: ref, className: cn("glass-radius-xl overflow-hidden", elevationClasses[elevation], variantClasses[variant], sizeClasses[size], className), ...props, children: [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: 'overflow-y-auto', style: { maxHeight }, children: filteredNodes.length > 0 ? filteredNodes.map(node => jsx(TreeNodeComponent, { node: node }, node.id)) : jsx("div", { className: 'text-center glass-py-8 text-primary/60', children: searchQuery ? "No files found" : "No files to display" }) }), creatingNode && jsx("div", { className: 'fixed inset-0 glass-surface-dark/50 glass-glass-glass-backdrop-blur-md z-50 glass-flex glass-items-center glass-justify-center glass-contrast-guard', children: jsxs("div", { className: 'glass-radius-lg glass-p-6 max-w-md glass-w-full glass-mx-4 glass-surface-subtle/5 ring-1 ring-white/10 glass-contrast-guard', children: [jsxs("h3", { className: 'glass-text-lg font-semibold text-primary mb-4', children: ["Create New ", creatingNode.type === "folder" ? "Folder" : "File"] }), jsx(GlassInput, { placeholder: `${creatingNode.type === "folder" ? "Folder" : "File"} name...`, value: newNodeName, onChange: e => setNewNodeName(e.target.value), onKeyDown: e => e.key === "Enter" && handleCreateSubmit(), className: 'mb-4', autoFocus: true }), jsxs("div", { className: "glass-flex glass-gap-2", children: [jsx(GlassButton, { variant: "ghost", onClick: e => setCreatingNode(null), className: "glass-flex-1 glass-focus glass-touch-target", children: "Cancel" }), jsx(GlassButton, { variant: "primary", onClick: handleCreateSubmit, disabled: !newNodeName.trim(), className: "glass-flex-1 glass-focus glass-touch-target", children: "Create" })] })] }) })] }); }); GlassFileTree.displayName = "GlassFileTree"; export { GlassFileTree }; //# sourceMappingURL=GlassFileTree.js.map