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 { @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 { .workspace-layout {
display: grid; display: grid;
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
width: 100%; grid-template-rows: auto minmax(0, 1fr);
height: 100%; width: 100vw;
height: 100dvh;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
@ -160,6 +137,8 @@
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;
@ -201,6 +180,7 @@
} }
.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);
@ -223,26 +203,15 @@
gap: var(--space-4); gap: var(--space-4);
} }
@media (max-width: 760px) { @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 { .global-sidebar {
grid-column: 1;
grid-row: 1;
border-right: 0; border-right: 0;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
} }
.workspace-layout { .workspace-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: auto 1fr; grid-template-rows: auto auto 1fr;
width: 100%; width: 100%;
height: auto; height: auto;
min-height: 0; min-height: 0;
@ -258,11 +227,13 @@
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
} }
.workspace-topbar { .workspace-topbar {
grid-column: 1;
grid-row: 2;
padding: 0 var(--space-4); padding: 0 var(--space-4);
} }
.shell { .shell {
grid-column: 1; grid-column: 1;
grid-row: 2; grid-row: 3;
overflow: visible; overflow: visible;
padding: var(--space-5) var(--space-4); padding: var(--space-5) var(--space-4);
} }
@ -270,6 +241,8 @@
} }
@layer components { @layer components {
.global-sidebar { .global-sidebar {
grid-column: 1;
grid-row: 1 / 3;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
@ -282,7 +255,7 @@
} }
.workspace-sidebar { .workspace-sidebar {
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1 / 3;
align-self: stretch; align-self: stretch;
min-width: 0; min-width: 0;
min-height: 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( const globalSidebar = await Deno.readTextFile(
new URL("../sidebar/GlobalSidebar.svelte", import.meta.url), 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( const sidebar = await Deno.readTextFile(
new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url), 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( assert(
rootLayout.includes("GlobalSidebar") && rootLayout.includes("GlobalSidebar") &&
rootLayout.includes("global-sidebar-layout") && rootLayout.includes("WorkspaceSidebar") &&
rootLayout.includes("data.workspaceScoped") &&
rootLayout.includes("workspace-topbar") && rootLayout.includes("workspace-topbar") &&
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", "Root layout chrome should choose GlobalSidebar or WorkspaceSidebar while account navigation stays in the header",
); );
assert( assert(
globalSidebar.includes("Global") && 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", "Root default sidebar should contain only global navigation, not workspace-scoped sections",
); );
assert( assert(
workspaceLayout.includes("WorkspaceSidebar") && rootLayoutLoad.includes("params.workspaceId") &&
workspaceLayout.includes("workspace={data.workspace}") && rootLayoutLoad.includes("workspaceApiPath(workspaceId") &&
workspaceLayoutLoad.includes("workspaceApiPath(params.workspaceId"), rootLayoutLoad.includes("workspaceScoped: true"),
"Workspace sidebar and workspace-scoped data loading should live under /w/[workspaceId] layout", "Root layout load should provide workspace-scoped sidebar data when a workspace route is active",
); );
assert( assert(
rootLayoutLoad.includes('"/account"') && rootLayoutLoad.includes('"/login/device"'), rootLayoutLoad.includes('"/account"') && rootLayoutLoad.includes('"/login/device"'),

View File

@ -2,15 +2,30 @@
import { page } from '$app/state'; import { page } from '$app/state';
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte'; import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte'; import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.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 { data, children }: LayoutProps = $props();
let sidebarCollapsed = $state(false);
</script> </script>
<WorkspaceAlerts /> <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"> <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">
@ -21,12 +36,7 @@
</a> </a>
</nav> </nav>
</header> </header>
<div class:global-sidebar-layout={!data.workspaceScoped} class="app-shell"> <main class="shell">
{#if !data.workspaceScoped} {@render children()}
<GlobalSidebar currentPath={page.url.pathname} /> </main>
{/if}
<div class="app-content">
{@render children()}
</div>
</div>
</div> </div>

View File

@ -1,14 +1,26 @@
import { redirect } from "@sveltejs/kit"; import { redirect } from "@sveltejs/kit";
import { loadJson, workspaceRoute } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath, workspaceRoute } from "$lib/workspace/api/http";
import type { WorkspaceResponse } from "$lib/workspace/sidebar/types"; import type { RepositoryListResponse, 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 }) => {
if (params.workspaceId) { const workspaceId = params.workspaceId;
return { workspaceScoped: true }; 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"]); 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,
};
};