Files
tts-workshop/apps/web/src/components/ObjectTree.tsx
T
hypercross 55e0351c1d fix(web): select tree nodes by index path instead of GUID
Cards in a deck share the deck's GUID, so GUID-based selection highlighted
every card with that GUID and rendered the first match. Key selection by the
node's unique index path instead.
2026-08-08 17:42:16 +08:00

105 lines
3.0 KiB
TypeScript

import { 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) {
return (
<ul className="space-y-0.5">
{nodes.map((node, index) => (
<TreeNode
key={index}
node={node}
depth={0}
path={`${index}`}
selectedPath={selectedPath}
onSelect={onSelect}
/>
))}
</ul>
);
}
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 (
<li>
<div
className={`flex items-center rounded pr-2 ${
selected
? 'bg-zinc-700 text-zinc-100'
: 'text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100'
}`}
style={{ paddingLeft: `${depth * 16 + 4}px` }}
>
{hasChildren ? (
<button
onClick={() => setExpanded((e) => !e)}
aria-label={expanded ? 'Collapse' : 'Expand'}
className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-zinc-500 hover:text-zinc-200"
>
<span
className={`inline-block text-xs transition-transform ${expanded ? 'rotate-90' : ''}`}
>
</span>
</button>
) : (
<span className="h-6 w-6 shrink-0" />
)}
<button
onClick={() => onSelect(path)}
className="block min-w-0 flex-1 truncate py-1 text-left text-sm"
>
<span
title={node.object.Name}
className="mr-1.5 inline-flex items-center gap-0.5 align-middle text-zinc-400"
>
{iconsForObject(node.object.Name).map((icon) => (
<Icon key={icon} icon={icon} className="h-4 w-4" />
))}
</span>
<span className="truncate">{node.label}</span>
</button>
</div>
{hasChildren && expanded && (
<ul>
{node.children.map((child, index) => (
<TreeNode
key={`${path}-${index}`}
node={child}
depth={depth + 1}
path={`${path}-${index}`}
selectedPath={selectedPath}
onSelect={onSelect}
/>
))}
</ul>
)}
</li>
);
}