5.3 KiB
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
<Knob value={0.5} min={0} max={1} onChange={spy} />receives a pointer-down + pointer-move 100 px upward - THEN
spyis called with a value > 0.5
Scenario: Slider fires onChange on keyboard arrow
- WHEN a
<SliderV value={0.5} step={0.1} onChange={spy} />is focused and ArrowUp is pressed - THEN
spyis called with0.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
ControlKindlist resolves to a React component
Verified by: tests/exports.test.tsx
Requirement: Surface runtime walks the schema
<Surface schema={ControlSurface} bindings={BindingRegistry} /> 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-padControls 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 thepluginParamhandler 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
<Knob value={0.5} min={0} max={1} label="Volume" />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
<SurfaceSkinProvider id="skin-id" vars={overrides} /> 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
<SurfaceSkinProvider id="nki" vars={{ "--cs-knob-track-color": "#ff8800" }}>wraps a<Knob> - THEN the knob's computed
--cs-knob-track-coloris#ff8800
Verified by: src/skin/surface-skin-provider.test.tsx
Requirement: OTel instrumentation
Every control interaction SHALL increment control_surface_interactions_total{kind}. Every <Surface> render commit SHALL record control_surface_render_duration_ms.
Scenario: Counter increments on press
- WHEN a
<ButtonMomentary>is pressed 5 times against an in-memoryMeterProvider - 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
<MeterSpectral> SHALL accept a Float32Array of magnitude bins (or an AsyncIterable<Float32Array> 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
<MeterSpectral bins={new Float32Array([0.1, 0.5, 0.9, 0.3])} />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