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
walkSurfacefrom@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.dispatchinvoked 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.