PropertyTree
A recursive, data-driven nested tree on the Disclosure primitive — branch nodes disclose, leaf nodes optionally select, each row carrying a StatusPip and a trailing KPI.
Import
PropertyTree is a registry composite — you own the source after installing:
npx ploxum add property-treeimport { PropertyTree, type PropertyTreeNode } from "@/components/property-tree";Depends on @roger-emerson/ploxum-primitives (Disclosure, StatusPip) and @roger-emerson/ploxum-utils (cn).
Usage
const nodes: PropertyTreeNode[] = [
{
id: "north",
label: "North concourse",
status: "warning",
metric: "47/48",
defaultOpen: true,
children: [
{ id: "north-cam", label: "Cameras", status: "ok", metric: "24" },
{ id: "north-radio", label: "Radio comms", status: "warning", metric: "5/6" },
],
},
{
id: "south",
label: "South concourse",
status: "ok",
metric: "48/48",
children: [{ id: "south-cam", label: "Cameras", status: "ok", metric: "24" }],
},
];
<PropertyTree nodes={nodes} onSelect={(id) => openZoneDetail(id)} />;Branch nodes (those with children) render as a Disclosure; leaf nodes render as a row. When onSelect is supplied, every leaf renders as a button with hover + focus-ring affordances. Indentation is 16px per depth level.
Props
PropertyTree
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | PropertyTreeNode[] | — | Top-level tree nodes, in display order. |
onSelect | (id: string) => void | — | When provided, leaf rows render as buttons calling onSelect(id). |
className | string | — | Merged onto the root <div>. |
ref | Ref<HTMLDivElement> | — | Forwarded to the root <div>. |
PropertyTreeNode
| Field | Type | Description |
|---|---|---|
id | string | Stable key; also the value passed to onSelect. |
label | ReactNode | Row label, truncates on overflow. |
status | StatusPipStatus? | "live" | "ok" | "warning" | "critical" | "offline" | "unknown". Renders a leading StatusPip. |
metric | ReactNode? | Trailing KPI — rendered monospace + tabular-nums. Static text only. |
children | PropertyTreeNode[]? | Child nodes. Presence makes the node a disclosing branch. |
defaultOpen | boolean? | Initial open state for a branch node. |
Design rules
- Status is always a StatusPip — shape + color, never hue alone. The pip carries the accessible status label.
- A branch node's
metricis the Disclosuretrailingslot, which lives inside the toggle button — so the branch metric must stay static and non-interactive. Never place a control there. - Selection is leaf-only. Branch rows toggle disclosure; only leaves call
onSelect. Don't expect a branch click to fire selection. - Metrics use monospace tabular numerals so counts align down the tree as they tick.
