Structural Search
A step-through viewer for a search across a structured space — a decision tree, an oracle, a state-search graph. The caller supplies the nodes (each with an id, label, optional parent and edge label) and the path of node ids the search visits in order. The component lays the nodes out as a top-down tree, draws edges between parents and children, highlights the prefix of the visited path, pulses the current node, and surfaces the per-step narration.
Pure layout / playback primitive — it does not run a search, score, or decide. The caller authors the nodes and the path; the component renders them. Drives the canonical "follow the decision tree" lesson (binary-search oracle, alpha-beta game tree, DPLL solver, A* on a small grid) and any side-by-side "what the structure looks like vs. which branch we chose" narration.
Start at the oracle root. Decide which half of the array is sorted.
Installation
npx shadcn@latest add https://craftbits.dev/r/structural-search.jsonUsage
import {
StructuralSearch,
type StructuralSearchNode,
type StructuralSearchStep,
} from "@craft-bits/core";
const nodes: StructuralSearchNode[] = [
{ id: "root", label: "arr[lo] <= arr[mid]" },
{ id: "true", label: "left sorted", parent: "root", edgeLabel: "true" },
{ id: "false", label: "right sorted", parent: "root", edgeLabel: "false" },
];
const path: StructuralSearchStep[] = [
{ nodeId: "root", description: "Start at the oracle root." },
{ nodeId: "true", description: "arr[lo] <= arr[mid] holds." },
];
<StructuralSearch nodes={nodes} path={path} />Controlled — the parent owns the step and can drive it from a prediction gate or an autoplay timer:
const [step, setStep] = useState(0);
<StructuralSearch
nodes={nodes}
path={path}
step={step}
onStepChange={setStep}
/>Hide the built-in prev / next controls when the step is driven entirely from outside:
<StructuralSearch nodes={nodes} path={path} step={step} hideControls />Each node can carry an edgeLabel rendered on the edge from its parent — useful for branch labels like true / false, or in-range / out:
const nodes: StructuralSearchNode[] = [
{ id: "root", label: "compare" },
{ id: "lt", label: "go left", parent: "root", edgeLabel: "lt" },
{ id: "ge", label: "go right", parent: "root", edgeLabel: "ge" },
];Understanding the component
- Pure structural replay. The component is fed two arrays:
nodes(the search space) andpath(the ordered ids visited). The same triple of nodes, path, and step always produces the same view — there is no internal search algorithm, scoring, or branching logic. - Topology from
parentreferences. Each node references its parent by id; order insidenodesdoes not matter. The layout walks parents to derive depth, then arranges nodes left-to-right within each depth row using a fixed cell size and a gap. - Visited prefix highlight. Every node with an index in
path[0..step]is painted with the active tone. Edges between two visited nodes go from thin and muted to thick and tone-coloured; everything else stays neutral. - Current-node pulse. The node at the active step carries a subtle sonar pulse and a 1.5-px tone-coloured ring. The pulse collapses to instant under
prefers-reduced-motion: reduce. - Edge labels. When a node declares
edgeLabel, the label renders centred on the edge from its parent in a small mono chip. The chip adopts the active tone when both endpoints are on the visited path. - Controlled and uncontrolled
step. PairstepwithonStepChangefor Radix-style control, or passdefaultStepand let the component own its step. Built-in prev / next chrome can be hidden when a parent drives the step from outside. - Step dots. A 10-px dot per step doubles as a jump-to control. Dots before the active one render filled and dimmed; the active one renders at 1.15-scale and full opacity. Each dot's hit area expands to 44 by 44 px.
- Keyboard. Arrow-left / arrow-right on the root advance the step. Step dots and prev / next buttons are real buttons with full focus, Enter, and Space support.
- Reduced motion. Edge fades, node pulse, narration cross-fade, prev / next tap scale, and dot scale all collapse to instant under
prefers-reduced-motion: reduce. The search still advances; only the motion drops.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | readonly StructuralSearchNode[] | required | Nodes in the search space. Each declares an id, label, optional parent, and optional edge label. |
path | readonly StructuralSearchStep[] | required | Ordered route through the structure. Step i visits the node at path[i].nodeId. |
step | number | — | Controlled current step. Pair with onStepChange. |
defaultStep | number | 0 | Uncontrolled initial step. |
onStepChange | (next: number) => void | — | Fires when the active step changes. |
tone | "default" | "accent" | "success" | "warning" | "error" | "accent" | Highlight palette for the path, the current-node pulse, and the edge accent. |
title | ReactNode | — | Optional title rendered above the tree. |
prevLabel | string | "Prev" | Label for the previous-step button. |
nextLabel | string | "Next" | Label for the next-step button. |
hideControls | boolean | false | Hide the built-in prev / next chrome. |
transition | Transition | SPRINGS.smooth | Override transitions. Reduced-motion users snap regardless. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- A visually-hidden
aria-live="polite"region narrates the current step, the visited node's label, and the per-step description so screen readers stay current as the search advances. - The root carries
aria-roledescription="structural search"plusdata-stateanddata-toneattributes so assistive tooling and consumer styles can hook into the search state. - The tree renders inside an
<svg role="img">with anaria-label, so the structure is announced as a single image rather than a stream of decorative shapes. - Each step dot is a real
<button>with an explicitaria-labeland anaria-current="step"flag on the active one. The visible dot is 10 by 10 px; the hit pad expands to 44 by 44 px to satisfy WCAG 2.5.8 AAA. - Each rendered node carries
data-currentanddata-visitedattributes so styles can react to traversal state without relying on tone hue alone. Current vs. visited vs. resting is also signalled by stroke weight and a sonar pulse, not by colour alone. - Prev / next buttons render at 44 by 44 px minimum, disable themselves at the ends, and respond to keyboard
EnterandSpace. Arrow-left / arrow-right keys on the root advance the step too. - Motion respects
prefers-reduced-motion: reduce— edge fades, node pulse, narration cross-fade, prev / next tap scale, and dot scale collapse to instant. The search still advances; only the motion drops.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/construction/StructuralSearch.tsx). The source was a 2,400-line six-phase Discardability Engine game (Feel → Build → Stress Test → Rewire → Code Bridge → Completion) that taught binary search as a "discard half" predicate by drag-building two decision-tree oracles, stress-testing them across five scenarios with prediction gates, watching the rotated-array oracle fail on a peak array, then rewiring it and mapping the oracle nodes back to real code. The library extract drops the game, the reducer, the scoring, the audio, and the prediction gates. What remains is the structural spine: a tree of nodes laid out top-down, a stepwise path through them, a visited-prefix highlight, and a current-node pulse. Consumers compose any narrative on top.