experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(page): eliminate visual flash when navbar hides by awaiting setBounds

The navbar hide sequence had a timing mismatch: CSS classes were removed
synchronously (hiding panels instantly) while the window shrink via
setBounds() was async IPC. This caused 1-2 frames where panels were gone
but the window was still wide, making the webview appear to jump as
flexbox re-centered content in the oversized window.

Fix: await the setBounds() promise before removing CSS classes. The window
shrinks first (panels get clipped by window edge), then classes are cleaned
up. Added a generation counter to prevent show/hide race conditions.

+22 -5
+22 -5
app/page/page.js
··· 308 308 309 309 // --- Set extra window width (for panels, widgets, etc.) --- 310 310 // Expands the window symmetrically. Flexbox keeps the center column in place. 311 + // Returns a promise that resolves when the window bounds have been applied. 311 312 function setWindowPadding(width) { 312 313 width = Math.max(0, width); 313 - if (width === extraWidth) return; 314 + if (width === extraWidth) return Promise.resolve(); 314 315 extraWidth = width; 315 316 if (pendingBoundsUpdate) { 316 317 cancelAnimationFrame(pendingBoundsUpdate); 317 318 pendingBoundsUpdate = null; 318 319 } 319 - api.window.setBounds(computeWindowBounds(screenBounds)); 320 + const p = api.window.setBounds(computeWindowBounds(screenBounds)); 320 321 DEBUG && console.log('[page] Window padding set to', width); 322 + return p; 321 323 } 322 324 323 325 // Initial positioning ··· 909 911 // loadingLifecycle.onChange) to avoid TDZ errors when startLoading() calls show() 910 912 // during module initialization. 911 913 914 + // Generation counter to prevent stale hide() completions from clobbering a 915 + // subsequent show(). Incremented by both show() and hide() — if show() fires 916 + // while hide() is awaiting setBounds, the hide's post-await code sees a stale 917 + // generation and bails out. 918 + let navbarGeneration = 0; 919 + 912 920 function show(opts) { 913 921 console.log('[page] show() called, source:', opts?.source); 922 + navbarGeneration++; 914 923 if (hideTimer) { 915 924 clearTimeout(hideTimer); 916 925 hideTimer = null; ··· 936 945 setWindowPadding(PANEL_OVERHANG * 2); 937 946 } 938 947 939 - function hide() { 948 + async function hide() { 940 949 if (isDragging || holdDragPending) return; 941 950 // Don't hide while page is loading — navbar stays visible with spinner 942 951 if (loadingLifecycle.state === 'loading') return; ··· 944 953 clearTimeout(hideTimer); 945 954 hideTimer = null; 946 955 } 956 + const gen = ++navbarGeneration; 957 + // Shrink the window FIRST, before hiding any CSS elements. 958 + // setBounds is async IPC — if we hide panels (CSS) before the window shrinks, 959 + // there's a frame where panels are gone but the window is still wide, and 960 + // flexbox re-centers the content in the oversized window, causing a visible jump. 961 + // By awaiting setBounds first, the window shrinks while panels are still visible 962 + // (they get clipped by the window edge), then we clean up CSS classes. 963 + await setWindowPadding(0); 964 + // If show() was called while we were awaiting, abort — don't clobber the new state 965 + if (navbarGeneration !== gen) return; 947 966 navbar.classList.remove('visible'); 948 967 // Hide page info and entities panels alongside navbar 949 - // Shrink window back to normal 950 968 if (pageInfoPanel) pageInfoPanel.classList.remove('visible'); 951 969 if (entitiesPanel) entitiesPanel.classList.remove('visible'); 952 - setWindowPadding(0); 953 970 updatePositions(); 954 971 navbar.inputElement?.blur(); 955 972 showSource = null;