diff --git a/frontend/src/app.html b/frontend/src/app.html index 0e7226f..bb4ed6d 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -5,6 +5,23 @@ Notify Bridge + %sveltekit.head% diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 88c162d..fd540b1 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -38,6 +38,11 @@ let providerFilterValue = $state(globalProviderFilter.id ?? 0); let _syncingFilter = false; + // Reserve the provider-filter row from first paint until the cache resolves. + // Without this, the row appears mid-paint and pushes nav items down on every + // hard reload — the most visible "jump" the user reported. + let showProviderFilter = $derived(allProviders.length >= 1 || providersCache.fetchedAt === 0); + // Sync filter value → store $effect(() => { const v = providerFilterValue; @@ -78,7 +83,24 @@ } catch (err: any) { pwdMsg = err.message; pwdSuccess = false; snackError(err.message); } } - let collapsed = $state(false); + // Read persisted UI state synchronously so first paint already matches the + // user's last session — otherwise the sidebar visibly snaps from expanded + // to collapsed (and groups slide open) right after mount. + function readPersistedCollapsed(): boolean { + if (typeof localStorage === 'undefined') return false; + return localStorage.getItem('sidebar_collapsed') === 'true'; + } + function readPersistedExpandedGroups(): Record { + if (typeof localStorage === 'undefined') return {}; + try { + const saved = localStorage.getItem('nav_expanded'); + return saved ? JSON.parse(saved) : {}; + } catch { + return {}; + } + } + + let collapsed = $state(readPersistedCollapsed()); let isMac = $derived(typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent)); // Nav counts — computed reactively from caches + global provider filter @@ -216,7 +238,7 @@ }; // Track which groups are expanded (persisted in localStorage) - let expandedGroups = $state>({}); + let expandedGroups = $state>(readPersistedExpandedGroups()); function toggleGroup(key: string) { expandedGroups = { ...expandedGroups, [key]: !expandedGroups[key] }; @@ -262,13 +284,8 @@ onMount(async () => { initTheme(); - if (typeof localStorage !== 'undefined') { - collapsed = localStorage.getItem('sidebar_collapsed') === 'true'; - try { - const saved = localStorage.getItem('nav_expanded'); - if (saved) expandedGroups = JSON.parse(saved); - } catch (e) { console.warn('Failed to parse nav_expanded:', e); } - } + // `collapsed` and `expandedGroups` are now hydrated synchronously in + // their $state initializers above to avoid a post-mount layout snap. await loadUser(); if (!auth.user && !isAuthPage) { redirecting = true; @@ -384,7 +401,7 @@ {/if} Notify Bridge -

v0.5.2

+

v{__APP_VERSION__}

{:else} @@ -398,8 +415,10 @@ - - {#if allProviders.length >= 1} + + {#if showProviderFilter}
{#if collapsed}