import React, { useState, useRef, useCallback, useEffect } from "react"; import { cn } from "@/lib/utils"; import { Folder, File, FileText, FileImage, FileVideo, FileAudio, Archive, Code, Settings, Download, Upload, ChevronLeft, ChevronRight, MoreHorizontal, RefreshCw, ArrowUp, FileSymlink, Move, GitCompare, Edit, } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { FileItem } from "../../../types/index.js"; // Linus-style data structure: separate creation intent from actual files interface CreateIntent { id: string; type: 'file' | 'directory'; defaultName: string; currentName: string; } // Format file size function formatFileSize(bytes?: number): string { // Handle undefined or null cases if (bytes === undefined || bytes === null) return "-"; // Display 0-byte files as "0 B" if (bytes === 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } // Display one decimal place for values less than 10, integers for values greater than 10 const formattedSize = size < 10 && unitIndex > 0 ? size.toFixed(1) : Math.round(size).toString(); return `${formattedSize} ${units[unitIndex]}`; } interface DragState { type: "none" | "internal" | "external"; files: FileItem[]; target?: FileItem; counter: number; mousePosition?: { x: number; y: number }; } interface FileManagerGridProps { files: FileItem[]; selectedFiles: FileItem[]; onFileSelect: (file: FileItem, multiSelect?: boolean) => void; onFileOpen: (file: FileItem) => void; onSelectionChange: (files: FileItem[]) => void; currentPath: string; isLoading?: boolean; onPathChange: (path: string) => void; onRefresh: () => void; onUpload?: (files: FileList) => void; onDownload?: (files: FileItem[]) => void; onContextMenu?: (event: React.MouseEvent, file?: FileItem) => void; viewMode?: "grid" | "list"; onRename?: (file: FileItem, newName: string) => void; editingFile?: FileItem | null; onStartEdit?: (file: FileItem) => void; onCancelEdit?: () => void; onDelete?: (files: FileItem[]) => void; onCopy?: (files: FileItem[]) => void; onCut?: (files: FileItem[]) => void; onPaste?: () => void; onUndo?: () => void; onFileDrop?: (draggedFiles: FileItem[], targetFile: FileItem) => void; onFileDiff?: (file1: FileItem, file2: FileItem) => void; onSystemDragStart?: (files: FileItem[]) => void; onSystemDragEnd?: (e: DragEvent) => void; hasClipboard?: boolean; // Linus-style creation intent props createIntent?: CreateIntent | null; onConfirmCreate?: (name: string) => void; onCancelCreate?: () => void; } const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => { const iconClass = viewMode === "grid" ? "w-8 h-8" : "w-6 h-6"; if (file.type === "directory") { return ; } if (file.type === "link") { return ; } const ext = file.name.split(".").pop()?.toLowerCase(); switch (ext) { case "txt": case "md": case "readme": return ; case "png": case "jpg": case "jpeg": case "gif": case "bmp": case "svg": return ; case "mp4": case "avi": case "mkv": case "mov": return ; case "mp3": case "wav": case "flac": case "ogg": return ; case "zip": case "tar": case "gz": case "rar": case "7z": return ; case "js": case "ts": case "jsx": case "tsx": case "py": case "java": case "cpp": case "c": case "cs": case "php": case "rb": case "go": case "rs": return ; case "json": case "xml": case "yaml": case "yml": case "toml": case "ini": case "conf": case "config": return ; default: return ; } }; export function FileManagerGrid({ files, selectedFiles, onFileSelect, onFileOpen, onSelectionChange, currentPath, isLoading, onPathChange, onRefresh, onUpload, onDownload, onContextMenu, viewMode = "grid", onRename, editingFile, onStartEdit, onCancelEdit, onDelete, onCopy, onCut, onPaste, onUndo, onFileDrop, onFileDiff, onSystemDragStart, onSystemDragEnd, hasClipboard, createIntent, onConfirmCreate, onCancelCreate, }: FileManagerGridProps) { const { t } = useTranslation(); const gridRef = useRef(null); const [editingName, setEditingName] = useState(""); // Unified drag state management const [dragState, setDragState] = useState({ type: "none", files: [], counter: 0, }); // Global mouse move listener - for drag tooltip following useEffect(() => { const handleGlobalMouseMove = (e: MouseEvent) => { if (dragState.type === "internal" && dragState.files.length > 0) { setDragState((prev) => ({ ...prev, mousePosition: { x: e.clientX, y: e.clientY }, })); } }; if (dragState.type === "internal" && dragState.files.length > 0) { document.addEventListener("mousemove", handleGlobalMouseMove); return () => document.removeEventListener("mousemove", handleGlobalMouseMove); } }, [dragState.type, dragState.files.length]); const editInputRef = useRef(null); // Set initial name when starting edit useEffect(() => { if (editingFile) { setEditingName(editingFile.name); // Delay focus to ensure DOM is updated setTimeout(() => { editInputRef.current?.focus(); editInputRef.current?.select(); }, 0); } }, [editingFile]); // Handle edit confirmation const handleEditConfirm = () => { if ( editingFile && onRename && editingName.trim() && editingName !== editingFile.name ) { onRename(editingFile, editingName.trim()); } onCancelEdit?.(); }; // Handle edit cancellation const handleEditCancel = () => { setEditingName(""); onCancelEdit?.(); }; // Handle input key events const handleEditKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); handleEditConfirm(); } else if (e.key === "Escape") { e.preventDefault(); handleEditCancel(); } }; // File drag handling function const handleFileDragStart = (e: React.DragEvent, file: FileItem) => { // If dragged file is selected, drag all selected files const filesToDrag = selectedFiles.includes(file) ? selectedFiles : [file]; setDragState({ type: "internal", files: filesToDrag, counter: 0, mousePosition: { x: e.clientX, y: e.clientY }, }); // Set drag data, add internal drag identifier const dragData = { type: "internal_files", files: filesToDrag.map((f) => f.path), }; e.dataTransfer.setData("text/plain", JSON.stringify(dragData)); // Trigger system-level drag start onSystemDragStart?.(filesToDrag); e.dataTransfer.effectAllowed = "move"; }; const handleFileDragOver = (e: React.DragEvent, targetFile: FileItem) => { e.preventDefault(); e.stopPropagation(); // Only set target when dragging to different file and not being dragged file if ( dragState.type === "internal" && !dragState.files.some((f) => f.path === targetFile.path) ) { setDragState((prev) => ({ ...prev, target: targetFile })); e.dataTransfer.dropEffect = "move"; } }; const handleFileDragLeave = (e: React.DragEvent, targetFile: FileItem) => { e.preventDefault(); e.stopPropagation(); // Clear drag target highlight if (dragState.target?.path === targetFile.path) { setDragState((prev) => ({ ...prev, target: undefined })); } }; const handleFileDrop = (e: React.DragEvent, targetFile: FileItem) => { e.preventDefault(); e.stopPropagation(); if (dragState.type !== "internal" || dragState.files.length === 0) { setDragState((prev) => ({ ...prev, target: undefined })); return; } // Check if dragging to self const isDroppingOnSelf = dragState.files.some( (f) => f.path === targetFile.path, ); if (isDroppingOnSelf) { console.log("Ignoring drop on self"); setDragState({ type: "none", files: [], counter: 0 }); return; } // Determine drag behavior: // 1. File/folder drag to folder = move operation // 2. Single file drag to single file = diff comparison // 3. Other cases = invalid operation if (targetFile.type === "directory") { // Move operation console.log( "Moving files to directory:", dragState.files.map((f) => f.name), "to", targetFile.name, ); onFileDrop?.(dragState.files, targetFile); } else if ( targetFile.type === "file" && dragState.files.length === 1 && dragState.files[0].type === "file" ) { // Diff comparison operation console.log( "Comparing files:", dragState.files[0].name, "vs", targetFile.name, ); onFileDiff?.(dragState.files[0], targetFile); } else { // Invalid operation, notify user console.log("Invalid drag operation"); } setDragState({ type: "none", files: [], counter: 0 }); }; const handleFileDragEnd = (e: React.DragEvent) => { setDragState({ type: "none", files: [], counter: 0 }); // Trigger system-level drag end detection onSystemDragEnd?.(e.nativeEvent); }; const [isSelecting, setIsSelecting] = useState(false); const [selectionStart, setSelectionStart] = useState<{ x: number; y: number; } | null>(null); const [selectionRect, setSelectionRect] = useState<{ x: number; y: number; width: number; height: number; } | null>(null); const [justFinishedSelecting, setJustFinishedSelecting] = useState(false); // Navigation history management const [navigationHistory, setNavigationHistory] = useState([ currentPath, ]); const [historyIndex, setHistoryIndex] = useState(0); // Path editing state const [isEditingPath, setIsEditingPath] = useState(false); const [editPathValue, setEditPathValue] = useState(currentPath); // Update navigation history useEffect(() => { const lastPath = navigationHistory[historyIndex]; if (currentPath !== lastPath) { const newHistory = navigationHistory.slice(0, historyIndex + 1); newHistory.push(currentPath); setNavigationHistory(newHistory); setHistoryIndex(newHistory.length - 1); } }, [currentPath]); // Navigation functions const goBack = () => { if (historyIndex > 0) { const newIndex = historyIndex - 1; setHistoryIndex(newIndex); onPathChange(navigationHistory[newIndex]); } }; const goForward = () => { if (historyIndex < navigationHistory.length - 1) { const newIndex = historyIndex + 1; setHistoryIndex(newIndex); onPathChange(navigationHistory[newIndex]); } }; const goUp = () => { const parts = currentPath.split("/").filter(Boolean); if (parts.length > 0) { parts.pop(); const parentPath = "/" + parts.join("/"); onPathChange(parentPath); } else if (currentPath !== "/") { onPathChange("/"); } }; // Path navigation const pathParts = currentPath.split("/").filter(Boolean); const navigateToPath = (index: number) => { if (index === -1) { onPathChange("/"); } else { const newPath = "/" + pathParts.slice(0, index + 1).join("/"); onPathChange(newPath); } }; // Path editing functionality const startEditingPath = () => { setEditPathValue(currentPath); setIsEditingPath(true); }; const cancelEditingPath = () => { setIsEditingPath(false); setEditPathValue(currentPath); }; const confirmEditingPath = () => { const trimmedPath = editPathValue.trim(); if (trimmedPath) { // Ensure path starts with / const normalizedPath = trimmedPath.startsWith("/") ? trimmedPath : "/" + trimmedPath; onPathChange(normalizedPath); } setIsEditingPath(false); }; const handlePathInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); confirmEditingPath(); } else if (e.key === "Escape") { e.preventDefault(); cancelEditingPath(); } }; // Sync editPathValue with currentPath useEffect(() => { if (!isEditingPath) { setEditPathValue(currentPath); } }, [currentPath, isEditingPath]); // Drag and drop handling - distinguish internal file drag and external file upload const handleDragEnter = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // Check if it's internal file drag const isInternalDrag = dragState.type === "internal"; if (!isInternalDrag) { // Only show upload prompt for external file drag setDragState((prev) => ({ ...prev, type: "external", counter: prev.counter + 1, })); if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { // External drag detected } } }, [dragState.type], ); const handleDragLeave = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // Check if it's internal file drag const isInternalDrag = dragState.type === "internal"; if (!isInternalDrag && dragState.type === "external") { setDragState((prev) => { const newCounter = prev.counter - 1; return { ...prev, counter: newCounter, type: newCounter <= 0 ? "none" : "external", }; }); } }, [dragState.type, dragState.counter], ); const handleDragOver = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // Check if it's internal file drag const isInternalDrag = dragState.type === "internal"; if (isInternalDrag) { // Update mouse position setDragState((prev) => ({ ...prev, mousePosition: { x: e.clientX, y: e.clientY }, })); e.dataTransfer.dropEffect = "move"; } else { e.dataTransfer.dropEffect = "copy"; } }, [dragState.type], ); // Mouse wheel event handling, ensure scrolling works normally const handleWheel = useCallback((e: React.WheelEvent) => { // Don't prevent default scroll behavior, let browser handle scrolling e.stopPropagation(); }, []); // Box selection functionality implementation const handleMouseDown = useCallback((e: React.MouseEvent) => { // Only start box selection in empty area, avoid interfering with file clicks if (e.target === e.currentTarget && e.button === 0) { e.preventDefault(); const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); const startX = e.clientX - rect.left; const startY = e.clientY - rect.top; setIsSelecting(true); setSelectionStart({ x: startX, y: startY }); setSelectionRect({ x: startX, y: startY, width: 0, height: 0 }); // Reset flag for just completed selection, prepare for new selection setJustFinishedSelecting(false); } }, []); const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (isSelecting && selectionStart && gridRef.current) { const rect = gridRef.current.getBoundingClientRect(); const currentX = e.clientX - rect.left; const currentY = e.clientY - rect.top; const x = Math.min(selectionStart.x, currentX); const y = Math.min(selectionStart.y, currentY); const width = Math.abs(currentX - selectionStart.x); const height = Math.abs(currentY - selectionStart.y); setSelectionRect({ x, y, width, height }); // Detect intersection with file items, perform real-time selection if (gridRef.current) { const fileElements = gridRef.current.querySelectorAll("[data-file-path]"); const selectedPaths: string[] = []; fileElements.forEach((element) => { const elementRect = element.getBoundingClientRect(); const containerRect = gridRef.current!.getBoundingClientRect(); // Simplify coordinate calculation - directly use coordinates relative to container const relativeElementRect = { left: elementRect.left - containerRect.left, top: elementRect.top - containerRect.top, right: elementRect.right - containerRect.left, bottom: elementRect.bottom - containerRect.top, }; // Selection box coordinates const selectionBox = { left: x, top: y, right: x + width, bottom: y + height, }; // Check if intersecting const intersects = !( relativeElementRect.right < selectionBox.left || relativeElementRect.left > selectionBox.right || relativeElementRect.bottom < selectionBox.top || relativeElementRect.top > selectionBox.bottom ); if (intersects) { const filePath = element.getAttribute("data-file-path"); if (filePath) { selectedPaths.push(filePath); console.log("Selected file:", filePath); } } }); console.log("Total selected paths:", selectedPaths.length); // Update selected files const newSelection = files.filter((file) => selectedPaths.includes(file.path), ); console.log( "New selection:", newSelection.map((f) => f.name), ); onSelectionChange(newSelection); } } }, [isSelecting, selectionStart, files, onSelectionChange], ); const handleMouseUp = useCallback( (e: React.MouseEvent) => { if (isSelecting) { setIsSelecting(false); setSelectionStart(null); setSelectionRect(null); // Only consider as box selection when movement distance is large enough, otherwise it's a click const startPos = selectionStart; if (startPos) { const rect = gridRef.current?.getBoundingClientRect(); if (rect) { const endX = e.clientX - rect.left; const endY = e.clientY - rect.top; const distance = Math.sqrt( Math.pow(endX - startPos.x, 2) + Math.pow(endY - startPos.y, 2), ); if (distance > 5) { // Real box selection, set flag to prevent immediate clearing setJustFinishedSelecting(true); setTimeout(() => { setJustFinishedSelecting(false); }, 50); } else { // Just a click, don't set flag, let handleGridClick handle normally setJustFinishedSelecting(false); } } } } }, [isSelecting, selectionStart], ); // Global mouse event listener, ensure box selection can end outside container useEffect(() => { const handleGlobalMouseUp = (e: MouseEvent) => { if (isSelecting) { setIsSelecting(false); setSelectionStart(null); setSelectionRect(null); // Global mouseup indicates drag box selection, set flag setJustFinishedSelecting(true); setTimeout(() => { setJustFinishedSelecting(false); }, 50); } }; const handleGlobalMouseMove = (e: MouseEvent) => { if (isSelecting && selectionStart && gridRef.current) { const rect = gridRef.current.getBoundingClientRect(); const currentX = e.clientX - rect.left; const currentY = e.clientY - rect.top; const x = Math.min(selectionStart.x, currentX); const y = Math.min(selectionStart.y, currentY); const width = Math.abs(currentX - selectionStart.x); const height = Math.abs(currentY - selectionStart.y); setSelectionRect({ x, y, width, height }); } }; if (isSelecting) { document.addEventListener("mouseup", handleGlobalMouseUp); document.addEventListener("mousemove", handleGlobalMouseMove); return () => { document.removeEventListener("mouseup", handleGlobalMouseUp); document.removeEventListener("mousemove", handleGlobalMouseMove); }; } }, [isSelecting, selectionStart]); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (dragState.type === "internal") { // Internal drag to empty area: trigger download console.log( "Internal drag to empty area detected, triggering download", ); if (onDownload && dragState.files.length > 0) { onDownload(dragState.files); } } else if (dragState.type === "external") { // External drag: handle file upload if (onUpload && e.dataTransfer.files.length > 0) { onUpload(e.dataTransfer.files); } } // Reset drag state setDragState({ type: "none", files: [], counter: 0 }); }, [onUpload, onDownload, dragState], ); // File selection handling const handleFileClick = (file: FileItem, event: React.MouseEvent) => { event.stopPropagation(); // Ensure grid gets focus to support keyboard events if (gridRef.current) { gridRef.current.focus(); } console.log( "File clicked:", file.name, "Current selected:", selectedFiles.length, ); if (event.detail === 2) { // Double click to open console.log("Double click - opening file"); onFileOpen(file); } else { // Single click to select const multiSelect = event.ctrlKey || event.metaKey; const rangeSelect = event.shiftKey; console.log( "Single click - multiSelect:", multiSelect, "rangeSelect:", rangeSelect, ); if (rangeSelect && selectedFiles.length > 0) { // Range selection (Shift+click) console.log("Range selection"); const lastSelected = selectedFiles[selectedFiles.length - 1]; const currentIndex = files.findIndex((f) => f.path === file.path); const lastIndex = files.findIndex((f) => f.path === lastSelected.path); if (currentIndex !== -1 && lastIndex !== -1) { const start = Math.min(currentIndex, lastIndex); const end = Math.max(currentIndex, lastIndex); const rangeFiles = files.slice(start, end + 1); console.log("Range selection result:", rangeFiles.length, "files"); onSelectionChange(rangeFiles); } } else if (multiSelect) { // Multi-selection (Ctrl+click) console.log("Multi selection"); const isSelected = selectedFiles.some((f) => f.path === file.path); if (isSelected) { console.log("Removing from selection"); onSelectionChange(selectedFiles.filter((f) => f.path !== file.path)); } else { console.log("Adding to selection"); onSelectionChange([...selectedFiles, file]); } } else { // Single selection console.log("Single selection - should select only:", file.name); onSelectionChange([file]); } } }; // Click empty area to cancel selection const handleGridClick = (event: React.MouseEvent) => { // Ensure grid gets focus to support keyboard events if (gridRef.current) { gridRef.current.focus(); } // If just completed box selection, don't clear selection if ( event.target === event.currentTarget && !isSelecting && !justFinishedSelecting ) { onSelectionChange([]); } }; // Keyboard support useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // Check if input box or editable element has focus, skip if so const activeElement = document.activeElement; if ( activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.contentEditable === "true") ) { return; } switch (event.key) { case "Escape": onSelectionChange([]); break; case "a": case "A": if (event.ctrlKey || event.metaKey) { event.preventDefault(); console.log("Ctrl+A pressed - selecting all files:", files.length); onSelectionChange([...files]); } break; case "c": case "C": if ( (event.ctrlKey || event.metaKey) && selectedFiles.length > 0 && onCopy ) { event.preventDefault(); onCopy(selectedFiles); } break; case "x": case "X": if ( (event.ctrlKey || event.metaKey) && selectedFiles.length > 0 && onCut ) { event.preventDefault(); onCut(selectedFiles); } break; case "v": case "V": if ((event.ctrlKey || event.metaKey) && onPaste && hasClipboard) { event.preventDefault(); onPaste(); } break; case "z": case "Z": if ((event.ctrlKey || event.metaKey) && onUndo) { event.preventDefault(); onUndo(); } break; case "Delete": if (selectedFiles.length > 0 && onDelete) { // Trigger delete operation onDelete(selectedFiles); } break; case "F2": if (selectedFiles.length === 1 && onStartEdit) { event.preventDefault(); onStartEdit(selectedFiles[0]); } break; case "F5": event.preventDefault(); onRefresh(); break; } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [ selectedFiles, files, onSelectionChange, onRefresh, onDelete, onCopy, onCut, onPaste, onUndo, ]); if (isLoading) { return (

{t("common.loading")}

); } return (
{/* Toolbar and path navigation */}
{/* Navigation buttons */}
{/* Breadcrumb navigation */}
{isEditingPath ? ( // Edit mode: path input box
setEditPathValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { confirmEditingPath(); } else if (e.key === "Escape") { cancelEditingPath(); } }} className="flex-1 px-2 py-1 bg-dark-hover border border-dark-border rounded text-sm focus:outline-none focus:ring-1 focus:ring-primary" placeholder={t("fileManager.enterPath")} autoFocus />
) : ( // View mode: breadcrumb navigation <> {pathParts.map((part, index) => ( {index < pathParts.length - 1 && ( / )} ))} )}
{/* Main file grid - scroll area */}
onContextMenu?.(e)} tabIndex={0} > {/* Drag hint overlay */} {dragState.type === "external" && (

{t("fileManager.dragFilesToUpload")}

{t("fileManager.dragSystemFilesToUpload")}

)} {files.length === 0 ? (

{t("fileManager.emptyFolder")}

{t("fileManager.dragSystemFilesToUpload")}
{t("fileManager.dragFilesToWindowToDownload")}
) : viewMode === "grid" ? (
{/* Linus-style creation intent UI - pure separation */} {createIntent && ( )} {files.map((file) => { const isSelected = selectedFiles.some( (f) => f.path === file.path, ); return (
f.path === file.path) && "opacity-50", )} title={`${file.name} - Selected: ${isSelected} - SelectedCount: ${selectedFiles.length}`} onClick={(e) => handleFileClick(file, e)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu?.(e, file); }} onDragStart={(e) => handleFileDragStart(e, file)} onDragOver={(e) => handleFileDragOver(e, file)} onDragLeave={(e) => handleFileDragLeave(e, file)} onDrop={(e) => handleFileDrop(e, file)} onDragEnd={handleFileDragEnd} >
{/* File icon */}
{getFileIcon(file, viewMode)}
{/* File name */}
{editingFile?.path === file.path ? ( setEditingName(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={handleEditConfirm} className={cn( "max-w-[120px] min-w-[60px] w-fit rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none", "text-center text-foreground placeholder:text-muted-foreground", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]", )} onClick={(e) => e.stopPropagation()} /> ) : (

{ // Prevent file selection event if (onStartEdit) { e.stopPropagation(); onStartEdit(file); } }} > {file.name}

)} {file.type === "file" && file.size !== undefined && file.size !== null && (

{formatFileSize(file.size)}

)} {file.type === "link" && file.linkTarget && (

→ {file.linkTarget}

)}
); })}
) : ( /* List view */
{/* Linus-style creation intent UI - list view */} {createIntent && ( )} {files.map((file) => { const isSelected = selectedFiles.some( (f) => f.path === file.path, ); return (
f.path === file.path) && "opacity-50", )} onClick={(e) => handleFileClick(file, e)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu?.(e, file); }} onDragStart={(e) => handleFileDragStart(e, file)} onDragOver={(e) => handleFileDragOver(e, file)} onDragLeave={(e) => handleFileDragLeave(e, file)} onDrop={(e) => handleFileDrop(e, file)} onDragEnd={handleFileDragEnd} > {/* File icon */}
{getFileIcon(file, viewMode)}
{/* File info */}
{editingFile?.path === file.path ? ( setEditingName(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={handleEditConfirm} className={cn( "flex-1 min-w-0 max-w-[200px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none", "text-foreground placeholder:text-muted-foreground", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]", )} onClick={(e) => e.stopPropagation()} /> ) : (

{ // Prevent file selection event if (onStartEdit) { e.stopPropagation(); onStartEdit(file); } }} > {file.name}

)} {file.type === "link" && file.linkTarget && (

→ {file.linkTarget}

)} {file.modified && (

{file.modified}

)}
{/* File size */}
{file.type === "file" && file.size !== undefined && file.size !== null && (

{formatFileSize(file.size)}

)}
{/* Permission info */}
{file.permissions && (

{file.permissions}

)}
); })}
)} {/* Selection rectangle */} {isSelecting && selectionRect && (
)}
{/* Status bar */}
{t("fileManager.itemCount", { count: files.length })} {selectedFiles.length > 0 && ( {t("fileManager.selectedCount", { count: selectedFiles.length })} )}
{/* Drag following tooltip */} {dragState.type === "internal" && dragState.files.length > 0 && dragState.mousePosition && (
{dragState.target ? ( dragState.target.type === "directory" ? ( <> Move to {dragState.target.name} ) : ( <> Diff compare with {dragState.target.name} ) ) : ( <> Drag outside window to download ({dragState.files.length} files) )}
)}
); } // Linus-style creation intent component: Grid view function CreateIntentGridItem({ intent, onConfirm, onCancel, }: { intent: CreateIntent; onConfirm?: (name: string) => void; onCancel?: () => void; }) { const [inputName, setInputName] = useState(intent.currentName); const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); inputRef.current?.select(); }, []); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); onConfirm?.(inputName.trim()); } else if (e.key === "Escape") { e.preventDefault(); onCancel?.(); } }; return (
{intent.type === 'directory' ? ( ) : ( )}
setInputName(e.target.value)} onKeyDown={handleKeyDown} onBlur={() => onConfirm?.(inputName.trim())} className="w-full max-w-[120px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-xs text-center text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px] outline-none" placeholder={intent.type === 'directory' ? t('fileManager.folderName') : t('fileManager.fileName')} />
); } // Linus-style creation intent component: List view function CreateIntentListItem({ intent, onConfirm, onCancel, }: { intent: CreateIntent; onConfirm?: (name: string) => void; onCancel?: () => void; }) { const [inputName, setInputName] = useState(intent.currentName); const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); inputRef.current?.select(); }, []); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); onConfirm?.(inputName.trim()); } else if (e.key === "Escape") { e.preventDefault(); onCancel?.(); } }; return (
{intent.type === 'directory' ? ( ) : ( )}
setInputName(e.target.value)} onKeyDown={handleKeyDown} onBlur={() => onConfirm?.(inputName.trim())} className="flex-1 min-w-0 max-w-[200px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-sm text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px] outline-none" placeholder={intent.type === 'directory' ? 'Folder name' : 'File name'} />
); }