From 308e1ffdbcbaffa5ef1cf6153faf243d1a0bfb8d Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 23 Jul 2026 19:23:02 +0900 Subject: [PATCH] web: add global sidebar fallback --- web/workspace/src/app.css | 49 ++++++++++++++++--- .../console/worker-console.ui.test.ts | 15 +++++- .../workspace/sidebar/GlobalSidebar.svelte | 30 ++++++++++++ web/workspace/src/routes/+layout.svelte | 13 +++-- web/workspace/src/routes/+layout.ts | 6 +-- 5 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index 8b5a1e6a..526dc9c7 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -133,6 +133,15 @@ min-width: 0; } .app-shell { + display: grid; + min-width: 0; + min-height: 0; + overflow: hidden; + } + .app-shell.global-sidebar-layout { + grid-template-columns: minmax(180px, 240px) minmax(0, 1fr); + } + .app-content { min-width: 0; min-height: 0; overflow: auto; @@ -214,14 +223,31 @@ gap: var(--space-4); } @media (max-width: 760px) { - .workspace-layout { - grid-template-columns: 1fr; - grid-template-rows: auto auto 1fr; - width: 100vw; + .app-layout { height: auto; min-height: 100dvh; overflow: visible; } + .app-shell, + .app-shell.global-sidebar-layout { + grid-template-columns: 1fr; + overflow: visible; + } + .app-content { + overflow: visible; + } + .global-sidebar { + border-right: 0; + border-bottom: 1px solid var(--line); + } + .workspace-layout { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + width: 100%; + height: auto; + min-height: 0; + overflow: visible; + } .workspace-layout.sidebar-collapsed { grid-template-columns: 1fr; } @@ -232,19 +258,28 @@ border-bottom: 1px solid var(--line); } .workspace-topbar { - grid-column: 1; - grid-row: 2; padding: 0 var(--space-4); } .shell { grid-column: 1; - grid-row: 3; + grid-row: 2; overflow: visible; padding: var(--space-5) var(--space-4); } } } @layer components { + .global-sidebar { + min-width: 0; + min-height: 0; + overflow-y: auto; + padding: var(--space-4) var(--space-3); + border-right: 1px solid var(--line); + } + .global-sidebar-section { + display: grid; + gap: var(--space-2); + } .workspace-sidebar { grid-column: 1; grid-row: 1; 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 c2ab4826..c8949c28 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 @@ -390,6 +390,9 @@ Deno.test("Account UI owns browser passkey session state without workspace autho const rootLayoutLoad = await Deno.readTextFile( new URL("./../../../routes/+layout.ts", import.meta.url), ); + const globalSidebar = await Deno.readTextFile( + new URL("../sidebar/GlobalSidebar.svelte", import.meta.url), + ); const workspaceLayout = await Deno.readTextFile( new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url), ); @@ -424,7 +427,9 @@ 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("workspace-topbar") && + rootLayout.includes("GlobalSidebar") && + rootLayout.includes("global-sidebar-layout") && + rootLayout.includes("workspace-topbar") && rootLayout.includes("topbar-icon-button") && rootLayout.includes('href="/account"') && rootLayout.includes("Open Account") && @@ -433,6 +438,14 @@ Deno.test("Account UI owns browser passkey session state without workspace autho !sidebar.includes("Open Account"), "Account navigation should live in the global layout header, not the workspace sidebar", ); + assert( + globalSidebar.includes("Global") && + globalSidebar.includes("/account") && + globalSidebar.includes("/login/device") && + !globalSidebar.includes("Tickets") && + !globalSidebar.includes("Repositories"), + "Root default sidebar should contain only global navigation, not workspace-scoped sections", + ); assert( workspaceLayout.includes("WorkspaceSidebar") && workspaceLayout.includes("workspace={data.workspace}") && diff --git a/web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte b/web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte new file mode 100644 index 00000000..bc6e6558 --- /dev/null +++ b/web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte @@ -0,0 +1,30 @@ + + + diff --git a/web/workspace/src/routes/+layout.svelte b/web/workspace/src/routes/+layout.svelte index fc368d29..4de53e22 100644 --- a/web/workspace/src/routes/+layout.svelte +++ b/web/workspace/src/routes/+layout.svelte @@ -1,9 +1,11 @@ @@ -19,7 +21,12 @@ -
- {@render children()} +
+ {#if !data.workspaceScoped} + + {/if} +
+ {@render children()} +
diff --git a/web/workspace/src/routes/+layout.ts b/web/workspace/src/routes/+layout.ts index e965cb42..4afd442b 100644 --- a/web/workspace/src/routes/+layout.ts +++ b/web/workspace/src/routes/+layout.ts @@ -8,12 +8,12 @@ export const prerender = false; export const load: LayoutLoad = async ({ fetch, params, url }) => { if (params.workspaceId) { - return {}; + return { workspaceScoped: true }; } const publicRoutes = new Set(["/account", "/login/device"]); if (publicRoutes.has(url.pathname)) { - return {}; + return { workspaceScoped: false }; } const workspace = await loadJson(fetch, "/api/workspace"); @@ -21,5 +21,5 @@ export const load: LayoutLoad = async ({ fetch, params, url }) => { const scopedPath = workspaceRoute(workspace.data.workspace_id); throw redirect(307, `${scopedPath}${url.search}`); } - return {}; + return { workspaceScoped: false }; };