From 6d9a2fa9f66d691e9f1754e94f25a1d2b0825672 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 19 Aug 2026 19:38:29 +0200 Subject: [PATCH] swarm-ui: fix underline not showing on the initial page load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause (found via a zoomed pixel-level screenshot check, not guessed): the indicator span always exists in the DOM starting from left:0/width:0/transparent, and the very first real position landed via a CSS-transitioned change from that fallback rather than a snap — so the underline visibly grew in from nothing over 140ms instead of being there immediately, reading as entirely absent on a fast/slow first paint alike depending on timing. Every subsequent navigation was unaffected (always transitioning between two already-visible states). Fixed with a one-render-only transition suppression (indicatorSettledOnce, gated one tick behind the indicator's first non-null commit) so the first placement snaps instead of animating in, while every later hop still animates normally. Verified against the exact repro: a fresh page load at the same short virtual-time-budget that previously showed no underline now shows it immediately. --- .../packages/swarm-ui/src/shell/Shell.css | 10 ++++++++++ .../packages/swarm-ui/src/shell/Shell.tsx | 20 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/packages/swarm-ui/src/shell/Shell.css b/frontend/packages/swarm-ui/src/shell/Shell.css index 6eafd5c1..77e59c13 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.css +++ b/frontend/packages/swarm-ui/src/shell/Shell.css @@ -128,6 +128,16 @@ width 140ms ease, background-color 140ms ease; } +/* Suppresses the transition for exactly one render — the indicator's + very first real position, so it snaps into place instead of visibly + growing from the unmounted `left:0/width:0/transparent` fallback (see + Shell.tsx's `indicatorSettledOnce`). Two-class selector so it wins + over the plain `.shell-nav-indicator` rule above without `!important`, + same reasoning `:root[data-motion='reduce']`'s extra attribute + selector already relies on. */ +.shell-nav-indicator.shell-nav-indicator-instant { + transition: none; +} .shell-body { max-width: 60em; margin: 0 auto; diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index 6117eae3..eb6ac85a 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -114,6 +114,14 @@ export function Shell({ children }: { children: ComponentChildren }) { // agree once every intermediate hop has landed. `-1` = not settled // anywhere yet (first mount) or on a route with no nav item (404). const settledIndexRef = useRef(-1); + // False until the indicator's very first real position has committed — + // gates `.shell-nav-indicator-instant` (see Shell.css) so that first + // placement snaps instead of *transitioning in* from the unmounted + // `left:0/width:0/transparent` fallback, which read as "no underline at + // all" on a fresh page load (reported live by mara, reproduced here with + // a zoomed pixel-level screenshot check — real, not a rendering fluke). + // Every subsequent change (a real navigation) still animates normally. + const [indicatorSettledOnce, setIndicatorSettledOnce] = useState(false); // Applied once here, not inside `SettingsMenu` — every route mounts // through this one ``, so the override takes effect regardless @@ -185,6 +193,16 @@ export function Shell({ children }: { children: ComponentChildren }) { }; }, [location]); + // Fires the render *after* the indicator's first non-null commit — + // deliberately one tick behind, not folded into the effect above, + // because the instant/animated class has to still read "instant" on + // the very render where `indicator` itself first goes non-null (that's + // the snap this exists for); only from the render after that should + // transitions be armed. + useEffect(() => { + if (indicator && !indicatorSettledOnce) setIndicatorSettledOnce(true); + }, [indicator, indicatorSettledOnce]); + // Re-measures the current (already-settled) position on resize — the // nav's own `flex-wrap` means a link's position genuinely changes at // narrow widths, not just its route. Not part of the hop effect above @@ -249,7 +267,7 @@ export function Shell({ children }: { children: ComponentChildren }) { nav item (404), so it doesn't pop in with a stale position the next time a real nav item becomes active. */}