({
node,
depth,
selectedFile,
onFileClick,
searchQuery,
}: {
node: TreeNode;
depth: number;
selectedFile: string | null;
onFileClick: (path: string | null) => void;
searchQuery: string;
})
| 236 | |
| 237 | // ─── Tree Item ──────────────────────────────────────────────────────────────── |
| 238 | function TreeItem({ |
| 239 | node, |
| 240 | depth, |
| 241 | selectedFile, |
| 242 | onFileClick, |
| 243 | searchQuery, |
| 244 | }: { |
| 245 | node: TreeNode; |
| 246 | depth: number; |
| 247 | selectedFile: string | null; |
| 248 | onFileClick: (path: string | null) => void; |
| 249 | searchQuery: string; |
| 250 | }) { |
| 251 | const [open, setOpen] = useState(depth < 2); |
| 252 | |
| 253 | // Auto-expand when search active |
| 254 | useEffect(() => { |
| 255 | if (searchQuery) setOpen(true); |
| 256 | }, [searchQuery]); |
| 257 | |
| 258 | const isMatch = node.name.toLowerCase().includes(searchQuery.toLowerCase()); |
| 259 | const hasMatchingDescendant = (n: TreeNode): boolean => |
| 260 | n.name.toLowerCase().includes(searchQuery.toLowerCase()) || |
| 261 | n.children.some(hasMatchingDescendant); |
| 262 | |
| 263 | if (searchQuery && !hasMatchingDescendant(node) && !isMatch) return null; |
| 264 | |
| 265 | const indent = depth * 12; |
| 266 | |
| 267 | if (node.isDir) { |
| 268 | return ( |
| 269 | <div> |
| 270 | <button |
| 271 | onClick={() => setOpen(o => !o)} |
| 272 | className="w-full flex items-center gap-1 py-[3px] px-2 rounded-lg text-gray-400 hover:text-white hover:bg-purple-500/10 transition-colors group" |
| 273 | style={{ paddingLeft: `${indent + 8}px` }} |
| 274 | > |
| 275 | {open |
| 276 | ? <ChevronDown className="w-3 h-3 flex-shrink-0 text-gray-500" /> |
| 277 | : <ChevronRight className="w-3 h-3 flex-shrink-0 text-gray-500" />} |
| 278 | {open |
| 279 | ? <FolderOpen className="w-3.5 h-3.5 flex-shrink-0 text-amber-400 ml-0.5" /> |
| 280 | : <Folder className="w-3.5 h-3.5 flex-shrink-0 text-amber-400 ml-0.5" />} |
| 281 | <span className="text-[13px] text-gray-300 group-hover:text-white truncate font-medium ml-1"> |
| 282 | {node.name} |
| 283 | </span> |
| 284 | </button> |
| 285 | {open && ( |
| 286 | <div> |
| 287 | {node.children.map(child => ( |
| 288 | <TreeItem |
| 289 | key={child.path} |
| 290 | node={child} |
| 291 | depth={depth + 1} |
| 292 | selectedFile={selectedFile} |
| 293 | onFileClick={onFileClick} |
| 294 | searchQuery={searchQuery} |
| 295 | /> |
nothing calls this directly
no test coverage detected