Move account control into header and simplify avatar
This commit is contained in:
+53
-23
@@ -33,6 +33,12 @@
|
|||||||
const unread = $derived($notificationHistory.filter(item => !item.read).length);
|
const unread = $derived($notificationHistory.filter(item => !item.read).length);
|
||||||
let menuButton = $state<HTMLButtonElement>();
|
let menuButton = $state<HTMLButtonElement>();
|
||||||
let accountButton = $state<HTMLButtonElement>();
|
let accountButton = $state<HTMLButtonElement>();
|
||||||
|
const accountName = $derived(data.user?.displayName || data.user?.username || 'Account');
|
||||||
|
const accountInitial = $derived(Array.from(accountName.trim())[0]?.toLocaleUpperCase() || '?');
|
||||||
|
|
||||||
|
function dismissAccount(event: MouseEvent) {
|
||||||
|
if (event.target instanceof Element && !event.target.closest('.account-menu')) accountOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
function openUtility(panel: 'notifications' | 'help') {
|
function openUtility(panel: 'notifications' | 'help') {
|
||||||
if (menuOpen) { menuButton?.focus(); menuOpen = false; }
|
if (menuOpen) { menuButton?.focus(); menuOpen = false; }
|
||||||
@@ -157,7 +163,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onkeydown={closeMenu} />
|
<svelte:window onkeydown={closeMenu} onclick={dismissAccount} />
|
||||||
|
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<a class="skip-link" href="#main-content">Skip to content</a>
|
<a class="skip-link" href="#main-content">Skip to content</a>
|
||||||
@@ -168,12 +174,34 @@
|
|||||||
<a class="brand-link" href={sessionReady ? '/' : '/account'} aria-label="Electric Cherry home">
|
<a class="brand-link" href={sessionReady ? '/' : '/account'} aria-label="Electric Cherry home">
|
||||||
<BrandMark size="sm" />
|
<BrandMark size="sm" />
|
||||||
</a>
|
</a>
|
||||||
{#if sessionReady}<button class="menu-toggle" bind:this={menuButton} type="button"
|
<div class="header-actions">
|
||||||
aria-expanded={menuOpen} aria-controls="primary-links"
|
{#if sessionReady}<button class="menu-toggle" bind:this={menuButton} type="button"
|
||||||
onclick={() => { menuOpen = !menuOpen; }}>
|
aria-label={menuOpen ? 'Close menu' : 'Menu'} aria-expanded={menuOpen} aria-controls="primary-links"
|
||||||
{menuOpen ? 'Close' : 'Menu'}
|
onclick={() => { menuOpen = !menuOpen; accountOpen = false; }}>
|
||||||
<span aria-hidden="true">{menuOpen ? '×' : '☰'}</span>
|
<span class="menu-label">{menuOpen ? 'Close' : 'Menu'}</span>
|
||||||
</button>
|
<span aria-hidden="true">{menuOpen ? '×' : '☰'}</span>
|
||||||
|
</button>{/if}
|
||||||
|
{#if data.user}
|
||||||
|
<div class="account-menu">
|
||||||
|
<button bind:this={accountButton} class="account-button" type="button"
|
||||||
|
aria-label={`Account: ${accountName}`} title={accountName}
|
||||||
|
aria-expanded={accountOpen} aria-controls="account-links"
|
||||||
|
onclick={() => { accountOpen = !accountOpen; menuOpen = false; }}>
|
||||||
|
<span aria-hidden="true">{accountInitial}</span>
|
||||||
|
</button>
|
||||||
|
{#if accountOpen}
|
||||||
|
<div class="account-links" id="account-links">
|
||||||
|
<div class="account-name">{accountName}</div>
|
||||||
|
<a href="/account">My account</a>
|
||||||
|
{#if data.user.role === 'admin' && sessionReady}<a href="/settings/users">Manage users</a>{/if}
|
||||||
|
<button disabled={signingOut} onclick={signOut}>{signingOut ? 'Signing out…' : 'Sign out'}</button>
|
||||||
|
{#if signOutError}<p role="alert">{signOutError}</p>{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if sessionReady}
|
||||||
<div class="tabs" id="primary-links" class:expanded={menuOpen}>
|
<div class="tabs" id="primary-links" class:expanded={menuOpen}>
|
||||||
{#each visibleTabs as t (t.href)}
|
{#each visibleTabs as t (t.href)}
|
||||||
{@const on = t.match($page.url.pathname)}
|
{@const on = t.match($page.url.pathname)}
|
||||||
@@ -185,13 +213,11 @@
|
|||||||
<span class="lbl">{t.label}</span>
|
<span class="lbl">{t.label}</span>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>{/if}
|
|
||||||
<div class="nav-tools" class:expanded={menuOpen} class:account-only={!sessionReady}>
|
|
||||||
{#if sessionReady}<button class="utility" aria-label={`Recent notifications${unread ? `, ${unread} unread` : ''}`} onclick={() => openUtility('notifications')}>Activity{unread ? ` · ${unread}` : ''}</button><button class="utility" aria-label="Keyboard shortcuts" title="Keyboard shortcuts (?)" onclick={() => openUtility('help')}>?</button>{/if}
|
|
||||||
{#if data.user}<div class="account-menu"><button bind:this={accountButton} class="utility account-button" aria-expanded={accountOpen} aria-controls="account-links" onclick={() => accountOpen = !accountOpen}>{data.user.displayName || data.user.username}<span class="role">{data.user.role}</span></button>
|
|
||||||
{#if accountOpen}<div class="account-links" id="account-links"><a href="/account">My account</a>{#if data.user.role === 'admin' && sessionReady}<a href="/settings/users">Manage users</a>{/if}<button disabled={signingOut} onclick={signOut}>{signingOut ? 'Signing out…' : 'Sign out'}</button>{#if signOutError}<p role="alert">{signOutError}</p>{/if}</div>{/if}
|
|
||||||
</div>{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-tools" class:expanded={menuOpen}>
|
||||||
|
<button class="utility" aria-label={`Recent notifications${unread ? `, ${unread} unread` : ''}`} onclick={() => openUtility('notifications')}>Activity{unread ? ` · ${unread}` : ''}</button><button class="utility" aria-label="Keyboard shortcuts" title="Keyboard shortcuts (?)" onclick={() => openUtility('help')}>?</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
{#if sessionReady && secondary.length}<div class="subnav"><div role="navigation" aria-label="Section">{#each secondary as item}<a href={item.href} aria-current={secondaryActive(item.href) ? 'page' : undefined}>{item.label}</a>{/each}</div></div>{/if}
|
{#if sessionReady && secondary.length}<div class="subnav"><div role="navigation" aria-label="Section">{#each secondary as item}<a href={item.href} aria-current={secondaryActive(item.href) ? 'page' : undefined}>{item.label}</a>{/each}</div></div>{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -251,9 +277,11 @@
|
|||||||
main.auth-main { max-width:520px; padding-top:16px; }
|
main.auth-main { max-width:520px; padding-top:16px; }
|
||||||
.viewer-note { display:flex; gap:8px 16px; flex-wrap:wrap; padding:14px 18px; margin-bottom:24px; border:1px solid var(--border-subtle); border-radius:var(--radius-md); background:var(--surface); font-size:var(--text-sm); color:var(--text-secondary); } .viewer-note strong { color:var(--text-primary); }
|
.viewer-note { display:flex; gap:8px 16px; flex-wrap:wrap; padding:14px 18px; margin-bottom:24px; border:1px solid var(--border-subtle); border-radius:var(--radius-md); background:var(--surface); font-size:var(--text-sm); color:var(--text-secondary); } .viewer-note strong { color:var(--text-primary); }
|
||||||
.account-menu { position:relative; }
|
.account-menu { position:relative; }
|
||||||
.account-button { display:flex; align-items:center; gap:8px; max-width:220px; overflow:hidden; text-overflow:ellipsis; }
|
.header-actions { order:3; display:flex; align-items:center; gap:var(--space-2); margin-left:auto; }
|
||||||
.role { text-transform:capitalize; color:var(--text-tertiary); font-size:var(--text-xs); }
|
.account-button { display:grid; place-items:center; width:44px; height:44px; padding:0; border:1px solid var(--border-strong); border-radius:50%; background:color-mix(in oklab, var(--accent-primary) 12%, var(--surface)); color:var(--accent-primary-hover); font:inherit; font-weight:var(--weight-semibold); cursor:pointer; transition:background-color var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard); }
|
||||||
.account-links { position:absolute; right:0; top:calc(100% + 8px); min-width:200px; z-index:30; display:grid; padding:8px; background:var(--surface); border:1px solid var(--border-strong); border-radius:var(--radius-md); box-shadow:0 12px 32px #0005; }
|
.account-button:hover,.account-button[aria-expanded='true'] { background:var(--surface-raised); border-color:var(--accent-primary-hover); }
|
||||||
|
.account-name { padding:8px 12px 12px; margin-bottom:4px; border-bottom:1px solid var(--border-subtle); color:var(--text-secondary); font-size:var(--text-sm); overflow-wrap:anywhere; }
|
||||||
|
.account-links { position:absolute; right:0; top:calc(100% + 8px); width:220px; max-width:calc(100vw - 32px); max-height:calc(100dvh - 84px); overflow-y:auto; z-index:30; display:grid; padding:8px; background:var(--surface); border:1px solid var(--border-strong); border-radius:var(--radius-md); box-shadow:0 12px 32px #0005; }
|
||||||
.account-links a,.account-links button { text-align:left; padding:10px 12px; color:var(--text-primary); background:none; border:0; border-radius:var(--radius-sm); font:inherit; font-size:var(--text-sm); cursor:pointer; text-decoration:none; }
|
.account-links a,.account-links button { text-align:left; padding:10px 12px; color:var(--text-primary); background:none; border:0; border-radius:var(--radius-sm); font:inherit; font-size:var(--text-sm); cursor:pointer; text-decoration:none; }
|
||||||
.account-links a:hover,.account-links button:hover { background:var(--surface-raised); }
|
.account-links a:hover,.account-links button:hover { background:var(--surface-raised); }
|
||||||
.account-links p { padding:0 12px; font-size:var(--text-xs); color:var(--state-danger); }
|
.account-links p { padding:0 12px; font-size:var(--text-xs); color:var(--state-danger); }
|
||||||
@@ -288,6 +316,7 @@
|
|||||||
.brand-link:focus-visible { box-shadow: var(--focus-ring); }
|
.brand-link:focus-visible { box-shadow: var(--focus-ring); }
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
|
order: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@@ -317,7 +346,7 @@
|
|||||||
view-transition-name: nav-active;
|
view-transition-name: nav-active;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tools { display: flex; align-items: center; gap: 6px; margin-left: auto; }
|
.nav-tools { order: 2; display: flex; align-items: center; gap: 6px; margin-left: auto; }
|
||||||
.utility { background: var(--surface-raised); color: var(--text-secondary); border: 1px solid var(--border-strong); padding: 8px 10px; border-radius: var(--radius-md); cursor: pointer; font: inherit; font-size: var(--text-xs); white-space: nowrap; }
|
.utility { background: var(--surface-raised); color: var(--text-secondary); border: 1px solid var(--border-strong); padding: 8px 10px; border-radius: var(--radius-md); cursor: pointer; font: inherit; font-size: var(--text-xs); white-space: nowrap; }
|
||||||
.shortcuts { display: grid; gap: 12px; margin: 0; }
|
.shortcuts { display: grid; gap: 12px; margin: 0; }
|
||||||
.shortcuts > div { display: flex; gap: 20px; align-items: center; }
|
.shortcuts > div { display: flex; gap: 20px; align-items: center; }
|
||||||
@@ -344,9 +373,8 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
padding: calc(8px + env(safe-area-inset-top)) max(var(--space-4), env(safe-area-inset-right)) 8px max(var(--space-4), env(safe-area-inset-left));
|
padding: calc(8px + env(safe-area-inset-top)) max(var(--space-4), env(safe-area-inset-right)) 8px max(var(--space-4), env(safe-area-inset-left));
|
||||||
max-height: 100dvh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
.header-actions { order:0; }
|
||||||
.menu-toggle {
|
.menu-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -363,13 +391,15 @@
|
|||||||
}
|
}
|
||||||
.menu-toggle:focus-visible { box-shadow: var(--focus-ring); }
|
.menu-toggle:focus-visible { box-shadow: var(--focus-ring); }
|
||||||
.tabs { display: none; width: 100%; margin: 0; }
|
.tabs { display: none; width: 100%; margin: 0; }
|
||||||
.tabs.expanded { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); padding: var(--space-3) 0 4px; }
|
.tabs.expanded { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); padding: var(--space-3) 0 4px; max-height:calc(100dvh - 128px - env(safe-area-inset-top)); overflow-y:auto; }
|
||||||
.tabs a { display: flex; align-items: center; min-height: 44px; border-radius: var(--radius-md); }
|
.tabs a { display: flex; align-items: center; min-height: 44px; border-radius: var(--radius-md); }
|
||||||
.nav-tools { display: none; }
|
.nav-tools { display: none; }
|
||||||
.nav-tools.expanded { display: flex; width: 100%; padding-top: 8px; }
|
.nav-tools.expanded { display: flex; width: 100%; padding-top: 8px; }
|
||||||
.nav-tools.account-only { display:flex; }
|
}
|
||||||
.account-menu { margin-left:auto; }
|
@media (max-width: 400px) {
|
||||||
.account-links { position:static; margin-top:8px; }
|
.menu-label { display:none; }
|
||||||
|
.menu-toggle { justify-content:center; width:44px; padding:8px; }
|
||||||
|
.brand-link :global(.mark-sm .word) { font-size:12px; letter-spacing:1px; }
|
||||||
}
|
}
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
main { padding: var(--space-6) max(var(--space-4), env(safe-area-inset-right)) calc(var(--space-16) + env(safe-area-inset-bottom)) max(var(--space-4), env(safe-area-inset-left)); }
|
main { padding: var(--space-6) max(var(--space-4), env(safe-area-inset-right)) calc(var(--space-16) + env(safe-area-inset-bottom)) max(var(--space-4), env(safe-area-inset-left)); }
|
||||||
|
|||||||
Reference in New Issue
Block a user