diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css
index 8d047123..fffc5f5b 100644
--- a/web/workspace/src/app.css
+++ b/web/workspace/src/app.css
@@ -125,6 +125,7 @@
.workspace-layout {
display: grid;
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
+ grid-template-rows: auto minmax(0, 1fr);
width: 100vw;
height: 100dvh;
margin: 0;
@@ -135,7 +136,69 @@
.workspace-layout.sidebar-collapsed {
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 {
+ grid-column: 2;
+ grid-row: 2;
display: flex;
flex-direction: column;
gap: var(--space-6);
@@ -160,6 +223,7 @@
@media (max-width: 760px) {
.workspace-layout {
grid-template-columns: 1fr;
+ grid-template-rows: auto auto 1fr;
width: 100vw;
height: auto;
min-height: 100dvh;
@@ -168,7 +232,18 @@
.workspace-layout.sidebar-collapsed {
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 {
+ grid-column: 1;
+ grid-row: 3;
overflow: visible;
padding: var(--space-5) var(--space-4);
}
@@ -176,6 +251,8 @@
}
@layer components {
.workspace-sidebar {
+ grid-column: 1;
+ grid-row: 1 / 3;
align-self: stretch;
min-width: 0;
min-height: 0;
diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts
index 0b94aebf..3acf3cf7 100644
--- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts
+++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts
@@ -385,8 +385,14 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
new URL("../auth/api.ts", import.meta.url),
);
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),
);
+ const sidebar = await Deno.readTextFile(
+ new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
+ );
assert(
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",
);
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",
);
});
diff --git a/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte b/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte
index a1151f8a..54326508 100644
--- a/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte
+++ b/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte
@@ -28,7 +28,6 @@
let workspaceId = $derived(workspace?.workspace_id ?? '');
let homeHref = $derived(workspaceId ? workspaceRoute(workspaceId) : '/');
- let accountHref = '/account';
let settingsHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/settings') : '/settings');
@@ -88,17 +87,6 @@
/>
-