web: restore root sidebar chrome
This commit is contained in:
parent
308e1ffdbc
commit
b3854004a1
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"'),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
</script>
|
||||
|
||||
<WorkspaceAlerts />
|
||||
|
||||
<div class="app-layout">
|
||||
<div class:sidebar-collapsed={data.workspaceScoped && sidebarCollapsed} class="workspace-layout">
|
||||
{#if data.workspaceScoped}
|
||||
<WorkspaceSidebar
|
||||
workspace={data.workspace ?? null}
|
||||
workspaceError={data.workspaceError ?? null}
|
||||
repositories={data.repositories ?? null}
|
||||
repositoriesError={data.repositoriesError ?? null}
|
||||
currentPath={page.url.pathname}
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggleCollapsed={() => (sidebarCollapsed = !sidebarCollapsed)}
|
||||
/>
|
||||
{:else}
|
||||
<GlobalSidebar currentPath={page.url.pathname} />
|
||||
{/if}
|
||||
<header class="workspace-topbar">
|
||||
<nav class="workspace-topbar-actions" aria-label="Global navigation">
|
||||
<a class="topbar-icon-button" href="/account" aria-label="Open Account" title="Account">
|
||||
|
|
@ -21,12 +36,7 @@
|
|||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
<div class:global-sidebar-layout={!data.workspaceScoped} class="app-shell">
|
||||
{#if !data.workspaceScoped}
|
||||
<GlobalSidebar currentPath={page.url.pathname} />
|
||||
{/if}
|
||||
<div class="app-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
<main class="shell">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceResponse>(fetch, apiPath("/workspace")),
|
||||
loadJson<RepositoryListResponse>(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"]);
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -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<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