Initial Commit
This commit is contained in:
102
packages/ui/react/react-control-surface/src/runtime/surface.md
Normal file
102
packages/ui/react/react-control-surface/src/runtime/surface.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# `<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.
|
||||
Reference in New Issue
Block a user