Components
Scene Hierarchy Tree
Scene hierarchy tree visualiser for React and Svelte.
The Scene Hierarchy Tree renders an outliner-style tree for entities in a scene graph. It is aimed at:
- Level editors
- Visualisation tools
- Any app that needs a structured tree of nodes with selection
It focuses on multi-level nesting, clear selection styling, and a neutral presentation you can theme with shadcn.
Selected: none
Installation (shadcn-style CLI)
These commands assume you will expose Geometa components through a shadcn-style CLI. Adjust the exact names once your CLI is wired up.
# React / Next.js
npx shadcn@latest add geometa-scene-tree
# Svelte
npx shadcn-svelte@latest add geometa-scene-treeUsage – React (Next.js)
'use client';
import * as React from 'react';
export type SceneNode = {
id: string;
name: string;
children?: SceneNode[];
};
interface SceneTreeProps {
nodes: SceneNode[];
selectedId?: string;
onSelect?(id: string): void;
}
export function SceneTree({ nodes, selectedId, onSelect }: SceneTreeProps) {
return (
<div className="rounded-md border bg-background p-2 text-sm">
<ul className="space-y-0.5">
{nodes.map((node) => (
<SceneTreeNode
key={node.id}
node={node}
depth={0}
selectedId={selectedId}
onSelect={onSelect}
/>
))}
</ul>
</div>
);
}
interface SceneTreeNodeProps {
node: SceneNode;
depth: number;
selectedId?: string;
onSelect?(id: string): void;
}
function SceneTreeNode({
node,
depth,
selectedId,
onSelect,
}: SceneTreeNodeProps) {
const [expanded, setExpanded] = React.useState(true);
const isSelected = node.id === selectedId;
const hasChildren = (node.children?.length ?? 0) > 0;
return (
<li>
<button
type="button"
className={[
'flex w-full items-center gap-1 rounded px-1 py-0.5 text-left outline-none',
isSelected ? 'bg-primary/10 text-primary' : 'hover:bg-muted',
].join(' ')}
style={{ paddingLeft: 4 + depth * 12 }}
onClick={() => onSelect?.(node.id)}
>
{hasChildren && (
<span
className="inline-flex h-4 w-4 items-center justify-center rounded-sm border text-[10px]"
onClick={(event) => {
event.stopPropagation();
setExpanded((prev) => !prev);
}}
>
{expanded ? '-' : '+'}
</span>
)}
{!hasChildren && <span className="inline-block h-4 w-4" />}
<span className="truncate font-mono text-xs text-muted-foreground">
{node.name}
</span>
</button>
{hasChildren && expanded && (
<ul className="mt-0.5 space-y-0.5">
{node.children!.map((child) => (
<SceneTreeNode
key={child.id}
node={child}
depth={depth + 1}
selectedId={selectedId}
onSelect={onSelect}
/>
))}
</ul>
)}
</li>
);
}
// Example usage
const exampleScene: SceneNode[] = [
{
id: 'root',
name: 'Root',
children: [
{ id: 'camera', name: 'Camera' },
{
id: 'light',
name: 'Light',
children: [
{ id: 'spot', name: 'Spot Light' },
{ id: 'ambient', name: 'Ambient Light' },
],
},
{ id: 'mesh', name: 'Mesh' },
],
},
];
export function SceneTreeExample() {
const [selectedId, setSelectedId] = React.useState<string | undefined>();
return (
<div className="flex flex-col gap-3">
<SceneTree
nodes={exampleScene}
selectedId={selectedId}
onSelect={setSelectedId}
/>
<div className="text-xs text-muted-foreground">
Selected:{' '}
<span className="font-mono">
{selectedId ?? 'none'}
</span>
</div>
</div>
);
}