/* Console 2.0 — Readout strip: the output heatmap as a control surface (the thin top strip in Console). Same model as OutputStage: - drag / click a cell → set value - ⌥/alt-click a cell → cycle state (off → fixed → live) - hover a cell → open the OutputEditor */ const RS_GROUP_COLOR = { formant: '--accent', pitch: '--accent-2', amp: '--good', filter: '--warn', fx: '--info', mod: '--accent-3', }; const RS_NEXT = { off: 'fixed', fixed: 'live', live: 'off' }; function ReadoutStrip({ params, values, onChange, pinned, onTogglePin }) { const [open, setOpen] = React.useState(null); const timers = React.useRef({ open: null, close: null }); const drag = React.useRef({ i: -1, moved: false, startY: 0, el: null, alt: false }); const scheduleOpen = (i) => { clearTimeout(timers.current.close); clearTimeout(timers.current.open); timers.current.open = setTimeout(() => setOpen(i), 110); }; const scheduleClose = () => { clearTimeout(timers.current.open); clearTimeout(timers.current.close); timers.current.close = setTimeout(() => setOpen(null), 280); }; const hold = () => clearTimeout(timers.current.close); const valFromEvent = (el, clientY) => { const r = el.getBoundingClientRect(); return Math.max(0, Math.min(1, 1 - (clientY - r.top) / r.height)); }; const down = (e, i) => { e.currentTarget.setPointerCapture?.(e.pointerId); drag.current = { i, moved: false, startY: e.clientY, el: e.currentTarget, alt: e.altKey || e.metaKey }; }; const move = (e, i) => { const d = drag.current; if (d.i !== i) return; if (Math.abs(e.clientY - d.startY) > 3) d.moved = true; if (d.moved && !d.alt) onChange(i, { val: valFromEvent(d.el, e.clientY) }); }; const up = (e, i) => { const d = drag.current; if (d.i !== i) return; if (d.alt && !d.moved) onChange(i, { status: RS_NEXT[params[i].status] || 'live' }); else if (!d.moved) onChange(i, { val: valFromEvent(d.el, e.clientY) }); drag.current = { i: -1, moved: false, startY: 0, el: null, alt: false }; }; return (