diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index 48070aaa..8b5a1e6a 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -122,9 +122,8 @@ } } @layer layout { - .workspace-layout { + .app-layout { display: grid; - grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); width: 100vw; height: 100dvh; @@ -133,12 +132,25 @@ overflow: hidden; min-width: 0; } + .app-shell { + 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%; + margin: 0; + padding: 0; + overflow: hidden; + min-width: 0; + } .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: flex-end; @@ -180,7 +192,6 @@ } .shell { grid-column: 2; - grid-row: 2; display: flex; flex-direction: column; gap: var(--space-6); @@ -236,7 +247,7 @@ @layer components { .workspace-sidebar { grid-column: 1; - grid-row: 1 / 3; + grid-row: 1; 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 3acf3cf7..c2ab4826 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,12 @@ 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 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), ); @@ -422,10 +428,17 @@ Deno.test("Account UI owns browser passkey session state without workspace autho 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", ); + 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", + ); 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/routes/+layout.svelte b/web/workspace/src/routes/+layout.svelte index cd5280bc..fc368d29 100644 --- a/web/workspace/src/routes/+layout.svelte +++ b/web/workspace/src/routes/+layout.svelte @@ -1,26 +1,14 @@ -
- (sidebarCollapsed = !sidebarCollapsed)} - /> +
-
+
{@render children()} -
+
diff --git a/web/workspace/src/routes/+layout.ts b/web/workspace/src/routes/+layout.ts index 688ec8d3..e965cb42 100644 --- a/web/workspace/src/routes/+layout.ts +++ b/web/workspace/src/routes/+layout.ts @@ -1,49 +1,25 @@ import { redirect } from "@sveltejs/kit"; -import { loadJson, workspaceApiPath, workspaceRoute } from "$lib/workspace/api/http"; -import type { RepositoryListResponse, WorkspaceResponse } from "$lib/workspace/sidebar/types"; +import { loadJson, workspaceRoute } from "$lib/workspace/api/http"; +import type { 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 }) => { - const workspaceId = params.workspaceId; - - if (!workspaceId) { - const publicRoutes = new Set(["/account", "/login/device"]); - if (publicRoutes.has(url.pathname)) { - return { - workspace: null, - workspaceError: null, - repositories: null, - repositoriesError: null, - }; - } - - const workspace = await loadJson(fetch, "/api/workspace"); - if (workspace.data) { - const scopedPath = workspaceRoute(workspace.data.workspace_id); - throw redirect(307, `${scopedPath}${url.search}`); - } - return { - workspace: null, - workspaceError: workspace.error, - repositories: null, - repositoriesError: null, - }; + if (params.workspaceId) { + return {}; } - const apiPath = (path: string) => workspaceApiPath(workspaceId, path); + const publicRoutes = new Set(["/account", "/login/device"]); + if (publicRoutes.has(url.pathname)) { + return {}; + } - 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, - }; + const workspace = await loadJson(fetch, "/api/workspace"); + if (workspace.data) { + const scopedPath = workspaceRoute(workspace.data.workspace_id); + throw redirect(307, `${scopedPath}${url.search}`); + } + return {}; }; diff --git a/web/workspace/src/routes/w/[workspaceId]/+layout.svelte b/web/workspace/src/routes/w/[workspaceId]/+layout.svelte new file mode 100644 index 00000000..d49f03df --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/+layout.svelte @@ -0,0 +1,23 @@ + + +
+ (sidebarCollapsed = !sidebarCollapsed)} + /> +
+ {@render children()} +
+
diff --git a/web/workspace/src/routes/w/[workspaceId]/+layout.ts b/web/workspace/src/routes/w/[workspaceId]/+layout.ts new file mode 100644 index 00000000..39058b67 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/+layout.ts @@ -0,0 +1,18 @@ +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, + }; +};