refactor(manifold): streamline output cards
This commit is contained in:
parent
5ab4d18523
commit
b5891a6ab9
4 changed files with 159 additions and 185 deletions
2
MAP.md
2
MAP.md
|
|
@ -59,7 +59,7 @@ 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, plus MIDI card fields), `output-state.ts`,
|
||||
- `manifold/src/dock/` — `OutputControlRow` (editable names, corner delete, cycling off/fixed/live status, 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`
|
||||
|
|
|
|||
|
|
@ -165,9 +165,10 @@ for the narrow pane.
|
|||
- `output-state.ts` — the per-output control model: `OutputControl` (state/muted/armed/min/max/
|
||||
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; 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.
|
||||
- `OutputControlRow.tsx` — one output card: editable name · M(mute) · S(solo/arm) · cycling
|
||||
off/fixed/live status · dual-range · live value; the curve pad is a right-hand column and MIDI mode
|
||||
adds CC#/channel fields. The delete `×` sits on the upper-right corner. **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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -770,6 +770,7 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
flex: expanded ? 1 : undefined,
|
||||
minHeight: expanded ? 0 : undefined,
|
||||
maxHeight: expanded ? undefined : 220,
|
||||
padding: expanded ? '8px 10px' : '6px 10px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* OutputControlRow — the shared per-output baseline control row (dock-spec §3.2).
|
||||
* Reused across the Routing, Synth and Visual drawers. Renders the FULL baseline:
|
||||
*
|
||||
* name · M (mute) · S (solo/arm) · [off|fixed|live] · dual-range · curve · value
|
||||
* editable name · M (mute) · S (solo/arm) · cycling status · dual-range · curve · value
|
||||
*
|
||||
* Writes eagerly through `onChange` into the single shared MFParam store
|
||||
* (ConsoleApp owns it) — never a second data path (dock-spec §3.2, §8).
|
||||
|
|
@ -65,7 +65,7 @@ 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. */
|
||||
/** Show the per-card MIDI CC and channel controls. */
|
||||
showMidi?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -81,28 +81,37 @@ export function OutputControlRow({
|
|||
const muted = param.muted ?? false;
|
||||
const armed = param.armed ?? false;
|
||||
const off = param.status === 'off';
|
||||
const barVal = param.status === 'fixed' ? param.val : value;
|
||||
const barVal = param.status === 'fixed' ? param.min + param.val * (param.max - param.min) : value;
|
||||
const midi = param.midi ?? defaultMidiSpec(param.engineIndex ?? 0);
|
||||
const stateIndex = Math.max(0, STATE_META.findIndex((s) => s.v === param.status));
|
||||
const state = STATE_META[stateIndex];
|
||||
const nextState = STATE_META[(stateIndex + 1) % STATE_META.length];
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
background: 'var(--bg-2)',
|
||||
border: `1px solid ${armed ? 'var(--accent)' : 'var(--line)'}`,
|
||||
boxShadow: armed ? '0 0 0 1px var(--glow-accent)' : 'none',
|
||||
borderRadius: 'var(--r-1)',
|
||||
padding: '5px 7px',
|
||||
padding: '8px 10px',
|
||||
opacity: off ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 5 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{showMidi ? (
|
||||
<input
|
||||
aria-label={`MIDI name for ${param.name}`}
|
||||
value={midi.name}
|
||||
onChange={(e) => onChange({ midi: { ...midi, name: e.target.value } })}
|
||||
aria-label={`Output name for ${param.name}`}
|
||||
value={param.name}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
name: e.target.value,
|
||||
...(showMidi ? { midi: { ...midi, name: e.target.value } } : {}),
|
||||
})
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
|
|
@ -116,44 +125,7 @@ export function OutputControlRow({
|
|||
padding: '1px 0',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: 'var(--fs-xs)',
|
||||
color: 'var(--fg)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{param.name}
|
||||
</span>
|
||||
)}
|
||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>{param.group}</span>
|
||||
{onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Delete ${param.name} output`}
|
||||
title={`Delete ${param.name} output`}
|
||||
onClick={onDelete}
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 'var(--r-1)',
|
||||
border: '1px solid var(--line)',
|
||||
background: 'transparent',
|
||||
color: 'var(--danger)',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 14,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
<GlyphToggle
|
||||
on={muted}
|
||||
glyph="M"
|
||||
|
|
@ -183,16 +155,7 @@ export function OutputControlRow({
|
|||
onChange={(e) =>
|
||||
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',
|
||||
}}
|
||||
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' }}
|
||||
/>
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--fg-mute)' }}>
|
||||
|
|
@ -204,53 +167,35 @@ export function OutputControlRow({
|
|||
max={16}
|
||||
value={midi.channel}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
midi: { ...midi, channel: Math.max(1, Math.min(16, Number(e.target.value) || 1)) },
|
||||
})
|
||||
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',
|
||||
}}
|
||||
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' }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{/* tri-state segmented */}
|
||||
<div style={{ display: 'flex', gap: 1, flex: '0 0 auto' }}>
|
||||
{STATE_META.map((s) => {
|
||||
const on = param.status === s.v;
|
||||
return (
|
||||
<button
|
||||
key={s.v}
|
||||
type="button"
|
||||
onClick={() => onChange({ status: s.v })}
|
||||
title={`Status: ${state.label} · click for ${nextState.label}`}
|
||||
onClick={() => onChange({ status: nextState.v })}
|
||||
style={{
|
||||
minWidth: 52,
|
||||
fontSize: 9,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.04em',
|
||||
padding: '2px 5px',
|
||||
padding: '3px 6px',
|
||||
cursor: 'pointer',
|
||||
border: `1px solid ${on ? s.color : 'var(--line)'}`,
|
||||
background: on ? s.color : 'transparent',
|
||||
color: on ? 'var(--bg)' : 'var(--fg-dim)',
|
||||
border: `1px solid ${state.color}`,
|
||||
background: state.color,
|
||||
color: 'var(--bg)',
|
||||
borderRadius: 'var(--r-1)',
|
||||
}}
|
||||
>
|
||||
{s.label}
|
||||
{state.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<DualRange
|
||||
min={param.min}
|
||||
max={param.max}
|
||||
|
|
@ -259,7 +204,6 @@ export function OutputControlRow({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* value bar (live model value, or held fixed value) */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
|
|
@ -299,12 +243,40 @@ export function OutputControlRow({
|
|||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCurve && (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<div style={{ flex: '0 0 88px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<CurvePad curve={param.curve} onChange={(c) => onChange({ curve: c })} size={88} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Delete ${param.name} output`}
|
||||
title={`Delete ${param.name} output`}
|
||||
onClick={onDelete}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -7,
|
||||
right: -7,
|
||||
zIndex: 2,
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
border: '1px solid var(--danger)',
|
||||
background: 'var(--bg-2)',
|
||||
color: 'var(--danger)',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 14,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue