diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cfc44f6..c89a1d2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -845,13 +845,29 @@ export default function App() { useEffect(() => { wkEsmRef.current = wkEsm; }, [wkEsm]); // Persistent Ctrl+wheel zoom. The native WebView2 Ctrl+wheel zoom isn't saved, - // so we run our own: CSS `zoom` on the root element, factor stored in - // localStorage and restored at startup. Ctrl+0 resets to 100%. + // so we run our own: CSS `zoom` on the app root, factor stored in localStorage + // and restored at startup. Ctrl+0 resets to 100%. + // + // `zoom` scales the element uniformly (grid included) but does NOT rescale + // viewport units, so a 100vh/100vw root would leave an empty strip when zoomed + // out. We counter that by sizing the root to (100/z)vw × (100/z)vh so that, + // after the zoom multiplies it back down, it exactly fills the window. + const appRootRef = useRef(null); useEffect(() => { const KEY = 'opslog.uiZoom'; let z = parseFloat(localStorage.getItem(KEY) || '1'); if (!Number.isFinite(z) || z <= 0) z = 1; - const apply = () => { (document.documentElement.style as any).zoom = String(z); }; + const apply = () => { + // Zoom the whole document (so portalled menus/modals/tooltips scale too)… + (document.documentElement.style as any).zoom = String(z); + // …and counter-size the app root: `zoom` doesn't rescale vh/vw, so a 100vh + // root leaves an empty strip when zoomed out. (100/z)vh zoomed by z = 100vh. + const el = appRootRef.current; + if (el) { + el.style.width = `${100 / z}vw`; + el.style.height = `${100 / z}vh`; + } + }; apply(); const onWheel = (e: WheelEvent) => { if (!e.ctrlKey && !e.metaKey) return; // plain wheel is left alone (scroll / freq nudge) @@ -4034,7 +4050,7 @@ export default function App() { }; return ( -
+
{/* ===== TOPBAR ===== */} {compact ? (