// src/layout/file-tree/FileTreeItem.tsx import { useCallback, type KeyboardEvent, type MouseEvent } from 'react' import { ChevronRight, MoreHorizontal } from 'lucide-react' import type { AutomergeUrl } from '../../storage/document-runtime-contract' import { isFolderEntry, type DirectoryEntry } from '../../types/pushwork' import { getFileIcon, getFolderIcon, getFileExtension, FILE_TREE_INDENT_PX, FILE_TREE_BASE_PAD_PX } from './file-tree-utils' import './FileTreeItem.css' export interface FileTreeItemProps { entry: DirectoryEntry depth: number isExpanded: boolean isActive: boolean onClick: (entry: DirectoryEntry) => void onContextMenu: (event: MouseEvent, entry: DirectoryEntry) => void onToggleExpand: (url: AutomergeUrl) => void onActionClick?: (entry: DirectoryEntry, position: { x: number; y: number }) => void /** Optional display name (e.g. from frontmatter title). Shows instead of entry.name when provided. */ displayName?: string } export function FileTreeItem({ entry, depth, isExpanded, isActive, onClick, onContextMenu, onToggleExpand, onActionClick, displayName, }: FileTreeItemProps) { const isFolder = isFolderEntry(entry) const extension = isFolder ? '' : getFileExtension(entry.name) const Icon = isFolder ? getFolderIcon(isExpanded) : getFileIcon(extension) const handleClick = useCallback((e: MouseEvent) => { e.stopPropagation() onClick(entry) }, [entry, onClick]) const handleChevronClick = useCallback((e: MouseEvent) => { e.stopPropagation() onToggleExpand(entry.url) }, [entry.url, onToggleExpand]) const handleContextMenu = useCallback((e: MouseEvent) => { e.preventDefault() e.stopPropagation() onContextMenu(e, entry) }, [entry, onContextMenu]) const handleKeyDown = useCallback((e: KeyboardEvent) => { switch (e.key) { case 'Enter': e.preventDefault() onClick(entry) break case 'ArrowRight': if (isFolder && !isExpanded) { e.preventDefault() onToggleExpand(entry.url) } break case 'ArrowLeft': if (isFolder && isExpanded) { e.preventDefault() onToggleExpand(entry.url) } break case 'ContextMenu': { e.preventDefault() if (onActionClick) { // Position relative to the action button if present, otherwise the row const el = e.currentTarget as HTMLElement const btn = el.querySelector('.file-tree-item__action-btn') as HTMLElement | null const rect = (btn ?? el).getBoundingClientRect() onActionClick(entry, { x: rect.right, y: rect.bottom }) } break } default: // Shift+F10 is the standard keyboard shortcut for context menu if (e.key === 'F10' && e.shiftKey) { e.preventDefault() if (onActionClick) { const el = e.currentTarget as HTMLElement const btn = el.querySelector('.file-tree-item__action-btn') as HTMLElement | null const rect = (btn ?? el).getBoundingClientRect() onActionClick(entry, { x: rect.right, y: rect.bottom }) } } break } }, [entry, isFolder, isExpanded, onClick, onToggleExpand, onActionClick]) const handleActionMouseDown = useCallback((e: MouseEvent) => { // Stop mousedown from reaching the document listener in FileTreeContextMenu // which would close the menu before our click handler can toggle it e.stopPropagation() }, []) const handleActionClick = useCallback((e: MouseEvent) => { e.stopPropagation() if (onActionClick) { const button = e.currentTarget as HTMLElement const rect = button.getBoundingClientRect() onActionClick(entry, { x: rect.right, y: rect.bottom }) } }, [entry, onActionClick]) // Base left padding (FILE_TREE_BASE_PAD_PX, 8px) gives the icon // breathing room from the panel's left edge at depth 0, matching the // right-side gap between icon and filename. Indent for deeper levels // stacks on top of that base. Kept inline (rather than in the CSS // padding shorthand) because depth varies per row. const paddingLeft = FILE_TREE_BASE_PAD_PX + depth * FILE_TREE_INDENT_PX return (