Download src/components/features/files-tab/tree-node.tsx from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 2.76 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/files-tab/tree-node.tsx
- Command line
-
hf download hf://SaylorTwift/openhands/src/components/features/files-tab/tree-node.tsx
-
curl -L -o tree-node.tsx https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/files-tab/tree-node.tsx
2.76 kB
| import { useState } from "react"; | |
| import { ChevronDown, ChevronRight } from "lucide-react"; | |
| import FileIcon from "#/icons/file.svg?react"; | |
| import FolderIcon from "#/icons/folder.svg?react"; | |
| import { FileTreeNode } from "#/utils/file-tree"; | |
| import { cn } from "#/utils/utils"; | |
| interface TreeNodeProps { | |
| node: FileTreeNode; | |
| depth: number; | |
| selectedPath: string | null; | |
| onSelectFile: (path: string) => void; | |
| } | |
| export function TreeNode({ | |
| node, | |
| depth, | |
| selectedPath, | |
| onSelectFile, | |
| }: TreeNodeProps) { | |
| const [isOpen, setIsOpen] = useState(false); | |
| const indentPx = 8 + depth * 12; | |
| if (node.isDirectory) { | |
| return ( | |
| <li> | |
| <button | |
| type="button" | |
| onClick={() => setIsOpen((prev) => !prev)} | |
| aria-expanded={isOpen} | |
| data-testid={`file-tree-dir-${node.path}`} | |
| className={cn( | |
| "flex w-full items-center gap-1.5 py-1 pr-2 text-left text-sm text-white", | |
| "hover:bg-tertiary cursor-pointer", | |
| )} | |
| // per-row indentation computed from tree depth at runtime | |
| style={{ paddingLeft: `${indentPx}px` }} | |
| > | |
| <span | |
| aria-hidden | |
| className="inline-flex w-3.5 shrink-0 items-center justify-center text-[var(--oh-muted)]" | |
| > | |
| {isOpen ? ( | |
| <ChevronDown className="w-3.5 h-3.5" /> | |
| ) : ( | |
| <ChevronRight className="w-3.5 h-3.5" /> | |
| )} | |
| </span> | |
| <FolderIcon className="w-3.5 h-3.5 shrink-0" /> | |
| <span className="truncate">{node.name}</span> | |
| </button> | |
| {isOpen && node.children.length > 0 && ( | |
| <ul> | |
| {node.children.map((child) => ( | |
| <TreeNode | |
| key={child.path} | |
| node={child} | |
| depth={depth + 1} | |
| selectedPath={selectedPath} | |
| onSelectFile={onSelectFile} | |
| /> | |
| ))} | |
| </ul> | |
| )} | |
| </li> | |
| ); | |
| } | |
| const isSelected = selectedPath === node.path; | |
| return ( | |
| <li> | |
| <button | |
| type="button" | |
| onClick={() => onSelectFile(node.path)} | |
| data-testid={`file-tree-file-${node.path}`} | |
| className={cn( | |
| "flex w-full items-center gap-1.5 py-1 pr-2 text-left text-sm", | |
| "hover:bg-tertiary cursor-pointer", | |
| isSelected | |
| ? "bg-[var(--oh-interactive-hover)] text-white" | |
| : "text-[var(--oh-text-tertiary)]", | |
| )} | |
| // per-row indentation computed from tree depth at runtime | |
| style={{ paddingLeft: `${indentPx + 16}px` }} | |
| > | |
| <FileIcon className="w-3.5 h-3.5 shrink-0" /> | |
| <span className="truncate">{node.name}</span> | |
| </button> | |
| </li> | |
| ); | |
| } | |