Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
183 lines
5.8 KiB
Svelte
183 lines
5.8 KiB
Svelte
<!--
|
|
Shared account menu — one component for both breakpoints. Anchored to the
|
|
user's name/avatar, it groups the account-level destinations (Downloads,
|
|
Settings, Display) and Sign out. Available on every authenticated,
|
|
non-immersive screen via the shared AppHeader.
|
|
|
|
TRACES: UR-054 | DR-075
|
|
-->
|
|
<script lang="ts">
|
|
import { tick } from "svelte";
|
|
import { goto } from "$app/navigation";
|
|
import { auth, currentUser, serverName, serverUrl } from "$lib/stores/auth";
|
|
import { library } from "$lib/stores/library";
|
|
|
|
let open = $state(false);
|
|
let triggerEl = $state<HTMLButtonElement | null>(null);
|
|
|
|
// Prefer the human server name; fall back to the bare host of the URL so the
|
|
// identity block always shows *something* server-identifying.
|
|
const serverHost = $derived.by(() => {
|
|
if ($serverName) return $serverName;
|
|
if (!$serverUrl) return "";
|
|
try {
|
|
return new URL($serverUrl).host;
|
|
} catch {
|
|
return $serverUrl;
|
|
}
|
|
});
|
|
|
|
const displayName = $derived($currentUser?.name ?? "Account");
|
|
const initial = $derived((displayName[0] ?? "?").toUpperCase());
|
|
|
|
async function close(returnFocus = true) {
|
|
open = false;
|
|
if (returnFocus) {
|
|
await tick();
|
|
triggerEl?.focus();
|
|
}
|
|
}
|
|
|
|
function toggle() {
|
|
open = !open;
|
|
}
|
|
|
|
function onKeydown(e: KeyboardEvent) {
|
|
if (e.key === "Escape" && open) {
|
|
e.stopPropagation();
|
|
close();
|
|
}
|
|
}
|
|
|
|
async function handleLogout() {
|
|
await close(false);
|
|
await auth.logout();
|
|
library.reset();
|
|
goto("/");
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={onKeydown} />
|
|
|
|
<div class="relative">
|
|
<button
|
|
bind:this={triggerEl}
|
|
onclick={toggle}
|
|
class="flex items-center gap-2 rounded-full py-1 pl-1 pr-1 md:pr-3 text-gray-300 hover:text-white hover:bg-[var(--color-surface)] transition-colors"
|
|
aria-haspopup="menu"
|
|
aria-expanded={open}
|
|
aria-label="Account menu"
|
|
>
|
|
<span
|
|
class="flex h-8 w-8 items-center justify-center rounded-full bg-[var(--color-jellyfin)] text-sm font-semibold text-white"
|
|
aria-hidden="true"
|
|
>
|
|
{initial}
|
|
</span>
|
|
<span class="hidden md:inline text-sm">{displayName}</span>
|
|
</button>
|
|
|
|
{#if open}
|
|
<!-- Backdrop closes the menu on any outside click. -->
|
|
<div
|
|
class="fixed inset-0 z-40"
|
|
onclick={() => close()}
|
|
onkeydown={(e) => {
|
|
if (e.key === "Enter" || e.key === " ") close();
|
|
}}
|
|
role="button"
|
|
tabindex="-1"
|
|
aria-label="Close account menu"
|
|
></div>
|
|
|
|
<div
|
|
class="absolute right-0 top-full mt-2 w-56 bg-[var(--color-surface)] rounded-lg shadow-lg border border-gray-700 py-1 z-50"
|
|
role="menu"
|
|
>
|
|
<!-- Identity block — not interactive. -->
|
|
<div class="px-4 py-3">
|
|
<p class="text-xs text-gray-500">Signed in as</p>
|
|
<p class="text-sm font-semibold text-white truncate">{displayName}</p>
|
|
{#if serverHost}
|
|
<p class="text-xs text-gray-400 truncate">{serverHost}</p>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="border-t border-gray-700 my-1"></div>
|
|
|
|
<a
|
|
href="/downloads"
|
|
role="menuitem"
|
|
class="flex items-center gap-3 px-4 py-3 text-sm text-gray-300 hover:bg-gray-700 transition-colors"
|
|
onclick={() => close(false)}
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
/>
|
|
</svg>
|
|
Downloads
|
|
</a>
|
|
<a
|
|
href="/settings"
|
|
role="menuitem"
|
|
class="flex items-center gap-3 px-4 py-3 text-sm text-gray-300 hover:bg-gray-700 transition-colors"
|
|
onclick={() => close(false)}
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
/>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
</svg>
|
|
Settings
|
|
</a>
|
|
<a
|
|
href="/settings#display"
|
|
role="menuitem"
|
|
class="flex items-center gap-3 px-4 py-3 text-sm text-gray-300 hover:bg-gray-700 transition-colors"
|
|
onclick={() => close(false)}
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v10a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM8 20h8"
|
|
/>
|
|
</svg>
|
|
Display
|
|
</a>
|
|
|
|
<div class="border-t border-gray-700 my-1"></div>
|
|
|
|
<button
|
|
onclick={handleLogout}
|
|
role="menuitem"
|
|
class="w-full flex items-center gap-3 px-4 py-3 text-sm text-gray-300 hover:bg-gray-700 transition-colors"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
|
/>
|
|
</svg>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|