web: move account nav to header

This commit is contained in:
Keisuke Hirata 2026-07-23 18:46:43 +09:00
parent 2285277060
commit 11663830e3
No known key found for this signature in database
4 changed files with 107 additions and 13 deletions

View File

@ -125,6 +125,7 @@
.workspace-layout { .workspace-layout {
display: grid; display: grid;
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr);
width: 100vw; width: 100vw;
height: 100dvh; height: 100dvh;
margin: 0; margin: 0;
@ -135,7 +136,69 @@
.workspace-layout.sidebar-collapsed { .workspace-layout.sidebar-collapsed {
grid-template-columns: max-content minmax(0, 1fr); grid-template-columns: max-content minmax(0, 1fr);
} }
.workspace-topbar {
grid-column: 2;
grid-row: 1;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 0;
min-height: 3.25rem;
padding: 0 var(--space-5);
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--bg-raised) 88%, transparent);
backdrop-filter: blur(14px);
}
.workspace-topbar-title {
display: inline-flex;
align-items: center;
gap: var(--space-2);
color: var(--text-strong);
font-weight: 700;
}
.workspace-topbar-logo {
display: inline-grid;
width: 1.8rem;
height: 1.8rem;
place-items: center;
border-radius: var(--radius-soft);
background: var(--interactive-hover);
color: var(--text-strong);
font-size: 0.9rem;
font-weight: 800;
}
.workspace-topbar-actions {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.topbar-icon-button {
display: inline-flex;
width: 2.35rem;
height: 2.35rem;
align-items: center;
justify-content: center;
border-radius: 999px;
color: var(--text-muted);
text-decoration: none;
}
.topbar-icon-button:hover,
.topbar-icon-button:focus-visible {
background: var(--interactive-hover);
color: var(--text-muted);
}
.topbar-icon {
width: 1.1rem;
height: 1.1rem;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.shell { .shell {
grid-column: 2;
grid-row: 2;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-6); gap: var(--space-6);
@ -160,6 +223,7 @@
@media (max-width: 760px) { @media (max-width: 760px) {
.workspace-layout { .workspace-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr;
width: 100vw; width: 100vw;
height: auto; height: auto;
min-height: 100dvh; min-height: 100dvh;
@ -168,7 +232,18 @@
.workspace-layout.sidebar-collapsed { .workspace-layout.sidebar-collapsed {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.workspace-sidebar {
grid-column: 1;
grid-row: 1;
}
.workspace-topbar {
grid-column: 1;
grid-row: 2;
padding: 0 var(--space-4);
}
.shell { .shell {
grid-column: 1;
grid-row: 3;
overflow: visible; overflow: visible;
padding: var(--space-5) var(--space-4); padding: var(--space-5) var(--space-4);
} }
@ -176,6 +251,8 @@
} }
@layer components { @layer components {
.workspace-sidebar { .workspace-sidebar {
grid-column: 1;
grid-row: 1 / 3;
align-self: stretch; align-self: stretch;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;

View File

@ -385,8 +385,14 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
new URL("../auth/api.ts", import.meta.url), new URL("../auth/api.ts", import.meta.url),
); );
const rootLayout = await Deno.readTextFile( const rootLayout = await Deno.readTextFile(
new URL("./../../../routes/+layout.svelte", import.meta.url),
);
const rootLayoutLoad = await Deno.readTextFile(
new URL("./../../../routes/+layout.ts", import.meta.url), new URL("./../../../routes/+layout.ts", import.meta.url),
); );
const sidebar = await Deno.readTextFile(
new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
);
assert( assert(
accountPage.includes("registerPasskey") && accountPage.includes("registerPasskey") &&
@ -412,7 +418,16 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
"Auth model should stay on Backend auth APIs rather than workspace authorization APIs", "Auth model should stay on Backend auth APIs rather than workspace authorization APIs",
); );
assert( assert(
rootLayout.includes('"/account"') && rootLayout.includes('"/login/device"'), rootLayout.includes("workspace-topbar") &&
rootLayout.includes("topbar-icon-button") &&
rootLayout.includes('href="/account"') &&
rootLayout.includes("Open Account") &&
!sidebar.includes("accountHref") &&
!sidebar.includes("Open Account"),
"Account navigation should live in the global layout header, not the workspace sidebar",
);
assert(
rootLayoutLoad.includes('"/account"') && rootLayoutLoad.includes('"/login/device"'),
"Root layout should not redirect account and device-login public routes to a workspace", "Root layout should not redirect account and device-login public routes to a workspace",
); );
}); });

View File

@ -28,7 +28,6 @@
let workspaceId = $derived(workspace?.workspace_id ?? ''); let workspaceId = $derived(workspace?.workspace_id ?? '');
let homeHref = $derived(workspaceId ? workspaceRoute(workspaceId) : '/'); let homeHref = $derived(workspaceId ? workspaceRoute(workspaceId) : '/');
let accountHref = '/account';
let settingsHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/settings') : '/settings'); let settingsHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/settings') : '/settings');
</script> </script>
@ -88,17 +87,6 @@
/> />
</svg> </svg>
</a> </a>
<a
class="sidebar-icon-button"
href={accountHref}
aria-label="Open Account"
title="Account"
>
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</a>
<a <a
class="sidebar-icon-button" class="sidebar-icon-button"
href={settingsHref} href={settingsHref}

View File

@ -21,6 +21,20 @@
collapsed={sidebarCollapsed} collapsed={sidebarCollapsed}
onToggleCollapsed={() => (sidebarCollapsed = !sidebarCollapsed)} onToggleCollapsed={() => (sidebarCollapsed = !sidebarCollapsed)}
/> />
<header class="workspace-topbar">
<div class="workspace-topbar-title" aria-label="Yoi Workspace">
<span class="workspace-topbar-logo" aria-hidden="true">Y</span>
<span>Yoi</span>
</div>
<nav class="workspace-topbar-actions" aria-label="Global navigation">
<a class="topbar-icon-button" href="/account" aria-label="Open Account" title="Account">
<svg class="topbar-icon" aria-hidden="true" viewBox="0 0 24 24">
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</a>
</nav>
</header>
<main class="shell"> <main class="shell">
{@render children()} {@render children()}
</main> </main>