Initial Commit
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# @suaveplan/react-midi — Web MIDI API React adapter
|
||||
|
||||
- **Target path**: `packages/ui/react/react-midi/`
|
||||
- **Tier**: browser/react
|
||||
- **Date**: 2026-05-23
|
||||
- **Status**: PROPOSED
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/midi` ships universal-tier MIDI types and a `MidiPortLike` structural contract but never touches the platform. To consume MIDI in a browser app, React components need:
|
||||
|
||||
1. Permission gating (`navigator.requestMIDIAccess({ sysex })`) with promise-shaped state for Suspense + ErrorBoundary integration.
|
||||
2. Port enumeration with live add/remove (devices plug/unplug at runtime).
|
||||
3. Per-port input/output hooks that wire into the universal decoder/encoder and surface typed `MidiMessage` events.
|
||||
4. SysEx opt-in (separate permission scope).
|
||||
5. Safe StrictMode behaviour (double-mount + cleanup must not double-bind).
|
||||
|
||||
This package owns the Web MIDI bridge so every downstream React surface (control-surface runtime, react-control-surface editor, react-audio-player's hardware-controller integration) shares one set of hooks instead of reimplementing permission/state plumbing.
|
||||
|
||||
## What changes
|
||||
|
||||
1. **NEW package** `@suaveplan/react-midi` at `packages/ui/react/react-midi/`.
|
||||
2. **`<MidiProvider>`** — context provider that triggers `navigator.requestMIDIAccess` exactly once (StrictMode-safe), exposes the `MIDIAccess` state, supports `sysex: boolean` prop, surfaces permission denial as a typed `MidiPermissionError`.
|
||||
3. **Hooks**:
|
||||
- `useMidi()` — returns `{ access, status: "loading"|"granted"|"denied"|"unsupported", inputs: MidiPortLike[], outputs: MidiPortLike[] }`.
|
||||
- `useMidiInput(portId | "first"): MidiPortLike | null` — picks a single input; subscribers receive parsed `MidiMessage` events via `port.onMessage`.
|
||||
- `useMidiOutput(portId | "first"): MidiPortLike | null` — sends bytes via the universal `encodeMessage`.
|
||||
- `useMidiMessages(portId, filter?)` — returns the most recent N messages (ring buffer) filtered by message type. For UI display.
|
||||
- `useMidiDevices()` — returns the live `{ inputs, outputs }` array, re-renders on `statechange`.
|
||||
- `useNRPN(portId)` / `useRPN(portId)` / `useCC14(portId)` — high-resolution-controller reducers exposed as React hooks.
|
||||
4. **`<MidiDeviceList>` + `<MidiActivityIndicator>`** — small UI primitives for picker / activity visualisation. Each is composable; consumers pass their own renderProps for custom styling.
|
||||
5. **Connect-state events** — fires `onDeviceAdd` / `onDeviceRemove` via context, plus per-port `state` reads.
|
||||
6. **Error codes** — `REACT_MIDI.{ACCESS_DENIED, UNSUPPORTED_PLATFORM, PORT_NOT_FOUND, SYSEX_NOT_PERMITTED}` via `defineCodes`.
|
||||
7. **OTel instrumentation** — counter `react_midi_messages_received_total{port}`, counter `react_midi_messages_sent_total{port}`.
|
||||
|
||||
## Impact
|
||||
|
||||
- New package depending on `@suaveplan/midi@workspace:^` and React 19 peer dep.
|
||||
- Browser-tier — uses `navigator.requestMIDIAccess`. Falls back gracefully (`status: "unsupported"`) on platforms that lack Web MIDI (Safari without flag, Firefox, sandboxed contexts).
|
||||
- Unblocks `@suaveplan/react-control-surface` (CC bindings drive plugin parameters via `useParameter` from `@suaveplan/react-audio`).
|
||||
Reference in New Issue
Block a user