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>
  );
}