Files
skills/docs/ui/react/react-control-surface/src/runtime/surface.md
2026-05-29 08:20:56 +01:00

3.8 KiB

<Surface> — schema-driven runtime walker

(no submodules)

Purpose

<Surface> is the runtime that turns a validated ControlSurface document into a live React DOM tree. It performs a depth-first traversal of the schema, renders every Control node via the appropriate atomic component for its kind, and lays out every Group node via one of the three grouping primitives (<Rack> / <Panel> / <Group>) chosen by the group's layout.kind. Every control's onChange is automatically wired to bindings.dispatch(control.binding, value) so the consumer never plumbs handlers manually.

This is the entry point for every consumer of the package. The atomic components are exported separately for cases where the consumer wants to compose a static UI, but the typical pattern is to author the surface as JSON, validate it with @suaveplan/control-surface-schema, populate a BindingRegistry, and let <Surface> do the rest.

Features

  • DFS-walk via walkSurface from @suaveplan/control-surface-schema
  • 18 atomic controls dispatched by kind
  • Three grouping primitives chosen by layout.kind (grid → <Rack>, stack → <Panel>, absolute / free3d → <Group>)
  • bindings.dispatch invoked on every control change
  • Unbound controls render but emit nothing (the no-op handler is a closure, not a per-render allocation)
  • Unknown control kinds throw REACT_CONTROL_SURFACE.UNKNOWN_KIND
  • buildSurfaceTree(schema, bindings) exported for custom shells

Usage: Basic

import {
    Surface,
    BindingRegistry,
} from "@suaveplan/react-control-surface";
import drumKitSurface from "./drum-kit.surface.json";

const registry = new BindingRegistry();
registry.register("pluginParam", ({ pluginId, paramPath, value }) => {
    engine.setParameter(pluginId, paramPath, value);
});

<Surface schema={drumKitSurface} bindings={registry} />;

Usage: Advanced

import {
    buildSurfaceTree,
    BindingRegistry,
} from "@suaveplan/react-control-surface";

function SkinnedSurface({ schema, registry, skinId }: Props) {
    const tree = useMemo(
        () => buildSurfaceTree(schema, registry),
        [schema, registry]
    );
    return (
        <SurfaceSkinProvider id={skinId}>
            <ErrorBoundary fallback={<FailedToRender />}>{tree}</ErrorBoundary>
        </SurfaceSkinProvider>
    );
}

API Reference

Prop Type Description
schema ControlSurface Pre-validated surface document.
bindings BindingRegistry Registry whose dispatch is wired to each control.

The exported buildSurfaceTree(schema, bindings) returns the rendered ReactNode without the <Surface> memoisation wrapper — use it when you need to mount the tree under a custom provider tree without an extra render layer.

Implementation Notes

The walker runs walkSurface once to collect every node in pre-order DFS, then iterates the collection in reverse to render leaves first and bubble each rendered element up into the parent group's children prop. This single-pass approach is O(n) and avoids recursive React re-renders during traversal — every component is constructed once and React reconciles them all in a single commit.

Each control's onChange is wrapped in a closure that calls bindings.dispatch(binding, value). When binding is undefined we return a no-op closure so the control still receives a stable handler identity (avoiding spurious re-renders inside the atomic components). Unknown layout kinds and control kinds fall through to TypeScript's exhaustiveness guard and throw REACT_CONTROL_SURFACE.UNKNOWN_KIND with the offending value embedded in the message.