/** * OutputEditor — the per-output menu (state · min · max · static value · curve). * Opens on hover over an output column/cell. Ported from `OutputEditor.jsx`. */ import { useLayoutEffect, useRef, useState } from 'react'; import type { CSSProperties, ReactNode } from 'react'; import { CurvePad } from './CurvePad'; import type { MFParam, ParamStatus } from './model'; const OE_STATUS: { v: ParamStatus; label: string; color: string }[] = [ { v: 'off', label: 'Off', color: 'var(--fg-dim)' }, { v: 'fixed', label: 'Fixed', color: 'var(--accent-2)' }, { v: 'live', label: 'Live', color: 'var(--accent)' }, ]; function MiniSlider({ label, value, onChange, disabled, }: { label: ReactNode; value: number; onChange: (v: number) => void; disabled?: boolean; }) { return ( ); } export interface OutputEditorProps { param: MFParam; onChange: (patch: Partial) => void; onHold: () => void; onLeave: () => void; place: CSSProperties; } export function OutputEditor({ param, onChange, onHold, onLeave, place }: OutputEditorProps) { const isLive = param.status === 'live'; const ref = useRef(null); // After the popover lands at its anchored spot, nudge it back on-screen so it // never clips the viewport edges (matters when the window isn't full-screen). const [shift, setShift] = useState<{ dx: number; dy: number }>({ dx: 0, dy: 0 }); useLayoutEffect(() => { const el = ref.current; if (!el) return; const fit = () => { const r = el.getBoundingClientRect(); // r already includes the current shift — back it out to get the natural // anchored box, then derive the shift that pulls it inside the viewport. const left = r.left - shift.dx; const top = r.top - shift.dy; const M = 8; // keep an 8px gutter from each edge let dx = 0; let dy = 0; if (left + r.width > window.innerWidth - M) dx = window.innerWidth - M - (left + r.width); if (left + dx < M) dx = M - left; if (top + r.height > window.innerHeight - M) dy = window.innerHeight - M - (top + r.height); if (top + dy < M) dy = M - top; // if too tall, prefer pinning the top if (dx !== shift.dx || dy !== shift.dy) setShift({ dx, dy }); }; fit(); window.addEventListener('resize', fit); return () => window.removeEventListener('resize', fit); }); return (
{param.name} {param.group}
{OE_STATUS.map((s) => { const on = param.status === s.v; return ( ); })}
onChange({ min: v })} /> onChange({ max: v })} /> onChange({ val: v })} disabled={isLive} /> onChange({ curve: c })} size={170} /> drag a bar to set value · ⌥/alt-click cycles state
); }