web: scope sidebar to workspace routes
This commit is contained in:
parent
61b1735292
commit
2bd69f9403
|
|
@ -122,9 +122,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer layout {
|
@layer layout {
|
||||||
.workspace-layout {
|
.app-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
|
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
|
|
@ -133,12 +132,25 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 0;
|
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 {
|
.workspace-layout.sidebar-collapsed {
|
||||||
grid-template-columns: max-content minmax(0, 1fr);
|
grid-template-columns: max-content minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
.workspace-topbar {
|
.workspace-topbar {
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
@ -180,7 +192,6 @@
|
||||||
}
|
}
|
||||||
.shell {
|
.shell {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 2;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-6);
|
gap: var(--space-6);
|
||||||
|
|
@ -236,7 +247,7 @@
|
||||||
@layer components {
|
@layer components {
|
||||||
.workspace-sidebar {
|
.workspace-sidebar {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1 / 3;
|
grid-row: 1;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,12 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
||||||
const rootLayoutLoad = await Deno.readTextFile(
|
const rootLayoutLoad = await Deno.readTextFile(
|
||||||
new URL("./../../../routes/+layout.ts", import.meta.url),
|
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(
|
const sidebar = await Deno.readTextFile(
|
||||||
new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
|
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("topbar-icon-button") &&
|
||||||
rootLayout.includes('href="/account"') &&
|
rootLayout.includes('href="/account"') &&
|
||||||
rootLayout.includes("Open Account") &&
|
rootLayout.includes("Open Account") &&
|
||||||
|
!rootLayout.includes("WorkspaceSidebar") &&
|
||||||
!sidebar.includes("accountHref") &&
|
!sidebar.includes("accountHref") &&
|
||||||
!sidebar.includes("Open Account"),
|
!sidebar.includes("Open Account"),
|
||||||
"Account navigation should live in the global layout header, not the workspace sidebar",
|
"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(
|
assert(
|
||||||
rootLayoutLoad.includes('"/account"') && rootLayoutLoad.includes('"/login/device"'),
|
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",
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
|
||||||
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
|
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
|
||||||
import WorkspaceSidebar from '$lib/workspace/sidebar/WorkspaceSidebar.svelte';
|
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import type { LayoutProps } from './$types';
|
import type { LayoutProps } from './$types';
|
||||||
|
|
||||||
let { data, children }: LayoutProps = $props();
|
let { children }: LayoutProps = $props();
|
||||||
let sidebarCollapsed = $state(false);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WorkspaceAlerts />
|
<WorkspaceAlerts />
|
||||||
|
|
||||||
<div class:sidebar-collapsed={sidebarCollapsed} class="workspace-layout">
|
<div class="app-layout">
|
||||||
<WorkspaceSidebar
|
|
||||||
workspace={data.workspace}
|
|
||||||
workspaceError={data.workspaceError}
|
|
||||||
repositories={data.repositories}
|
|
||||||
repositoriesError={data.repositoriesError}
|
|
||||||
currentPath={page.url.pathname}
|
|
||||||
collapsed={sidebarCollapsed}
|
|
||||||
onToggleCollapsed={() => (sidebarCollapsed = !sidebarCollapsed)}
|
|
||||||
/>
|
|
||||||
<header class="workspace-topbar">
|
<header class="workspace-topbar">
|
||||||
<nav class="workspace-topbar-actions" aria-label="Global navigation">
|
<nav class="workspace-topbar-actions" aria-label="Global navigation">
|
||||||
<a class="topbar-icon-button" href="/account" aria-label="Open Account" title="Account">
|
<a class="topbar-icon-button" href="/account" aria-label="Open Account" title="Account">
|
||||||
|
|
@ -31,7 +19,7 @@
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main class="shell">
|
<div class="app-shell">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
import { redirect } from "@sveltejs/kit";
|
import { redirect } from "@sveltejs/kit";
|
||||||
import { loadJson, workspaceApiPath, workspaceRoute } from "$lib/workspace/api/http";
|
import { loadJson, workspaceRoute } from "$lib/workspace/api/http";
|
||||||
import type { RepositoryListResponse, WorkspaceResponse } from "$lib/workspace/sidebar/types";
|
import type { WorkspaceResponse } from "$lib/workspace/sidebar/types";
|
||||||
import type { LayoutLoad } from "./$types";
|
import type { LayoutLoad } from "./$types";
|
||||||
|
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
export const load: LayoutLoad = async ({ fetch, params, url }) => {
|
export const load: LayoutLoad = async ({ fetch, params, url }) => {
|
||||||
const workspaceId = params.workspaceId;
|
if (params.workspaceId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (!workspaceId) {
|
|
||||||
const publicRoutes = new Set(["/account", "/login/device"]);
|
const publicRoutes = new Set(["/account", "/login/device"]);
|
||||||
if (publicRoutes.has(url.pathname)) {
|
if (publicRoutes.has(url.pathname)) {
|
||||||
return {
|
return {};
|
||||||
workspace: null,
|
|
||||||
workspaceError: null,
|
|
||||||
repositories: null,
|
|
||||||
repositoriesError: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = await loadJson<WorkspaceResponse>(fetch, "/api/workspace");
|
const workspace = await loadJson<WorkspaceResponse>(fetch, "/api/workspace");
|
||||||
|
|
@ -25,25 +21,5 @@ export const load: LayoutLoad = async ({ fetch, params, url }) => {
|
||||||
const scopedPath = workspaceRoute(workspace.data.workspace_id);
|
const scopedPath = workspaceRoute(workspace.data.workspace_id);
|
||||||
throw redirect(307, `${scopedPath}${url.search}`);
|
throw redirect(307, `${scopedPath}${url.search}`);
|
||||||
}
|
}
|
||||||
return {
|
return {};
|
||||||
workspace: null,
|
|
||||||
workspaceError: workspace.error,
|
|
||||||
repositories: null,
|
|
||||||
repositoriesError: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiPath = (path: string) => workspaceApiPath(workspaceId, path);
|
|
||||||
|
|
||||||
const [workspace, repositories] = await Promise.all([
|
|
||||||
loadJson<WorkspaceResponse>(fetch, apiPath("/workspace")),
|
|
||||||
loadJson<RepositoryListResponse>(fetch, apiPath("/repositories")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
workspace: workspace.data,
|
|
||||||
workspaceError: workspace.error,
|
|
||||||
repositories: repositories.data,
|
|
||||||
repositoriesError: repositories.error,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
23
web/workspace/src/routes/w/[workspaceId]/+layout.svelte
Normal file
23
web/workspace/src/routes/w/[workspaceId]/+layout.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import WorkspaceSidebar from '$lib/workspace/sidebar/WorkspaceSidebar.svelte';
|
||||||
|
import type { LayoutProps } from './$types';
|
||||||
|
|
||||||
|
let { data, children }: LayoutProps = $props();
|
||||||
|
let sidebarCollapsed = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class:sidebar-collapsed={sidebarCollapsed} class="workspace-layout">
|
||||||
|
<WorkspaceSidebar
|
||||||
|
workspace={data.workspace}
|
||||||
|
workspaceError={data.workspaceError}
|
||||||
|
repositories={data.repositories}
|
||||||
|
repositoriesError={data.repositoriesError}
|
||||||
|
currentPath={page.url.pathname}
|
||||||
|
collapsed={sidebarCollapsed}
|
||||||
|
onToggleCollapsed={() => (sidebarCollapsed = !sidebarCollapsed)}
|
||||||
|
/>
|
||||||
|
<main class="shell">
|
||||||
|
{@render children()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
18
web/workspace/src/routes/w/[workspaceId]/+layout.ts
Normal file
18
web/workspace/src/routes/w/[workspaceId]/+layout.ts
Normal file
|
|
@ -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<WorkspaceResponse>(fetch, apiPath("/workspace")),
|
||||||
|
loadJson<RepositoryListResponse>(fetch, apiPath("/repositories")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspace: workspace.data,
|
||||||
|
workspaceError: workspace.error,
|
||||||
|
repositories: repositories.data,
|
||||||
|
repositoriesError: repositories.error,
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user