web: add global sidebar fallback

This commit is contained in:
Keisuke Hirata 2026-07-23 19:23:02 +09:00
parent 2bd69f9403
commit 308e1ffdbc
No known key found for this signature in database
5 changed files with 99 additions and 14 deletions

View File

@ -133,6 +133,15 @@
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;
@ -214,14 +223,31 @@
gap: var(--space-4);
}
@media (max-width: 760px) {
.workspace-layout {
grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr;
width: 100vw;
.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 {
border-right: 0;
border-bottom: 1px solid var(--line);
}
.workspace-layout {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
width: 100%;
height: auto;
min-height: 0;
overflow: visible;
}
.workspace-layout.sidebar-collapsed {
grid-template-columns: 1fr;
}
@ -232,19 +258,28 @@
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: 3;
grid-row: 2;
overflow: visible;
padding: var(--space-5) var(--space-4);
}
}
}
@layer components {
.global-sidebar {
min-width: 0;
min-height: 0;
overflow-y: auto;
padding: var(--space-4) var(--space-3);
border-right: 1px solid var(--line);
}
.global-sidebar-section {
display: grid;
gap: var(--space-2);
}
.workspace-sidebar {
grid-column: 1;
grid-row: 1;

View File

@ -390,6 +390,9 @@ 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 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),
);
@ -424,6 +427,8 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
"Auth model should stay on Backend auth APIs rather than workspace authorization APIs",
);
assert(
rootLayout.includes("GlobalSidebar") &&
rootLayout.includes("global-sidebar-layout") &&
rootLayout.includes("workspace-topbar") &&
rootLayout.includes("topbar-icon-button") &&
rootLayout.includes('href="/account"') &&
@ -433,6 +438,14 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
!sidebar.includes("Open Account"),
"Account navigation should live in the global layout header, not the workspace sidebar",
);
assert(
globalSidebar.includes("Global") &&
globalSidebar.includes("/account") &&
globalSidebar.includes("/login/device") &&
!globalSidebar.includes("Tickets") &&
!globalSidebar.includes("Repositories"),
"Root default sidebar should contain only global navigation, not workspace-scoped sections",
);
assert(
workspaceLayout.includes("WorkspaceSidebar") &&
workspaceLayout.includes("workspace={data.workspace}") &&

View File

@ -0,0 +1,30 @@
<script lang="ts">
type Props = {
currentPath: string;
};
const { currentPath }: Props = $props();
const items = [
{ href: '/account', label: 'Account' },
{ href: '/login/device', label: 'Device Login' },
];
</script>
<aside class="global-sidebar" aria-label="Global navigation">
<div class="global-sidebar-section">
<p class="sidebar-section-label">Global</p>
<nav class="sidebar-list" aria-label="Global pages">
{#each items as item}
<a
class="sidebar-link"
class:active={currentPath === item.href}
href={item.href}
aria-current={currentPath === item.href ? 'page' : undefined}
>
<span>{item.label}</span>
</a>
{/each}
</nav>
</div>
</aside>

View File

@ -1,9 +1,11 @@
<script lang="ts">
import { page } from '$app/state';
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte';
import '../app.css';
import type { LayoutProps } from './$types';
let { children }: LayoutProps = $props();
let { data, children }: LayoutProps = $props();
</script>
<WorkspaceAlerts />
@ -19,7 +21,12 @@
</a>
</nav>
</header>
<div class="app-shell">
<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>
</div>

View File

@ -8,12 +8,12 @@ export const prerender = false;
export const load: LayoutLoad = async ({ fetch, params, url }) => {
if (params.workspaceId) {
return {};
return { workspaceScoped: true };
}
const publicRoutes = new Set(["/account", "/login/device"]);
if (publicRoutes.has(url.pathname)) {
return {};
return { workspaceScoped: false };
}
const workspace = await loadJson<WorkspaceResponse>(fetch, "/api/workspace");
@ -21,5 +21,5 @@ export const load: LayoutLoad = async ({ fetch, params, url }) => {
const scopedPath = workspaceRoute(workspace.data.workspace_id);
throw redirect(307, `${scopedPath}${url.search}`);
}
return {};
return { workspaceScoped: false };
};