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`.
|
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 →
|
Switching mode reshapes the WASM net to the mode's `ml` dims (ConsoleApp P5.3; boot mode paf_synth →
|
||||||
4→[10,10,14]→33).
|
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).
|
`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`
|
- `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`
|
(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/
|
- `output-state.ts` — the per-output control model: `OutputControl` (state/muted/armed/min/max/
|
||||||
curve/fixedValue + backend specs `MidiCcSpec`/`OscSpec`/`VcvSpec`), `toOutputControl()`,
|
curve/fixedValue + backend specs `MidiCcSpec`/`OscSpec`/`VcvSpec`), `toOutputControl()`,
|
||||||
`buildArmMask()` (solo focus).
|
`buildArmMask()` (solo focus).
|
||||||
- `OutputControlRow.tsx` — one output row: name · M(mute) · S(solo/arm) · off|fixed|live · dual-range
|
- `OutputControlRow.tsx` — one output card: editable name · M(mute) · S(solo/arm) · cycling
|
||||||
· curve pad · live value; MIDI mode adds the card's name, CC#, and channel fields. **Writes eagerly
|
off/fixed/live status · dual-range · live value; the curve pad is a right-hand column and MIDI mode
|
||||||
to the shared `MFParam` store via `onChange`.** The centered `+` control follows the last visible card.
|
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
|
- `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`.
|
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,
|
flex: expanded ? 1 : undefined,
|
||||||
minHeight: expanded ? 0 : undefined,
|
minHeight: expanded ? 0 : undefined,
|
||||||
maxHeight: expanded ? undefined : 220,
|
maxHeight: expanded ? undefined : 220,
|
||||||
|
padding: expanded ? '8px 10px' : '6px 10px',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* OutputControlRow — the shared per-output baseline control row (dock-spec §3.2).
|
* OutputControlRow — the shared per-output baseline control row (dock-spec §3.2).
|
||||||
* Reused across the Routing, Synth and Visual drawers. Renders the FULL baseline:
|
* 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
|
* Writes eagerly through `onChange` into the single shared MFParam store
|
||||||
* (ConsoleApp owns it) — never a second data path (dock-spec §3.2, §8).
|
* (ConsoleApp owns it) — never a second data path (dock-spec §3.2, §8).
|
||||||
|
|
@ -65,7 +65,7 @@ export interface OutputControlRowProps {
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
/** Show the curve pad inline (expand depth); hidden in compact rows. */
|
/** Show the curve pad inline (expand depth); hidden in compact rows. */
|
||||||
showCurve?: boolean;
|
showCurve?: boolean;
|
||||||
/** Show the per-card MIDI name, CC, and channel controls. */
|
/** Show the per-card MIDI CC and channel controls. */
|
||||||
showMidi?: boolean;
|
showMidi?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,28 +81,37 @@ export function OutputControlRow({
|
||||||
const muted = param.muted ?? false;
|
const muted = param.muted ?? false;
|
||||||
const armed = param.armed ?? false;
|
const armed = param.armed ?? false;
|
||||||
const off = param.status === 'off';
|
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 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 10,
|
||||||
background: 'var(--bg-2)',
|
background: 'var(--bg-2)',
|
||||||
border: `1px solid ${armed ? 'var(--accent)' : 'var(--line)'}`,
|
border: `1px solid ${armed ? 'var(--accent)' : 'var(--line)'}`,
|
||||||
boxShadow: armed ? '0 0 0 1px var(--glow-accent)' : 'none',
|
boxShadow: armed ? '0 0 0 1px var(--glow-accent)' : 'none',
|
||||||
borderRadius: 'var(--r-1)',
|
borderRadius: 'var(--r-1)',
|
||||||
padding: '5px 7px',
|
padding: '8px 10px',
|
||||||
opacity: off ? 0.6 : 1,
|
opacity: off ? 0.6 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 5 }}>
|
||||||
{showMidi ? (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<input
|
<input
|
||||||
aria-label={`MIDI name for ${param.name}`}
|
aria-label={`Output name for ${param.name}`}
|
||||||
value={midi.name}
|
value={param.name}
|
||||||
onChange={(e) => onChange({ midi: { ...midi, name: e.target.value } })}
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
name: e.target.value,
|
||||||
|
...(showMidi ? { midi: { ...midi, name: e.target.value } } : {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
|
|
@ -116,195 +125,158 @@ export function OutputControlRow({
|
||||||
padding: '1px 0',
|
padding: '1px 0',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>{param.group}</span>
|
||||||
<span
|
<GlyphToggle
|
||||||
style={{
|
on={muted}
|
||||||
flex: 1,
|
glyph="M"
|
||||||
minWidth: 0,
|
title={muted ? 'Muted (silenced downstream, still computed)' : 'Mute downstream'}
|
||||||
fontSize: 'var(--fs-xs)',
|
color="var(--danger)"
|
||||||
color: 'var(--fg)',
|
onClick={() => onChange({ muted: !muted })}
|
||||||
overflow: 'hidden',
|
/>
|
||||||
textOverflow: 'ellipsis',
|
<GlyphToggle
|
||||||
whiteSpace: 'nowrap',
|
on={armed}
|
||||||
}}
|
glyph="S"
|
||||||
>
|
title={armed ? 'Armed — focus training on this output' : 'Solo / arm (focus training)'}
|
||||||
{param.name}
|
color="var(--accent)"
|
||||||
</span>
|
onClick={() => onChange({ armed: !armed })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showMidi && (
|
||||||
|
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--fg-mute)' }}>
|
||||||
|
CC
|
||||||
|
<input
|
||||||
|
aria-label={`MIDI CC for ${param.name}`}
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={127}
|
||||||
|
value={midi.cc}
|
||||||
|
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' }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--fg-mute)' }}>
|
||||||
|
ch
|
||||||
|
<input
|
||||||
|
aria-label={`MIDI channel for ${param.name}`}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={16}
|
||||||
|
value={midi.channel}
|
||||||
|
onChange={(e) =>
|
||||||
|
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' }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>{param.group}</span>
|
|
||||||
{onDelete && (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Delete ${param.name} output`}
|
title={`Status: ${state.label} · click for ${nextState.label}`}
|
||||||
title={`Delete ${param.name} output`}
|
onClick={() => onChange({ status: nextState.v })}
|
||||||
onClick={onDelete}
|
|
||||||
style={{
|
style={{
|
||||||
width: 22,
|
minWidth: 52,
|
||||||
height: 22,
|
fontSize: 9,
|
||||||
borderRadius: 'var(--r-1)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
background: 'transparent',
|
|
||||||
color: 'var(--danger)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: 'var(--font-mono)',
|
fontFamily: 'var(--font-mono)',
|
||||||
fontSize: 14,
|
textTransform: 'uppercase',
|
||||||
lineHeight: 1,
|
letterSpacing: '0.04em',
|
||||||
|
padding: '3px 6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: `1px solid ${state.color}`,
|
||||||
|
background: state.color,
|
||||||
|
color: 'var(--bg)',
|
||||||
|
borderRadius: 'var(--r-1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
×
|
{state.label}
|
||||||
</button>
|
</button>
|
||||||
)}
|
<DualRange
|
||||||
<GlyphToggle
|
min={param.min}
|
||||||
on={muted}
|
max={param.max}
|
||||||
glyph="M"
|
onMin={(v) => onChange({ min: v })}
|
||||||
title={muted ? 'Muted (silenced downstream, still computed)' : 'Mute downstream'}
|
onMax={(v) => onChange({ max: v })}
|
||||||
color="var(--danger)"
|
/>
|
||||||
onClick={() => onChange({ muted: !muted })}
|
|
||||||
/>
|
|
||||||
<GlyphToggle
|
|
||||||
on={armed}
|
|
||||||
glyph="S"
|
|
||||||
title={armed ? 'Armed — focus training on this output' : 'Solo / arm (focus training)'}
|
|
||||||
color="var(--accent)"
|
|
||||||
onClick={() => onChange({ armed: !armed })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showMidi && (
|
|
||||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--fg-mute)' }}>
|
|
||||||
CC
|
|
||||||
<input
|
|
||||||
aria-label={`MIDI CC for ${param.name}`}
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={127}
|
|
||||||
value={midi.cc}
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--fg-mute)' }}>
|
|
||||||
ch
|
|
||||||
<input
|
|
||||||
aria-label={`MIDI channel for ${param.name}`}
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={16}
|
|
||||||
value={midi.channel}
|
|
||||||
onChange={(e) =>
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</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 })}
|
|
||||||
style={{
|
|
||||||
fontSize: 9,
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.04em',
|
|
||||||
padding: '2px 5px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
border: `1px solid ${on ? s.color : 'var(--line)'}`,
|
|
||||||
background: on ? s.color : 'transparent',
|
|
||||||
color: on ? 'var(--bg)' : 'var(--fg-dim)',
|
|
||||||
borderRadius: 'var(--r-1)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{s.label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<DualRange
|
|
||||||
min={param.min}
|
|
||||||
max={param.max}
|
|
||||||
onMin={(v) => onChange({ min: v })}
|
|
||||||
onMax={(v) => onChange({ max: v })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* value bar (live model value, or held fixed value) */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
height: 6,
|
|
||||||
background: 'var(--bg-1)',
|
|
||||||
borderRadius: 'var(--r-pill)',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
title={`value ${barVal.toFixed(3)}${muted ? ' (muted)' : ''}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'relative',
|
||||||
left: 0,
|
height: 6,
|
||||||
top: 0,
|
background: 'var(--bg-1)',
|
||||||
bottom: 0,
|
borderRadius: 'var(--r-pill)',
|
||||||
width: `${Math.max(0, Math.min(1, barVal)) * 100}%`,
|
overflow: 'hidden',
|
||||||
background: muted ? 'var(--fg-dim)' : gc,
|
|
||||||
opacity: muted ? 0.4 : 0.8,
|
|
||||||
transition: 'width 70ms linear',
|
|
||||||
}}
|
}}
|
||||||
/>
|
title={`value ${barVal.toFixed(3)}${muted ? ' (muted)' : ''}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: `${Math.max(0, Math.min(1, barVal)) * 100}%`,
|
||||||
|
background: muted ? 'var(--fg-dim)' : gc,
|
||||||
|
opacity: muted ? 0.4 : 0.8,
|
||||||
|
transition: 'width 70ms linear',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{param.status === 'fixed' && (
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--fg-mute)', textTransform: 'uppercase' }}>held</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
value={param.val}
|
||||||
|
onChange={(e) => onChange({ val: parseFloat(e.target.value) })}
|
||||||
|
className="mf-slider-input"
|
||||||
|
style={{ flex: 1, ['--mf-pct' as string]: `${Math.max(0, Math.min(1, param.val))}` } as CSSProperties}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{param.status === 'fixed' && (
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
||||||
<span style={{ fontSize: 9, color: 'var(--fg-mute)', textTransform: 'uppercase' }}>held</span>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.01"
|
|
||||||
value={param.val}
|
|
||||||
onChange={(e) => onChange({ val: parseFloat(e.target.value) })}
|
|
||||||
className="mf-slider-input"
|
|
||||||
style={{ flex: 1, ['--mf-pct' as string]: `${Math.max(0, Math.min(1, param.val))}` } as CSSProperties}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showCurve && (
|
{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} />
|
<CurvePad curve={param.curve} onChange={(c) => onChange({ curve: c })} size={88} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue