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}