Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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();
}
});

Expand Down Expand Up @@ -355,6 +396,12 @@
<!-- Mobile PWA surface (phone-focused shell, bypasses desktop MainApp chrome) -->
{:else if $authStore.isAuthenticated && appInitialized && isMobileRoute($currentRoute.view)}
<LazyRootView loader={ROOT_VIEW_LOADERS.mobile} label="mobile workspace" />
<!-- Mount desktop navigation only after its permission/capability snapshot is stable. -->
{:else if $authStore.isAuthenticated && appInitialized && !authenticatedShellUIReady}
<BrandedLoader
label={getStartupCopy('common.loading', i18nReady, t)}
fullViewport={false}
/>
<!-- Show main app when user is authenticated -->
{:else if $authStore.isAuthenticated && appInitialized}
<LazyRootView loader={ROOT_VIEW_LOADERS.desktop} label="workspace" />
Expand Down
30 changes: 12 additions & 18 deletions frontend/src/lib/components/UserAvatar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -109,21 +104,20 @@
<div data-testid="user-avatar-menu" onmouseenter={loadPersonalWorkspaceIfNeeded} onfocusin={loadPersonalWorkspaceIfNeeded}>
<DropdownMenu
triggerAvatar={showAvatar ? authStore.currentUser?.avatar_url : null}
triggerText={expanded && label ? label : (showAvatar ? '' : userInitials)}
triggerText={expanded && label ? label : ''}
triggerLabel={label || t('nav.profile')}
triggerIcon={expanded && !showAvatar ? User : null}
triggerIcon={!showAvatar ? User : null}
triggerIconClass="w-5 h-5"
triggerClass={expanded
? "w-full px-3 h-10 rounded flex items-center cursor-pointer nav-button"
: (showAvatar
? "w-8 h-8 rounded-full cursor-pointer hover:opacity-80 transition-opacity overflow-hidden"
: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer nav-button text-xs font-bold select-none"
)
triggerClass={showAvatar
? "w-full pl-1.5 pr-3 h-10 rounded flex items-center cursor-pointer nav-button overflow-hidden"
: "w-full px-3 h-10 rounded flex items-center cursor-pointer nav-button"
}
triggerGap={expanded ? "gap-3" : ""}
triggerAlignment={expanded ? "start" : "center"}
triggerGap={showAvatar || expanded ? "gap-3" : ""}
triggerAlignment={showAvatar || expanded ? "start" : "center"}
showChevron={false}
triggerTestid="user-avatar-trigger"
{isOpen}
{onOpenChange}
items={[
...((!minimal && authStore.currentUser) ? [{
id: 'my-workspace',
Expand Down
21 changes: 13 additions & 8 deletions frontend/src/lib/features/api-docs/ApiDocsSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import MethodBadge from './MethodBadge.svelte';
import Input from '../../components/Input.svelte';
import { filterGroups } from './openapi-store.svelte.js';
import ScrollableSidebar from '../../layout/ScrollableSidebar.svelte';

let {
groups,
Expand All @@ -21,7 +22,7 @@
}
</script>

<aside class="sidebar" data-testid="api-docs-sidebar">
{#snippet sidebarHeader()}
<header class="sidebar-head">
<h1 class="sidebar-title">API reference</h1>
<p class="sidebar-meta">{groups.reduce((n, g) => n + g.operations.length, 0)} operations</p>
Expand All @@ -41,7 +42,16 @@
<span class="filter-count">{visibleCount}</span>
{/if}
</div>
{/snippet}

<ScrollableSidebar
as="aside"
class="sidebar"
data-testid="api-docs-sidebar"
aria-label="API reference"
header={sidebarHeader}
scrollTestid="api-docs-navigation-scroll"
>
<nav class="groups">
{#each visibleGroups as group (group.tag)}
<section class="group">
Expand Down Expand Up @@ -70,18 +80,15 @@
<p class="empty">No operations match “{query}”.</p>
{/if}
</nav>
</aside>
</ScrollableSidebar>

<style>
.sidebar {
:global(.sidebar) {
width: 280px;
flex-shrink: 0;
border-right: 1px solid var(--ds-border);
background: var(--ds-surface);
overflow-y: auto;
height: 100%;
display: flex;
flex-direction: column;
}
.sidebar-head {
padding: 18px 18px 8px;
Expand Down Expand Up @@ -127,8 +134,6 @@
}
.groups {
padding: 4px 0 24px;
flex: 1 1 auto;
overflow-y: auto;
}
.group {
margin-top: 14px;
Expand Down
37 changes: 18 additions & 19 deletions frontend/src/lib/features/channels/ChannelNavigation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import Button from '../../components/Button.svelte';
import { getHexFromColorName } from '../../utils/colors.js';
import { t } from '../../stores/i18n.svelte.js';
import SidebarHeader from '../../layout/SidebarHeader.svelte';
import NavigationSidebar from '../../layout/NavigationSidebar.svelte';
import { channelTypes as channelTypeDefs, allTypesEntry } from './channelTypes.js';
import { isSystemAdmin } from '../../stores/permissions.svelte.js';

Expand Down Expand Up @@ -51,11 +51,23 @@
}
</script>

<!-- Channel Navigation Sidebar -->
<div class="w-64 border-r flex flex-col p-6" style="border-color: var(--ds-border); background-color: var(--ds-surface-raised);">
<!-- Header -->
<SidebarHeader title={t('channels.title')} description={t('channels.subtitle')} noBorder />
{#snippet sidebarFooter()}
{#if $isSystemAdmin}
<div class="px-6 pb-6 pt-4 border-t" style="border-color: var(--ds-border);">
<Button
variant="default"
icon={IconTag}
onclick={handleManageCategories}
class="w-full justify-center"
>
{t('channels.manageCategories')}
</Button>
</div>
{/if}
{/snippet}

<!-- Channel Navigation Sidebar -->
<NavigationSidebar title={t('channels.title')} description={t('channels.subtitle')} footer={sidebarFooter}>
<!-- Navigation -->
<nav class="flex-1 space-y-4">
<!-- Channel Types Section -->
Expand Down Expand Up @@ -118,17 +130,4 @@
</div>
</nav>

<!-- Footer - Manage Categories -->
{#if $isSystemAdmin}
<div class="pt-4 border-t" style="border-color: var(--ds-border);">
<Button
variant="default"
icon={IconTag}
onclick={handleManageCategories}
class="w-full justify-center"
>
{t('channels.manageCategories')}
</Button>
</div>
{/if}
</div>
</NavigationSidebar>
6 changes: 3 additions & 3 deletions frontend/src/lib/features/channels/Channels.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -434,14 +434,14 @@
</script>

<!-- Main container with sidebar layout -->
<div class="flex min-h-screen" style="background-color: var(--ds-surface);">
<div class="flex h-full min-h-0 overflow-hidden" style="background-color: var(--ds-surface);">
<!-- Left Sidebar - Category Navigation (only when not embedded in Admin) -->
{#if !embedded}
<ChannelNavigation />
{/if}

<!-- Main Content -->
<div class="flex-1 {embedded ? '' : 'p-6'}">
<div class="flex-1 min-h-0 overflow-y-auto {embedded ? '' : 'p-6'}">
<!-- Embedded Tab Navigation -->
{#if embedded}
<div class="border-b mb-6" style="border-color: var(--ds-border);">
Expand Down Expand Up @@ -651,7 +651,7 @@
<div>
<Label color="default" class="mb-2">Type</Label>
<div class="flex flex-wrap gap-2">
{#each channelTypeDefs as option}
{#each channelTypeDefs as option (option.id)}
<button
type="button"
onclick={() => channelFormData.type = option.id}
Expand Down
Loading
Loading