Private
Public Access
1
0

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:
Catherine Renelle
2026-02-27 14:47:13 -05:00
parent ecd037ce5c
commit b1d994c9e4
+90 -77
View File
@@ -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'} <NotificationBell />
<a <!-- Profile dropdown -->
href="/admin/site" <div class="relative profile-menu">
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition" <button
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"
aria-label="User menu"
> >
Site Admin <Avatar name={data.user.name} avatarUrl={data.user.avatarUrl} size="xs" class="border border-white/30" />
<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> </a>
{/if}
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'} {#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
<a <a
href="/admin" href="/admin"
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition" 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 Admin
</a> </a>
{/if} {/if}
<a href="/account" class="flex items-center gap-1.5 text-white/80 text-sm hover:text-white transition"> {#if data.user.globalRole === 'SITE_ADMIN'}
<Avatar name={data.user.name} avatarUrl={data.user.avatarUrl} size="xs" class="border border-white/30" /> <a
{data.user.name} 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> </a>
<NotificationBell /> {/if}
<div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
<button <button
onclick={async () => { onclick={async () => {
profileMenuOpen = false;
await fetch('/api/auth/logout', { method: 'POST' }); await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/'; window.location.href = '/';
}} }}
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition" 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 Logout
</button> </button>
</div>
{/if}
</div>
{:else} {:else}
<a <a
href="/auth/login" href="/auth/login"
@@ -117,12 +165,6 @@
> >
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>
@@ -160,7 +202,6 @@
</button> </button>
{#if data.user} {#if data.user}
<NotificationBell /> <NotificationBell />
{/if}
<button <button
onclick={() => (mobileMenuOpen = !mobileMenuOpen)} onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
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"
@@ -174,54 +215,44 @@
{/if} {/if}
</svg> </svg>
</button> </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}
</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="/account" class="flex items-center gap-1.5 text-white/80 text-sm py-1 hover:text-white transition" onclick={() => (mobileMenuOpen = false)}> <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)}>
<Avatar name={data.user.name} avatarUrl={data.user.avatarUrl} size="xs" class="border border-white/30" /> My Work
{data.user.name}
</a> </a>
{#if data.user.globalRole === 'SITE_ADMIN'} <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)}>
<a Boards
href="/admin/site" </a>
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center" <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)}>
onclick={() => (mobileMenuOpen = false)} Profile
>
Site Admin
</a> </a>
{/if}
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'} {#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
<a <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)}>
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 Admin
</a> </a>
{/if} {/if}
<a {#if data.user.globalRole === 'SITE_ADMIN'}
href="/my-work" <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)}
>
My Work
</a>
<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>
{/if}
<button <button
onclick={async () => { onclick={async () => {
await fetch('/api/auth/logout', { method: 'POST' }); await fetch('/api/auth/logout', { method: 'POST' });
@@ -232,24 +263,6 @@
Logout Logout
</button> </button>
</div> </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>