103 lines
3.8 KiB
Markdown
103 lines
3.8 KiB
Markdown
# `<Surface>` — schema-driven runtime walker
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## 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
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
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.
|