web: restore root sidebar chrome

This commit is contained in:
Keisuke Hirata 2026-07-23 19:47:11 +09:00
parent 308e1ffdbc
commit b3854004a1
No known key found for this signature in database
6 changed files with 57 additions and 109 deletions

View File

@ -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;

View File

@ -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"'),

View File

@ -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>

View File

@ -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"]);

View File

@ -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>

View File

@ -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,
};
};