diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index ddde452e7..b7f7a400f 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -9,6 +9,10 @@ import { i18n, SUPPORTED_LOCALES, t } from './lib/stores/i18n.svelte.js'; import { safeLoginReturnPath } from './lib/utils/loginReturnPath.js'; import { getStartupCopy } from './lib/utils/startupCopy.js'; + import { + loadAuthenticatedShellUI, + resetAuthenticatedShellUILoad, + } from './lib/services/authenticatedShellUI.js'; import BrandedLoader from './lib/components/BrandedLoader.svelte'; import LazyRootDialog from './lib/components/LazyRootDialog.svelte'; import LazyRootView from './lib/components/LazyRootView.svelte'; @@ -38,6 +42,10 @@ let startupError = $state(''); let startupSlow = $state(false); let i18nReady = $state(false); + let authenticatedShellUIReady = $state(false); + let authenticatedShellUILoading = $state(false); + let authenticatedShellUIAudience = $state(null); + let authenticatedShellUILoadGeneration = 0; let startupAttempt = 0; let themeAudience = null; let themeLoadGeneration = 0; @@ -122,6 +130,28 @@ } } + async function prepareAuthenticatedShellUI(userId) { + const audience = `user:${userId ?? 'authenticated'}`; + if ( + authenticatedShellUIAudience === audience && + (authenticatedShellUIReady || authenticatedShellUILoading) + ) { + return; + } + + const generation = ++authenticatedShellUILoadGeneration; + authenticatedShellUIAudience = audience; + authenticatedShellUIReady = false; + authenticatedShellUILoading = true; + await loadAuthenticatedShellUI(userId); + + if (generation !== authenticatedShellUILoadGeneration) return; + if (!$authStore.isAuthenticated || authStore.currentUser?.id !== userId) return; + + authenticatedShellUILoading = false; + authenticatedShellUIReady = true; + } + // Show login dialog when setup is completed but user is not authenticated and app is initialized // But NOT for portal routes (they are public) const shouldShowLoginDialog = $derived( @@ -146,6 +176,17 @@ if ($authStore.isAuthenticated && setupCompleted) { showLoginDialog = false; appInitialized = true; + void prepareAuthenticatedShellUI(authStore.currentUser?.id); + } else if ( + authenticatedShellUIAudience !== null || + authenticatedShellUIReady || + authenticatedShellUILoading + ) { + authenticatedShellUILoadGeneration += 1; + authenticatedShellUIAudience = null; + authenticatedShellUIReady = false; + authenticatedShellUILoading = false; + resetAuthenticatedShellUILoad(); } }); @@ -355,6 +396,12 @@ {:else if $authStore.isAuthenticated && appInitialized && isMobileRoute($currentRoute.view)} + + {:else if $authStore.isAuthenticated && appInitialized && !authenticatedShellUIReady} + {:else if $authStore.isAuthenticated && appInitialized} diff --git a/frontend/src/lib/components/UserAvatar.svelte b/frontend/src/lib/components/UserAvatar.svelte index 67b7cf026..cfdde79eb 100644 --- a/frontend/src/lib/components/UserAvatar.svelte +++ b/frontend/src/lib/components/UserAvatar.svelte @@ -14,7 +14,9 @@ // minimal drops the items that navigate to the desktop app (My Workspace, // Profile, Security) — used on the mobile surface where those routes render // the full desktop UI. Theme + Sign Out remain. - minimal = false + minimal = false, + isOpen = $bindable(false), + onOpenChange = null, } = $props(); // Local state @@ -23,13 +25,6 @@ // Subscribe to personal workspace from store const personalWorkspace = $derived($workspacesStore.personalWorkspace); - // Generate user initials - const userInitials = $derived( - authStore.currentUser - ? (authStore.currentUser.first_name?.[0]?.toUpperCase() || '') + (authStore.currentUser.last_name?.[0]?.toUpperCase() || '') - : '' - ); - // Only show avatar if attachments are enabled and user has an avatar const showAvatar = $derived(attachmentStatus.enabled && authStore.currentUser?.avatar_url); @@ -109,21 +104,20 @@
-
+{/snippet} + - + diff --git a/frontend/src/lib/pages/Admin.svelte b/frontend/src/lib/pages/Admin.svelte index cd02e5382..65ab7289b 100644 --- a/frontend/src/lib/pages/Admin.svelte +++ b/frontend/src/lib/pages/Admin.svelte @@ -46,6 +46,7 @@ import PluginModalContainer from '../layout/PluginModalContainer.svelte'; import LinkComponent from '../components/Link.svelte'; import SidebarHeader from '../layout/SidebarHeader.svelte'; + import ScrollableSidebar from '../layout/ScrollableSidebar.svelte'; import { IconFileText, IconMenu2, IconPuzzle, IconSearch, IconX } from '@tabler/icons-svelte-runes'; import { useEventListener } from 'runed'; import PermissionGuard from '../layout/PermissionGuard.svelte'; @@ -298,8 +299,58 @@ } +{#snippet adminSidebarHeader()} +
+
+
+ +
+ +
+ + +
+ +
+
+
+
+{/snippet} + -
+
@@ -313,63 +364,19 @@ onclick={() => closeAdminNavigation(true)} > {/if} - + {/if} @@ -623,7 +629,7 @@ {/if} - {#each allAdminItems.filter(item => item.isPlugin) as pluginItem} + {#each allAdminItems.filter(item => item.isPlugin) as pluginItem (pluginItem.id)} {#if activeTab === pluginItem.id} {@const pluginName = pluginItem.pluginData?.pluginName || 'unknown'} {@const iframeSrc = `/api/plugins/${pluginName}/assets/${pluginItem.pluginData?.component || 'index.html'}`} @@ -659,7 +665,7 @@ } @media (max-width: 1100px) { - .admin-sidebar { + :global(.admin-sidebar) { position: absolute; z-index: 30; inset: 0 auto 0 0; @@ -673,7 +679,7 @@ box-shadow: var(--ds-shadow-overlay, 0 8px 24px rgb(9 30 66 / 25%)); } - .admin-sidebar.admin-navigation-open { + :global(.admin-sidebar.admin-navigation-open) { visibility: visible; transform: translateX(0); } @@ -761,7 +767,7 @@ } @media (prefers-reduced-motion: reduce) { - .admin-sidebar { + :global(.admin-sidebar) { transition: none; } } diff --git a/frontend/src/lib/pages/ApiDocs.svelte b/frontend/src/lib/pages/ApiDocs.svelte index 28202f2ea..7137580f5 100644 --- a/frontend/src/lib/pages/ApiDocs.svelte +++ b/frontend/src/lib/pages/ApiDocs.svelte @@ -71,7 +71,7 @@