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