From 09f964451d7d92e9891430ec4595c1276d486aab Mon Sep 17 00:00:00 2001 From: zwlucas Date: Fri, 5 Jun 2026 17:52:54 -0300 Subject: feat: upload Signed-off-by: zwlucas --- src/components/Sidebar/FileTree.tsx | 227 ++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 src/components/Sidebar/FileTree.tsx (limited to 'src/components/Sidebar/FileTree.tsx') diff --git a/src/components/Sidebar/FileTree.tsx b/src/components/Sidebar/FileTree.tsx new file mode 100644 index 0000000..03e05e7 --- /dev/null +++ b/src/components/Sidebar/FileTree.tsx @@ -0,0 +1,227 @@ +import { useState, useCallback } from 'react' +import type { FileEntry } from '@/types' +import { useTabsStore } from '@/stores/useTabsStore' +import { useContextMenuStore } from '@/stores/useContextMenuStore' + +const extColors: Record = { + ts: '#3178c6', + tsx: '#3178c6', + js: '#f7df1e', + jsx: '#f7df1e', + rs: '#dea584', + json: '#5eb7e0', + md: '#e8e8ed', + html: '#e34c4c', + css: '#42a5f5', + py: '#3572a5', +} + +const ROW_HEIGHT = 32 +const INDENT = 16 +const GUIDE_COLOR = 'var(--color-panel-border)' +const GUIDE_HOVER_COLOR = 'var(--color-text-muted)' + +function ChevronIcon({ expanded }: { expanded: boolean }) { + return ( + + + + ) +} + +function FileTypeIcon({ name, isDir, expanded }: { name: string; isDir: boolean; expanded: boolean }) { + if (isDir) { + return ( + + {expanded ? ( + + ) : ( + + )} + + ) + } + + const ext = name.split('.').pop()?.toLowerCase() ?? '' + const color = extColors[ext] ?? '#5c5e64' + + return ( + + + + + ) +} + +interface GuideLinesProps { + depth: number + branchDepths: number[] + isLast: boolean + parentHovered: boolean +} + +function GuideLines({ depth, branchDepths, isLast, parentHovered }: GuideLinesProps) { + const color = parentHovered ? GUIDE_HOVER_COLOR : GUIDE_COLOR + const mid = ROW_HEIGHT / 2 + const leftBase = 12 + + return ( +
+ {branchDepths.map((bd) => { + const l = leftBase + bd * INDENT + return ( +
+ ) + })} +
+
+
+ ) +} + +interface FileTreeItemProps { + entry: FileEntry + depth: number + isLast: boolean + branchDepths: number[] +} + +function FileTreeItem({ entry, depth, isLast, branchDepths }: FileTreeItemProps) { + const [expanded, setExpanded] = useState(depth < 1) + const [parentHovered, setParentHovered] = useState(false) + const openFile = useTabsStore((s) => s.openFile) + const activeTabId = useTabsStore((s) => s.activeTabId) + const openContextMenu = useContextMenuStore((s) => s.open) + + const handleContextMenu = useCallback((e: React.MouseEvent) => { + e.preventDefault() + openContextMenu(e.clientX, e.clientY, [ + { id: 'open', label: entry.is_dir ? 'Expand' : 'Open', icon: entry.is_dir ? 'folder' : 'file', action: () => entry.is_dir ? setExpanded(!expanded) : openFile(entry.path) }, + { id: 'sep1', label: '', separator: true, action: () => {} }, + { id: 'copyPath', label: 'Copy Path', icon: 'link', action: () => navigator.clipboard.writeText(entry.path) }, + ]) + }, [entry, expanded, openFile, openContextMenu]) + + if (entry.is_dir) { + return ( +
+ +
+ {entry.children && ( + + )} +
+
+ ) + } + + const isActive = entry.path === activeTabId + + return ( + + ) +} + +interface FileTreeProps { + entries: FileEntry[] + depth?: number + parentBranchDepths?: number[] +} + +export default function FileTree({ entries, depth = 0, parentBranchDepths = [] }: FileTreeProps) { + return ( +
+ {entries.map((entry, idx) => ( + + ))} +
+ ) +} -- cgit v1.2.3