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