diff --git a/MAP.md b/MAP.md
index 27b547e..143a4df 100644
--- a/MAP.md
+++ b/MAP.md
@@ -59,8 +59,8 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set.
consts (mode_id, engine_id, ml dims, params, voice_spaces, ui) — the SOURCE OF TRUTH for `MF_MODES`.
Switching mode reshapes the WASM net to the mode's `ml` dims (ConsoleApp P5.3; boot mode paf_synth →
4→[10,10,14]→33).
-- `manifold/src/dock/` — `OutputControlRow` (add/delete card identity + off/fixed/live + mute + solo/arm + min/max/curve), `output-state.ts`,
- `OutputsBackendConfig.tsx` (per-backend specialised Outputs panel — the sole per-backend editor).
+- `manifold/src/dock/` — `OutputControlRow` (add/delete card identity + off/fixed/live + mute + solo/arm + min/max/curve, plus MIDI card fields), `output-state.ts`,
+ `OutputsBackendConfig.tsx` (per-backend specialised Outputs panel — the sole per-backend editor; the centered add-card control lives below the rows).
- `manifold/src/backends/` — `OutputBackend` adapter + `BackendManager` (spine consumer); `midi-backend.ts`
(WebMIDI), `osc-backend.ts`+`osc-client.ts` (OSC-over-WS), `vcv-backend.ts` (VCV-over-WS), `cv-backend.ts`
(`UseqCvBackend` — uSEQ CV/gate over USB Web Serial, backend id `cvgate`) + `useq-protocol.ts` (v2 wire
@@ -68,7 +68,7 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set.
`particle-backend.ts`, `passthrough-backend.ts`, `presets.ts` (named presets), `manager.ts`.
- `manifold/src/midi-devices/` — external-synth device templates. `generated/` is codegen output from
`schemas/midi_devices/` (`MIDI_DEVICES` catalogue + `MIDI_DEVICES_BY_ID`, params by name+CC). The MIDI Outputs
- config (`dock/OutputsBackendConfig.tsx`) reads it for the device picker + param-select that fills the CC table.
+ config (`dock/OutputsBackendConfig.tsx`) reads it for the device picker + param-select that fills the per-card MIDI fields.
- `manifold/src/inputs/` — modular INPUT layer feeding the ML head. The Inputs dock picks ONE exclusive mode
(`InputMode` = `internal` | `gamepad` | `midi`; Internal/XY-pad is default). `input-layer.ts` owns a single rAF
loop composing the active source's axes → **one dedicated engine input slot per axis, 1:1, no blending** → one
diff --git a/manifold/ONBOARDING.md b/manifold/ONBOARDING.md
index 9ed0d59..9ac50ee 100644
--- a/manifold/ONBOARDING.md
+++ b/manifold/ONBOARDING.md
@@ -166,9 +166,10 @@ for the narrow pane.
curve/fixedValue + backend specs `MidiCcSpec`/`OscSpec`/`VcvSpec`), `toOutputControl()`,
`buildArmMask()` (solo focus).
- `OutputControlRow.tsx` — one output row: name · M(mute) · S(solo/arm) · off|fixed|live · dual-range
- · curve pad · live value. **Writes eagerly to the shared `MFParam` store via `onChange`.**
- - `OutputsBackendConfig.tsx` — preset bar (save/restore/rename/delete) + per-backend config (MIDI
- CC#/channel, OSC path/range, VCV polarity).
+ · curve pad · live value; MIDI mode adds the card's name, CC#, and channel fields. **Writes eagerly
+ to the shared `MFParam` store via `onChange`.** The centered `+` control follows the last visible card.
+ - `OutputsBackendConfig.tsx` — preset bar (save/restore/rename/delete) + transport/device config (MIDI
+ port/templates, OSC path/range, VCV polarity); MIDI per-output fields live on `OutputControlRow`.
### Primitives — `src/primitives/` (barrel: `index.ts`)
Seven: `Button`, `Slider`, `PillToggle`, `Badge`, `Switch`, `XYPad`, `VirtualJoystick`. Dumb,
diff --git a/manifold/src/console/Drawers.tsx b/manifold/src/console/Drawers.tsx
index 31b222e..6129482 100644
--- a/manifold/src/console/Drawers.tsx
+++ b/manifold/src/console/Drawers.tsx
@@ -757,9 +757,6 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
off {counts.off || 0}
muted {mutedN}
-
- + output
-
{ModeConfig(ctx, depth)}
{/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden when condensed. */}
@@ -787,9 +784,32 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
onChange={(patch) => ctx.setParam(i, patch)}
onDelete={activeParams.length > 1 ? () => ctx.deleteOutput(i) : undefined}
showCurve={expanded}
+ showMidi={ctx.outputMode === 'midi'}
/>
);
})}
+
+ +
+
{!expanded && activeParams.length > 6 && (
+{activeParams.length - 6} more — expand to edit
diff --git a/manifold/src/dock/OutputControlRow.tsx b/manifold/src/dock/OutputControlRow.tsx
index 40c13f9..3246816 100644
--- a/manifold/src/dock/OutputControlRow.tsx
+++ b/manifold/src/dock/OutputControlRow.tsx
@@ -12,6 +12,7 @@ import { GROUP_COLOR } from '../console/model';
import type { MFParam, ParamStatus } from '../console/model';
import { CurvePad } from '../console/CurvePad';
import { DualRange } from '../console/DualRange';
+import { defaultMidiSpec } from './output-state';
const STATE_META: { v: ParamStatus; label: string; color: string }[] = [
{ v: 'off', label: 'off', color: 'var(--fg-dim)' },
@@ -64,6 +65,8 @@ export interface OutputControlRowProps {
onDelete?: () => void;
/** Show the curve pad inline (expand depth); hidden in compact rows. */
showCurve?: boolean;
+ /** Show the per-card MIDI name, CC, and channel controls. */
+ showMidi?: boolean;
}
export function OutputControlRow({
@@ -72,12 +75,14 @@ export function OutputControlRow({
onChange,
onDelete,
showCurve = false,
+ showMidi = false,
}: OutputControlRowProps) {
const gc = `var(${GROUP_COLOR[param.group] || '--accent'})`;
const muted = param.muted ?? false;
const armed = param.armed ?? false;
const off = param.status === 'off';
const barVal = param.status === 'fixed' ? param.val : value;
+ const midi = param.midi ?? defaultMidiSpec(param.engineIndex ?? 0);
return (
-
- {param.name}
-
+ {showMidi ? (
+ onChange({ midi: { ...midi, name: e.target.value } })}
+ style={{
+ flex: 1,
+ minWidth: 0,
+ width: 0,
+ border: 0,
+ borderBottom: '1px solid var(--line)',
+ background: 'transparent',
+ color: 'var(--fg)',
+ fontFamily: 'var(--font-mono)',
+ fontSize: 'var(--fs-xs)',
+ padding: '1px 0',
+ }}
+ />
+ ) : (
+
+ {param.name}
+
+ )}
{param.group}
{onDelete && (
+ {showMidi && (
+
+
+ CC
+
+ onChange({ midi: { ...midi, cc: Math.max(0, Math.min(127, Number(e.target.value) || 0)) } })
+ }
+ style={{
+ width: 54,
+ background: 'var(--bg-1)',
+ border: '1px solid var(--line)',
+ borderRadius: 'var(--r-1)',
+ color: 'var(--fg)',
+ fontFamily: 'var(--font-mono)',
+ fontSize: 10,
+ padding: '2px 4px',
+ }}
+ />
+
+
+ ch
+
+ onChange({
+ midi: { ...midi, channel: Math.max(1, Math.min(16, Number(e.target.value) || 1)) },
+ })
+ }
+ style={{
+ width: 44,
+ background: 'var(--bg-1)',
+ border: '1px solid var(--line)',
+ borderRadius: 'var(--r-1)',
+ color: 'var(--fg)',
+ fontFamily: 'var(--font-mono)',
+ fontSize: 10,
+ padding: '2px 4px',
+ }}
+ />
+
+
+ )}
+
{/* tri-state segmented */}
diff --git a/manifold/src/dock/OutputsBackendConfig.tsx b/manifold/src/dock/OutputsBackendConfig.tsx
index 59f14c8..b9e5143 100644
--- a/manifold/src/dock/OutputsBackendConfig.tsx
+++ b/manifold/src/dock/OutputsBackendConfig.tsx
@@ -4,7 +4,8 @@
*
* Layout: ONE preset bar (save-as / restore / rename / delete, per-backend
* namespace) on top, then a per-backend config section:
- * - MIDI → output-port picker, number-of-CCs, per-output CC#/channel/name.
+ * - MIDI → output-port picker, number-of-CCs, and device templates; each
+ * output card owns its CC#/channel/name fields.
* - OSC → bridge URL + connect status + send-raw toggle, per-output path/range.
* - VCV/CV→ bridge URL + connect status + send-raw toggle, per-output polarity
* (uni 0–10 V / bipolar ±5 V). The browser drives + trains the VCV
@@ -20,7 +21,7 @@
import { useEffect, useState } from 'react';
import type { ConsoleCtx } from '../console/types';
import type { BackendId, CvChannelId } from './output-state';
-import { CV_CHANNELS, defaultCvSpec, defaultMidiSpec, defaultOscSpec, defaultVcvSpec } from './output-state';
+import { CV_CHANNELS, defaultCvSpec, defaultOscSpec, defaultVcvSpec } from './output-state';
import {
applyPreset,
deletePreset,
@@ -212,8 +213,8 @@ function PresetBar({ ctx, backend }: { ctx: ConsoleCtx; backend: BackendId }) {
/**
* Pick an external synth (e.g. Moog Sub 37), see its parameters BY NAME, tick the
- * ones to control, and "Apply" — this fills the per-output CC table (CC#, channel,
- * name) from the device template and sets the CC count. Pure local state + the
+ * ones to control, and "Apply" — this fills the per-output card fields (CC#,
+ * channel, name) from the device template and sets the CC count. Pure local state + the
* existing setParam/setMidiCcCount; the applied result lives in the shared params
* store, so it is saved/restored by the named-preset bar like any other config.
*/
@@ -355,72 +356,13 @@ function MidiConfig({ ctx }: { ctx: ConsoleCtx }) {
))}
- {ctx.midiCcCount} CC output{ctx.midiCcCount === 1 ? '' : 's'} · add/delete cards below
+ {ctx.midiCcCount} MIDI output card{ctx.midiCcCount === 1 ? '' : 's'}
{s.message}
-
Per-output CC · name · channel
-
-
-
-
- Name
- CC#
- Ch
- State
-
-
-
- {ctx.params.slice(0, ctx.midiCcCount).map((p, i) => {
- const m = p.midi ?? defaultMidiSpec(i);
- return (
-
-
- ctx.setParam(i, { midi: { ...m, name: e.target.value } })}
- />
-
-
-
- ctx.setParam(i, { midi: { ...m, cc: Math.max(0, Math.min(127, num(e.target.value, m.cc))) } })
- }
- />
-
-
-
- ctx.setParam(i, {
- midi: { ...m, channel: Math.max(1, Math.min(16, num(e.target.value, m.channel))) },
- })
- }
- />
-
-
- {p.status}
- {p.muted ? ' · muted' : ''}
-
-
- );
- })}
-
-
-
>
);
}