Condense nav: profile dropdown replaces inline nav items
Desktop nav now shows only: theme toggle, notification bell, and avatar+name. Clicking avatar opens a dropdown menu with My Work, Profile, Admin, Site Admin, and Logout. Logged-out users see only the theme toggle and a Login button. Mobile nav mirrors the same menu items in the hamburger dropdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+139
-126
@@ -13,6 +13,7 @@
|
|||||||
let { children, data }: { children: Snippet; data: { user: any; tenant: any; theme?: string } } = $props();
|
let { children, data }: { children: Snippet; data: { user: any; tenant: any; theme?: string } } = $props();
|
||||||
|
|
||||||
let mobileMenuOpen = $state(false);
|
let mobileMenuOpen = $state(false);
|
||||||
|
let profileMenuOpen = $state(false);
|
||||||
const theme = $derived(getTheme());
|
const theme = $derived(getTheme());
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -22,8 +23,16 @@
|
|||||||
return () => disconnectSocket();
|
return () => disconnectSocket();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleProfileClickOutside(e: MouseEvent) {
|
||||||
|
if (profileMenuOpen && !(e.target as HTMLElement).closest('.profile-menu')) {
|
||||||
|
profileMenuOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onclick={handleProfileClickOutside} />
|
||||||
|
|
||||||
<div class="min-h-screen layout-page-bg">
|
<div class="min-h-screen layout-page-bg">
|
||||||
<nav class="shadow-sm layout-nav-bg">
|
<nav class="shadow-sm layout-nav-bg">
|
||||||
<div class="px-4 sm:px-6 lg:px-8">
|
<div class="px-4 sm:px-6 lg:px-8">
|
||||||
@@ -54,15 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Nav items -->
|
<!-- Right: Nav items -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-2">
|
||||||
{#if data.user}
|
|
||||||
<a
|
|
||||||
href="/my-work"
|
|
||||||
class="rounded px-2 py-1 text-sm text-white/80 hover:text-white hover:bg-white/20 transition"
|
|
||||||
>
|
|
||||||
My Work
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
<button
|
<button
|
||||||
onclick={toggleTheme}
|
onclick={toggleTheme}
|
||||||
class="rounded p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition"
|
class="rounded p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition"
|
||||||
@@ -80,36 +81,83 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
{#if data.user.globalRole === 'SITE_ADMIN'}
|
|
||||||
<a
|
|
||||||
href="/admin/site"
|
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition"
|
|
||||||
>
|
|
||||||
Site Admin
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
|
|
||||||
<a
|
|
||||||
href="/admin"
|
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition"
|
|
||||||
>
|
|
||||||
Admin
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
<a href="/account" class="flex items-center gap-1.5 text-white/80 text-sm hover:text-white transition">
|
|
||||||
<Avatar name={data.user.name} avatarUrl={data.user.avatarUrl} size="xs" class="border border-white/30" />
|
|
||||||
{data.user.name}
|
|
||||||
</a>
|
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
<button
|
<!-- Profile dropdown -->
|
||||||
onclick={async () => {
|
<div class="relative profile-menu">
|
||||||
await fetch('/api/auth/logout', { method: 'POST' });
|
<button
|
||||||
window.location.href = '/';
|
onclick={(e) => { e.stopPropagation(); profileMenuOpen = !profileMenuOpen; }}
|
||||||
}}
|
class="flex items-center gap-1.5 rounded px-2 py-1 text-white/80 hover:text-white hover:bg-white/20 transition"
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition"
|
aria-label="User menu"
|
||||||
>
|
>
|
||||||
Logout
|
<Avatar name={data.user.name} avatarUrl={data.user.avatarUrl} size="xs" class="border border-white/30" />
|
||||||
</button>
|
<span class="text-sm">{data.user.name}</span>
|
||||||
|
<svg class="w-3.5 h-3.5 transition-transform {profileMenuOpen ? 'rotate-180' : ''}" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{#if profileMenuOpen}
|
||||||
|
<div class="absolute right-0 top-full mt-1 z-50 w-48 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-xl py-1">
|
||||||
|
<a
|
||||||
|
href="/my-work"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||||
|
onclick={() => (profileMenuOpen = false)}
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
My Work
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/account"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||||
|
onclick={() => (profileMenuOpen = false)}
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
|
||||||
|
<a
|
||||||
|
href="/admin"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||||
|
onclick={() => (profileMenuOpen = false)}
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Admin
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{#if data.user.globalRole === 'SITE_ADMIN'}
|
||||||
|
<a
|
||||||
|
href="/admin/site"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||||
|
onclick={() => (profileMenuOpen = false)}
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3.293 1.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L7.586 10 5.293 7.707a1 1 0 010-1.414zM11 12a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Site Admin
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
|
||||||
|
<button
|
||||||
|
onclick={async () => {
|
||||||
|
profileMenuOpen = false;
|
||||||
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
window.location.href = '/';
|
||||||
|
}}
|
||||||
|
class="flex items-center gap-2 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href="/auth/login"
|
href="/auth/login"
|
||||||
@@ -117,17 +165,11 @@
|
|||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
href="/auth/register"
|
|
||||||
class="rounded bg-white px-3 py-1.5 text-sm text-[var(--color-primary)] font-medium hover:bg-white/90 transition"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile header -->
|
<!-- Mobile header -->
|
||||||
<div class="flex h-14 items-center justify-between sm:hidden">
|
<div class="flex h-14 items-center justify-between sm:hidden">
|
||||||
<a href="/" class="flex items-center gap-2 text-white font-bold text-lg">
|
<a href="/" class="flex items-center gap-2 text-white font-bold text-lg">
|
||||||
{#if data.tenant?.logoUrl}
|
{#if data.tenant?.logoUrl}
|
||||||
@@ -160,96 +202,67 @@
|
|||||||
</button>
|
</button>
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
|
<button
|
||||||
|
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
||||||
|
class="rounded p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition"
|
||||||
|
aria-label="Open menu"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
{#if mobileMenuOpen}
|
||||||
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
{:else}
|
||||||
|
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd" />
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
href="/auth/login"
|
||||||
|
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
|
||||||
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
|
||||||
class="rounded p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition"
|
|
||||||
aria-label="Open menu"
|
|
||||||
>
|
|
||||||
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
{#if mobileMenuOpen}
|
|
||||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
||||||
{:else}
|
|
||||||
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd" />
|
|
||||||
{/if}
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile dropdown -->
|
<!-- Mobile dropdown -->
|
||||||
{#if mobileMenuOpen}
|
{#if mobileMenuOpen && data.user}
|
||||||
<div class="sm:hidden border-t border-white/20 px-4 py-3 space-y-2">
|
<div class="sm:hidden border-t border-white/20 px-4 py-3 space-y-2">
|
||||||
{#if data.user}
|
<div class="w-full">
|
||||||
<div class="w-full">
|
<SearchBar />
|
||||||
<SearchBar />
|
</div>
|
||||||
</div>
|
<div class="flex flex-col gap-1">
|
||||||
<div class="flex flex-col gap-1">
|
<a href="/my-work" class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center" onclick={() => (mobileMenuOpen = false)}>
|
||||||
<a href="/account" class="flex items-center gap-1.5 text-white/80 text-sm py-1 hover:text-white transition" onclick={() => (mobileMenuOpen = false)}>
|
My Work
|
||||||
<Avatar name={data.user.name} avatarUrl={data.user.avatarUrl} size="xs" class="border border-white/30" />
|
</a>
|
||||||
{data.user.name}
|
<a href="/boards" class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center" onclick={() => (mobileMenuOpen = false)}>
|
||||||
|
Boards
|
||||||
|
</a>
|
||||||
|
<a href="/account" class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center" onclick={() => (mobileMenuOpen = false)}>
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
|
||||||
|
<a href="/admin" class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center" onclick={() => (mobileMenuOpen = false)}>
|
||||||
|
Admin
|
||||||
</a>
|
</a>
|
||||||
{#if data.user.globalRole === 'SITE_ADMIN'}
|
{/if}
|
||||||
<a
|
{#if data.user.globalRole === 'SITE_ADMIN'}
|
||||||
href="/admin/site"
|
<a href="/admin/site" class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center" onclick={() => (mobileMenuOpen = false)}>
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center"
|
Site Admin
|
||||||
onclick={() => (mobileMenuOpen = false)}
|
|
||||||
>
|
|
||||||
Site Admin
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
|
|
||||||
<a
|
|
||||||
href="/admin"
|
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center"
|
|
||||||
onclick={() => (mobileMenuOpen = false)}
|
|
||||||
>
|
|
||||||
Admin
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
<a
|
|
||||||
href="/my-work"
|
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center"
|
|
||||||
onclick={() => (mobileMenuOpen = false)}
|
|
||||||
>
|
|
||||||
My Work
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
{/if}
|
||||||
href="/boards"
|
<button
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center"
|
onclick={async () => {
|
||||||
onclick={() => (mobileMenuOpen = false)}
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
>
|
window.location.href = '/';
|
||||||
Boards
|
}}
|
||||||
</a>
|
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition"
|
||||||
<button
|
>
|
||||||
onclick={async () => {
|
Logout
|
||||||
await fetch('/api/auth/logout', { method: 'POST' });
|
</button>
|
||||||
window.location.href = '/';
|
</div>
|
||||||
}}
|
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<a
|
|
||||||
href="/auth/login"
|
|
||||||
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center"
|
|
||||||
onclick={() => (mobileMenuOpen = false)}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/auth/register"
|
|
||||||
class="rounded bg-white px-3 py-1.5 text-sm text-[var(--color-primary)] font-medium hover:bg-white/90 transition text-center"
|
|
||||||
onclick={() => (mobileMenuOpen = false)}
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Reference in New Issue
Block a user