web: add global sidebar fallback
This commit is contained in:
parent
2bd69f9403
commit
308e1ffdbc
|
|
@ -133,6 +133,15 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.app-shell {
|
.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-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
@ -214,14 +223,31 @@
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.workspace-layout {
|
.app-layout {
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: auto auto 1fr;
|
|
||||||
width: 100vw;
|
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
overflow: visible;
|
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 {
|
.workspace-layout.sidebar-collapsed {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
@ -232,19 +258,28 @@
|
||||||
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: 3;
|
grid-row: 2;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: var(--space-5) var(--space-4);
|
padding: var(--space-5) var(--space-4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer components {
|
@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 {
|
.workspace-sidebar {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,9 @@ 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 globalSidebar = await Deno.readTextFile(
|
||||||
|
new URL("../sidebar/GlobalSidebar.svelte", import.meta.url),
|
||||||
|
);
|
||||||
const workspaceLayout = await Deno.readTextFile(
|
const workspaceLayout = await Deno.readTextFile(
|
||||||
new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url),
|
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",
|
"Auth model should stay on Backend auth APIs rather than workspace authorization APIs",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
|
rootLayout.includes("GlobalSidebar") &&
|
||||||
|
rootLayout.includes("global-sidebar-layout") &&
|
||||||
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"') &&
|
||||||
|
|
@ -433,6 +438,14 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
||||||
!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(
|
||||||
|
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(
|
assert(
|
||||||
workspaceLayout.includes("WorkspaceSidebar") &&
|
workspaceLayout.includes("WorkspaceSidebar") &&
|
||||||
workspaceLayout.includes("workspace={data.workspace}") &&
|
workspaceLayout.includes("workspace={data.workspace}") &&
|
||||||
|
|
|
||||||
30
web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte
Normal file
30
web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte
Normal 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>
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
<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 GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import type { LayoutProps } from './$types';
|
import type { LayoutProps } from './$types';
|
||||||
|
|
||||||
let { children }: LayoutProps = $props();
|
let { data, children }: LayoutProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WorkspaceAlerts />
|
<WorkspaceAlerts />
|
||||||
|
|
@ -19,7 +21,12 @@
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</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()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ export const prerender = false;
|
||||||
|
|
||||||
export const load: LayoutLoad = async ({ fetch, params, url }) => {
|
export const load: LayoutLoad = async ({ fetch, params, url }) => {
|
||||||
if (params.workspaceId) {
|
if (params.workspaceId) {
|
||||||
return {};
|
return { workspaceScoped: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { workspaceScoped: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = await loadJson<WorkspaceResponse>(fetch, "/api/workspace");
|
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);
|
const scopedPath = workspaceRoute(workspace.data.workspace_id);
|
||||||
throw redirect(307, `${scopedPath}${url.search}`);
|
throw redirect(307, `${scopedPath}${url.search}`);
|
||||||
}
|
}
|
||||||
return {};
|
return { workspaceScoped: false };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user