From 7b1cd50cb9305e95a26c7f8a3d64a91ef3cba565 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 1 May 2026 22:51:05 -0400 Subject: [PATCH] fix(web): user-menu links navigate client-side, not via full reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The username dropdown's container had onclick={(e) => e.stopPropagation()} to keep the window-level close-on-outside-click handler from firing on inside clicks. But that also blocked SvelteKit's document-level link-click interceptor, so clicking Settings/Admin inside the menu fell through to the browser's native navigation — a full-page reload. Symptom: clicking Admin redirected to /. On the hard reload, the admin guard (/admin/+layout.ts) ran before bootstrap() had settled the user store; user.value was still null, so the guard threw redirect(302, '/'). By the time you saw / render, bootstrap had finished and the dropdown showed Admin again, primed to repeat the cycle. Replaces the stopPropagation-based outside-click protection with a target-containment check in the window handler: close only when the click landed outside both the menu button and the menu container. Drops the redundant stopPropagation on the menu button too (no longer needed since the window handler now ignores in-menu clicks). Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/lib/components/Shell.svelte | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/Shell.svelte b/web/src/lib/components/Shell.svelte index e32f8530..6d505911 100644 --- a/web/src/lib/components/Shell.svelte +++ b/web/src/lib/components/Shell.svelte @@ -9,6 +9,24 @@ let { children } = $props<{ children: import('svelte').Snippet }>(); let menuOpen = $state(false); + let menuRef: HTMLElement | undefined = $state(); + let menuButtonRef: HTMLElement | undefined = $state(); + + // Close the dropdown when the click was outside both the button and the menu. + // The previous implementation used onclick={(e) => e.stopPropagation()} on the + // menu container, which kept the window-level close-on-outside-click handler + // from firing — but it also blocked SvelteKit's document-level link-click + // interceptor, so clicking Admin/Settings inside the dropdown fell through + // to the browser's native navigation (a full-page reload). On a hard reload, + // /admin/+layout.ts's load function ran before bootstrap() settled the user + // store, so the admin guard saw user.value === null and redirected to /. + function handleWindowClick(e: MouseEvent) { + if (!menuOpen) return; + const target = e.target as Node | null; + if (!target) return; + if (menuRef?.contains(target) || menuButtonRef?.contains(target)) return; + menuOpen = false; + } async function handleLogout() { menuOpen = false; @@ -34,7 +52,7 @@ ]; - (menuOpen = false)} onkeydown={(e) => e.key === 'Escape' && (menuOpen = false)} /> + e.key === 'Escape' && (menuOpen = false)} />
@@ -44,17 +62,17 @@
{#if menuOpen}
e.stopPropagation()} - onkeydown={(e) => e.stopPropagation()} role="menu" tabindex="-1" >