diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index 526dc9c7..9c529e28 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -122,35 +122,12 @@ } } @layer layout { - .app-layout { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - width: 100vw; - height: 100dvh; - margin: 0; - padding: 0; - overflow: hidden; - 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; - } .workspace-layout { display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); - width: 100%; - height: 100%; + grid-template-rows: auto minmax(0, 1fr); + width: 100vw; + height: 100dvh; margin: 0; padding: 0; overflow: hidden; @@ -160,6 +137,8 @@ grid-template-columns: max-content minmax(0, 1fr); } .workspace-topbar { + grid-column: 2; + grid-row: 1; display: flex; align-items: center; justify-content: flex-end; @@ -201,6 +180,7 @@ } .shell { grid-column: 2; + grid-row: 2; display: flex; flex-direction: column; gap: var(--space-6); @@ -223,26 +203,15 @@ gap: var(--space-4); } @media (max-width: 760px) { - .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 { + grid-column: 1; + grid-row: 1; border-right: 0; border-bottom: 1px solid var(--line); } .workspace-layout { grid-template-columns: 1fr; - grid-template-rows: auto 1fr; + grid-template-rows: auto auto 1fr; width: 100%; height: auto; min-height: 0; @@ -258,11 +227,13 @@ 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: 2; + grid-row: 3; overflow: visible; padding: var(--space-5) var(--space-4); } @@ -270,6 +241,8 @@ } @layer components { .global-sidebar { + grid-column: 1; + grid-row: 1 / 3; min-width: 0; min-height: 0; overflow-y: auto; @@ -282,7 +255,7 @@ } .workspace-sidebar { grid-column: 1; - grid-row: 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 c8949c28..4ebd0d24 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 @@ -393,12 +393,6 @@ Deno.test("Account UI owns browser passkey session state without workspace autho 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), - ); - const workspaceLayoutLoad = await Deno.readTextFile( - new URL("./../../../routes/w/[workspaceId]/+layout.ts", import.meta.url), - ); const sidebar = await Deno.readTextFile( new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url), ); @@ -428,15 +422,15 @@ Deno.test("Account UI owns browser passkey session state without workspace autho ); assert( rootLayout.includes("GlobalSidebar") && - rootLayout.includes("global-sidebar-layout") && + rootLayout.includes("WorkspaceSidebar") && + rootLayout.includes("data.workspaceScoped") && rootLayout.includes("workspace-topbar") && rootLayout.includes("topbar-icon-button") && rootLayout.includes('href="/account"') && rootLayout.includes("Open Account") && - !rootLayout.includes("WorkspaceSidebar") && !sidebar.includes("accountHref") && !sidebar.includes("Open Account"), - "Account navigation should live in the global layout header, not the workspace sidebar", + "Root layout chrome should choose GlobalSidebar or WorkspaceSidebar while account navigation stays in the header", ); assert( globalSidebar.includes("Global") && @@ -447,10 +441,10 @@ Deno.test("Account UI owns browser passkey session state without workspace autho "Root default sidebar should contain only global navigation, not workspace-scoped sections", ); assert( - workspaceLayout.includes("WorkspaceSidebar") && - workspaceLayout.includes("workspace={data.workspace}") && - workspaceLayoutLoad.includes("workspaceApiPath(params.workspaceId"), - "Workspace sidebar and workspace-scoped data loading should live under /w/[workspaceId] layout", + rootLayoutLoad.includes("params.workspaceId") && + rootLayoutLoad.includes("workspaceApiPath(workspaceId") && + rootLayoutLoad.includes("workspaceScoped: true"), + "Root layout load should provide workspace-scoped sidebar data when a workspace route is active", ); assert( rootLayoutLoad.includes('"/account"') && rootLayoutLoad.includes('"/login/device"'), diff --git a/web/workspace/src/routes/+layout.svelte b/web/workspace/src/routes/+layout.svelte index 4de53e22..68a95623 100644 --- a/web/workspace/src/routes/+layout.svelte +++ b/web/workspace/src/routes/+layout.svelte @@ -2,15 +2,30 @@ import { page } from '$app/state'; import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte'; import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte'; + import WorkspaceSidebar from '$lib/workspace/sidebar/WorkspaceSidebar.svelte'; import '../app.css'; import type { LayoutProps } from './$types'; let { data, children }: LayoutProps = $props(); + let sidebarCollapsed = $state(false); -
+
+ {#if data.workspaceScoped} + (sidebarCollapsed = !sidebarCollapsed)} + /> + {:else} + + {/if}
-
- {#if !data.workspaceScoped} - - {/if} -
- {@render children()} -
-
+
+ {@render children()} +
diff --git a/web/workspace/src/routes/+layout.ts b/web/workspace/src/routes/+layout.ts index 4afd442b..77e13c9c 100644 --- a/web/workspace/src/routes/+layout.ts +++ b/web/workspace/src/routes/+layout.ts @@ -1,14 +1,26 @@ import { redirect } from "@sveltejs/kit"; -import { loadJson, workspaceRoute } from "$lib/workspace/api/http"; -import type { WorkspaceResponse } from "$lib/workspace/sidebar/types"; +import { loadJson, workspaceApiPath, workspaceRoute } from "$lib/workspace/api/http"; +import type { RepositoryListResponse, WorkspaceResponse } from "$lib/workspace/sidebar/types"; import type { LayoutLoad } from "./$types"; export const ssr = false; export const prerender = false; export const load: LayoutLoad = async ({ fetch, params, url }) => { - if (params.workspaceId) { - return { workspaceScoped: true }; + const workspaceId = params.workspaceId; + if (workspaceId) { + const apiPath = (path: string) => workspaceApiPath(workspaceId, path); + const [workspace, repositories] = await Promise.all([ + loadJson(fetch, apiPath("/workspace")), + loadJson(fetch, apiPath("/repositories")), + ]); + return { + workspaceScoped: true, + workspace: workspace.data, + workspaceError: workspace.error, + repositories: repositories.data, + repositoriesError: repositories.error, + }; } const publicRoutes = new Set(["/account", "/login/device"]); diff --git a/web/workspace/src/routes/w/[workspaceId]/+layout.svelte b/web/workspace/src/routes/w/[workspaceId]/+layout.svelte deleted file mode 100644 index d49f03df..00000000 --- a/web/workspace/src/routes/w/[workspaceId]/+layout.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
- (sidebarCollapsed = !sidebarCollapsed)} - /> -
- {@render children()} -
-
diff --git a/web/workspace/src/routes/w/[workspaceId]/+layout.ts b/web/workspace/src/routes/w/[workspaceId]/+layout.ts deleted file mode 100644 index 39058b67..00000000 --- a/web/workspace/src/routes/w/[workspaceId]/+layout.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; -import type { RepositoryListResponse, WorkspaceResponse } from "$lib/workspace/sidebar/types"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({ fetch, params }) => { - const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path); - const [workspace, repositories] = await Promise.all([ - loadJson(fetch, apiPath("/workspace")), - loadJson(fetch, apiPath("/repositories")), - ]); - - return { - workspace: workspace.data, - workspaceError: workspace.error, - repositories: repositories.data, - repositoriesError: repositories.error, - }; -};