Three-column composition shell for the visual authoring surface.
Stitches Wave 2/3 pieces together:
- Left (200px): inline palette — one categorized button per primitive
kind enumerated from PRIMITIVE_REGISTRY.list(), clicking seeds the
descriptor with generateDefaultParams(kind) + an empty params tree
where Zod schemas expose a nested primitives array.
- Center (flex): BlockList with the descriptors primitives, routes
select/expand/remove/reorder/nested-reorder callbacks back through
immutable descriptor updates.
- Right (320px): PreviewPane (narrative / JSON / board tabs).
State lives locally:
- selectedIndex: number | null
- expandedIndices: ReadonlySet<number>
Both recompute sensibly after reorder/remove so UI focus never points
at a stale slot.
Tree mutations are immutable throughout: top-level reorder uses
arrayMove; nested reorder deep-clones the affected parent nodes
params.primitives without touching siblings. Removing a primitive at
depth N only rewrites the ancestor chain down to that node.
Invalid descriptor: if validationResult.ok === false, a yellow warning
banner lists the error messages above the grid. The builder remains
usable below the banner so the author can keep editing to resolve
errors rather than being locked out.
Tests (VisualBuilderPane.test.tsx, 4 scenarios, react-dom/server
harness): renders 3 columns, invalid descriptor shows banner, palette
includes buttons for all 22 primitive kinds, nested trigger structure
renders with child BlockCards in the expanded area.
T22 wires this pane into CustomModifierEditor behind a Form/Visual
mode toggle.