({ nodes, onNodeSelect }: Props)
| 38 | |
| 39 | /** Compact table-like row layout for node lists */ |
| 40 | export default function NodeListResult({ nodes, onNodeSelect }: Props) { |
| 41 | const [expanded, setExpanded] = useState(false); |
| 42 | |
| 43 | if (nodes.length === 0) { |
| 44 | return <p className="result-empty">No nodes found.</p>; |
| 45 | } |
| 46 | |
| 47 | // Count by type for the header |
| 48 | const typeCounts: Record<string, number> = {}; |
| 49 | for (const n of nodes) { |
| 50 | typeCounts[n.type] = (typeCounts[n.type] || 0) + 1; |
| 51 | } |
| 52 | const summary = Object.entries(typeCounts) |
| 53 | .sort((a, b) => b[1] - a[1]) |
| 54 | .map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`) |
| 55 | .join(', '); |
| 56 | |
| 57 | const visible = expanded ? nodes : nodes.slice(0, INITIAL_LIMIT); |
| 58 | const hasMore = nodes.length > INITIAL_LIMIT; |
| 59 | |
| 60 | return ( |
| 61 | <div className="result-node-list"> |
| 62 | <div className="result-list-header">{summary}</div> |
| 63 | {visible.map((node) => { |
| 64 | const color = getNodeColor(node.type); |
| 65 | const inlineProp = node.properties |
| 66 | ? pickInlineProp(node.properties) |
| 67 | : null; |
| 68 | return ( |
| 69 | <div |
| 70 | key={node.id} |
| 71 | className="result-row" |
| 72 | onClick={() => onNodeSelect?.(node.id)} |
| 73 | > |
| 74 | <span className="result-type-badge" style={{ background: color }}> |
| 75 | {node.type} |
| 76 | </span> |
| 77 | <span className="result-node-name">{node.name}</span> |
| 78 | {inlineProp && ( |
| 79 | <span className="result-prop-inline">{inlineProp}</span> |
| 80 | )} |
| 81 | </div> |
| 82 | ); |
| 83 | })} |
| 84 | {hasMore && ( |
| 85 | <button |
| 86 | className="result-show-more" |
| 87 | onClick={() => setExpanded(!expanded)} |
| 88 | > |
| 89 | {expanded ? 'Show less' : `Show all ${nodes.length}`} |
| 90 | </button> |
| 91 | )} |
| 92 | </div> |
| 93 | ); |
| 94 | } |
nothing calls this directly
no test coverage detected