From b7c46828f31a8498e16e67c5fc2b483e90f39963 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Sun, 28 Jun 2026 22:21:18 +0200 Subject: [PATCH] fix(manifold): keep the output hover-editor inside the viewport The OutputEditor popover was placed with a fixed top + an index-based left/right heuristic and never measured the viewport, so it clipped the screen edges when the window wasn't full-screen. It now measures its anchored rect on layout and translates itself back inside the viewport (8px gutter), re-checking on resize. --- manifold/src/console/OutputEditor.tsx | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/manifold/src/console/OutputEditor.tsx b/manifold/src/console/OutputEditor.tsx index 73904c7..c9f935b 100644 --- a/manifold/src/console/OutputEditor.tsx +++ b/manifold/src/console/OutputEditor.tsx @@ -2,6 +2,7 @@ * 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'; @@ -67,8 +68,35 @@ export interface OutputEditorProps { 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 (