From 0b8c4fe1f75a8971517d8d2a42d9bb1c75fed05f Mon Sep 17 00:00:00 2001 From: TerryM Date: Mon, 8 Jun 2026 01:31:26 +0800 Subject: [PATCH] fix(header): kill the inline<->burger oscillation around the threshold width User report: after switching to Bahasa Melayu and opening the window to full width, the header juddered every frame. Root cause was a feedback loop between the fit measurement and the right-side layout: * In inline mode the right side renders the favorites label ("Kegemaran Saya") AND no burger button => ~80px wider. * In burger mode it renders the burger but drops the label => ~80px narrower. Available space for the nav was therefore mode-dependent, and the old 60px hysteresis was smaller than that ~80px swing. In a band roughly 1282-1302px (with ms needed=591px) the measurement decided: burger: needed + 60 <= available_burger -> flip to inline inline: needed > available_inline -> flip back to burger (repeat every frame, ResizeObserver re-fires, header shakes) Two-part fix: 1. Keep the burger button mounted in both modes. When inline it is pointer-events:none + invisible + aria-hidden=true + tabIndex=-1, so it stays unclickable but still occupies its 40px box. The right-side width no longer changes when we flip modes, so the measurement input stops jumping. 2. Widen the burger->inline hysteresis from 60 to 120 to absorb the remaining ~80px difference from the favorites label (and per-locale variations: "My Favorites" vs "Kegemaran Saya" etc). The header now picks a mode at each width and stays there. Verified by sweeping the row width across 1100-2200px on the ms locale in the browser: modeFlips=0 at every previously-broken width, single clean burger->inline transition once there's room. --- src/layouts/PublicLayout.tsx | 45 ++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index c45a510..f208a95 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -421,9 +421,13 @@ export function PublicLayout() { setShowInlineNav((current) => { if (current) return needed <= available; - // Need real breathing room before switching back, to avoid flicker - // when the right-side width shrinks after the burger button hides. - return needed + 60 <= available; + // Hysteresis has to be wider than the right-side width difference + // between the two modes (“My Favorites / Kegemaran Saya” label + // shows in inline mode, ~80px). 60px was not enough and produced a + // dead zone (~1282–1302px in ms) where the header oscillated each + // frame. 120px keeps switching one-way — once we’re in burger we + // only flip back when there’s real headroom. + return needed + 120 <= available; }); }; @@ -813,20 +817,27 @@ export function PublicLayout() {
- {showInlineNav ? null : ( - - )} + {/* Burger toggle is always in the DOM so the right-side width + doesn’t change between inline and burger modes. The visible + / invisible toggle controls click-ability without resizing + the row, which would otherwise feed back into the fit + measurement and cause oscillation at borderline widths. */} +