import { useMemo, useState } from 'react'; import { Icon } from '@iconify/react'; import type { ObjectTreeNode } from '@tts/extract'; import { iconsForObject } from './objectIcons'; interface Props { nodes: ObjectTreeNode[]; selectedPath: string | null; onSelect: (path: string) => void; } export default function ObjectTree({ nodes, selectedPath, onSelect }: Props) { const [highlighted, setHighlighted] = useState>(() => new Set()); const types = useMemo(() => collectTypes(nodes), [nodes]); // When nothing is highlighted, every type is shown. Otherwise only the // highlighted types are visible. const visibleNodes = useMemo( () => filterTree(nodes, highlighted), [nodes, highlighted], ); const toggleType = (name: string) => setHighlighted((prev) => { const next = new Set(prev); if (next.has(name)) next.delete(name); else next.add(name); return next; }); const clearAll = () => setHighlighted(new Set()); return (
{types.length > 0 && (
{types.map(({ name, count }) => { const active = highlighted.has(name); return ( ); })} {highlighted.size > 0 && ( )}
)}
); } function TreeNode({ node, depth, path, selectedPath, onSelect, }: { node: ObjectTreeNode; depth: number; /** Index path from the root, used as a stable unique key and selection id. */ path: string; selectedPath: string | null; onSelect: (path: string) => void; }) { // Selection is keyed by the node's unique index path, not its GUID: cards in // a deck frequently share a GUID (the deck's), so GUID-based selection would // highlight and render the wrong card. const selected = path === selectedPath; const hasChildren = node.children.length > 0; const [expanded, setExpanded] = useState(false); return (
  • {hasChildren ? ( ) : ( )}
    {hasChildren && expanded && ( )}
  • ); } /** Distinct object types present in the tree, with a count of each. */ function collectTypes(nodes: ObjectTreeNode[]): { name: string; count: number }[] { const counts = new Map(); const visit = (node: ObjectTreeNode) => { counts.set(node.object.Name, (counts.get(node.object.Name) ?? 0) + 1); node.children.forEach(visit); }; nodes.forEach(visit); return [...counts.entries()] .map(([name, count]) => ({ name, count })) .sort((a, b) => a.name.localeCompare(b.name)); } /** * Keep a node when its type is highlighted or any descendant survives, so the * containment hierarchy is preserved and highlighted parents still lead to * highlighted children. With nothing highlighted, every node is kept. * * Each result carries the node's original index path into the full (unfiltered) * tree, so selection stays stable regardless of filtering. */ function filterTree( nodes: ObjectTreeNode[], highlighted: Set, prefix = '', ): { node: ObjectTreeNode; path: string }[] { const result: { node: ObjectTreeNode; path: string }[] = []; nodes.forEach((node, index) => { const path = prefix ? `${prefix}-${index}` : `${index}`; const children = filterTree(node.children, highlighted, path); const visible = highlighted.size === 0 || highlighted.has(node.object.Name) || children.length > 0; if (visible) { result.push({ node: children.length > 0 ? { ...node, children: children.map((c) => c.node) } : node, path, }); } }); return result; }