File size: 2,756 Bytes
9b906ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 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>
);
}
|