# react-control-surface spec delta ## ADDED Requirements ### Requirement: BoundControlProps contract Every atomic control component SHALL accept `BoundControlProps`: `{ value, onChange, min, max, step?, default?, disabled?, label?, ariaLabel?, renderHint? }`. No control reads from React context or runs side-effects beyond `onChange`. Pure value-in / value-out. #### Scenario: Knob fires onChange on drag - **WHEN** a `` receives a pointer-down + pointer-move 100 px upward - **THEN** `spy` is called with a value > 0.5 #### Scenario: Slider fires onChange on keyboard arrow - **WHEN** a `` is focused and ArrowUp is pressed - **THEN** `spy` is called with `0.6` **Verified by:** `src/controls/knob/knob.test.tsx`, `src/controls/slider-v/slider-v.test.tsx` ### Requirement: 18 atomic controls The package SHALL export 18 atomic controls covering every `ControlKind` in `@suaveplan/control-surface-schema`: `Knob`, `SliderV`, `SliderH`, `ButtonMomentary`, `ButtonLatching`, `Switch2`, `Switch3`, `LED`, `DigitalDisplay`, `MeterVU`, `MeterPeak`, `MeterSpectral`, `XYPad`, `JogWheel`, `PianoKey`, `DrumPad`, `TextLabel`, `Image`. #### Scenario: Every kind is exported - **WHEN** the package barrel is imported - **THEN** every name in the closed `ControlKind` list resolves to a React component **Verified by:** `tests/exports.test.tsx` ### Requirement: Surface runtime walks the schema `` SHALL DFS-walk the schema and render each node via the appropriate atomic component for its `kind`. Unknown kinds throw `ReactControlSurfaceError("REACT_CONTROL_SURFACE.UNKNOWN_KIND")`. #### Scenario: 4-pad drum surface renders 4 buttons - **WHEN** a surface with a 4-grid Group containing 4 `drum-pad` Controls is rendered - **THEN** the DOM contains 4 `[role="button"]` elements with the proper labels #### Scenario: Unknown kind throws - **WHEN** a surface contains a Control with `kind: "not-a-thing"` (forced past schema validation in a test) - **THEN** rendering throws `UNKNOWN_KIND` **Verified by:** `tests/runtime.test.tsx` ### Requirement: BindingRegistry dispatch `BindingRegistry` SHALL dispatch each binding kind (`pluginParam`, `transport`, `midiOut`, `midiIn`, `dmx`, `compound`) to its registered handler. Missing handlers throw `BINDING_HANDLER_MISSING`. #### Scenario: pluginParam binding drives onChange to the param hook - **WHEN** a knob with binding `{ kind: "pluginParam", pluginId: "p1", paramPath: "vol" }` is dragged and the `pluginParam` handler is a spy - **THEN** the spy is called with `{ pluginId: "p1", paramPath: "vol", value }` matching the knob's emitted value #### Scenario: compound binding fans out - **WHEN** a knob has `{ kind: "compound", bindings: [pluginParam, midiOut] }` - **THEN** every drag invokes both handlers, in registration order **Verified by:** `tests/binding-fanout.test.tsx` ### Requirement: Accessibility — every control is operable without a mouse Every control SHALL be focusable via Tab, operable via arrow keys, expose `aria-valuemin` / `aria-valuemax` / `aria-valuenow` / `aria-valuetext` where applicable, respect `prefers-reduced-motion`, and have a non-empty `aria-label` derived from `label` or explicitly passed. #### Scenario: Knob aria attributes - **WHEN** a `` is rendered - **THEN** the rendered DOM has `role="slider"`, `aria-valuemin="0"`, `aria-valuemax="1"`, `aria-valuenow="0.5"`, `aria-label="Volume"` **Verified by:** `tests/accessibility.test.tsx` ### Requirement: Skin contract via CSS variables `` SHALL inject `--cs-knob-track-color`, `--cs-knob-indicator-color`, `--cs-slider-thumb-color`, etc., consumable by every atomic control via standard CSS variable resolution. No JS-level skin dispatch. #### Scenario: Skin overrides take effect - **WHEN** a `` wraps a `` - **THEN** the knob's computed `--cs-knob-track-color` is `#ff8800` **Verified by:** `src/skin/surface-skin-provider.test.tsx` ### Requirement: OTel instrumentation Every control interaction SHALL increment `control_surface_interactions_total{kind}`. Every `` render commit SHALL record `control_surface_render_duration_ms`. #### Scenario: Counter increments on press - **WHEN** a `` is pressed 5 times against an in-memory `MeterProvider` - **THEN** the snapshot shows `control_surface_interactions_total{kind=button-momentary}=5` **Verified by:** `src/telemetry/telemetry.test.ts` ### Requirement: MeterSpectral consumes analysis-spectrum output `` SHALL accept a `Float32Array` of magnitude bins (or an `AsyncIterable` for streaming) and render the bins as a bar graph. The component does NOT import `@suaveplan/audio-analysis-spectrum` directly — it accepts pre-computed bins, keeping it tier-pure (no analysis dependency). #### Scenario: Bin array renders bars - **WHEN** `` is rendered - **THEN** the DOM contains 4 bar elements with heights proportional to the bins **Verified by:** `src/controls/meter-spectral/meter-spectral.test.tsx`