feat: list accessible workspaces in global sidebar

This commit is contained in:
2026-09-02 19:22:45 +09:00
parent 8344921b65
commit d996822957
4 changed files with 108 additions and 26 deletions
@@ -812,6 +812,12 @@ 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 globalNavSections = await Deno.readTextFile(
new URL("../sidebar/GlobalNavSections.svelte", import.meta.url),
);
const workspaceCatalogPage = await Deno.readTextFile(
new URL("./../../../routes/+page.svelte", import.meta.url),
);
const sidebarFrame = await Deno.readTextFile( const sidebarFrame = await Deno.readTextFile(
new URL("../sidebar/SidebarFrame.svelte", import.meta.url), new URL("../sidebar/SidebarFrame.svelte", import.meta.url),
); );
@@ -874,13 +880,24 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
"Root layout chrome should keep GlobalSidebar as the root slot owner while account navigation stays in the header", "Root layout chrome should keep GlobalSidebar as the root slot owner while account navigation stays in the header",
); );
assert( assert(
globalSidebar.includes('aria-label="Global pages"') && globalSidebar.includes("GlobalNavSections") &&
!globalSidebar.includes('<p class="sidebar-section-label">') && globalNavSections.includes('aria-label="Global pages"') &&
globalSidebar.includes("/account") && !globalNavSections.includes('<p class="sidebar-section-label">') &&
globalSidebar.includes("/login/device") && globalNavSections.includes('"/account"') &&
!globalSidebar.includes("Tickets") && globalNavSections.includes('"/login/device"') &&
!globalSidebar.includes("Repositories"), !globalNavSections.includes('label: "Workspaces"') &&
"Root default sidebar should contain only global navigation, not workspace-scoped sections", globalNavSections.includes("sidebar-nav-section--category") &&
globalNavSections.includes("global-workspaces-heading") &&
globalNavSections.includes("workspaces") &&
globalNavSections.includes("workspace.display_name") &&
globalNavSections.includes("workspaceHref(workspace.workspace_id)") &&
workspaceCatalogPage.includes("SidebarOverride") &&
workspaceCatalogPage.includes("sidebar={homeSidebar}") &&
workspaceCatalogPage.includes("GlobalNavSections") &&
workspaceCatalogPage.includes("{workspaces}") &&
!globalNavSections.includes("Tickets") &&
!globalNavSections.includes("Repositories"),
"Root page sidebar should replace the Workspaces button with a categorized accessible Workspace list below the remaining global navigation",
); );
assert( assert(
workspaceLayout.includes("{#snippet workspaceSidebar()}") && workspaceLayout.includes("{#snippet workspaceSidebar()}") &&
@@ -0,0 +1,70 @@
<script lang="ts">
import type { WorkspaceCatalogRecord } from "$lib/workspace/api/workspace-catalog";
type Props = {
currentPath: string;
workspaces?: WorkspaceCatalogRecord[] | null;
workspaceError?: string | null;
};
let {
currentPath,
workspaces = null,
workspaceError = null,
}: Props = $props();
const globalItems = [
{ href: "/#workspace-create-title", label: "Create Workspace" },
{ href: "/account", label: "Account" },
{ href: "/login/device", label: "Device Login" },
];
function workspaceHref(workspaceId: string): string {
return `/w/${encodeURIComponent(workspaceId)}`;
}
</script>
<nav class="sidebar-sections" aria-label="Global pages">
<section class="sidebar-nav-section">
<div class="sidebar-list">
{#each globalItems 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}
</div>
</section>
{#if workspaces !== null}
<section class="sidebar-nav-section sidebar-nav-section--category" aria-labelledby="global-workspaces-heading">
<h2 id="global-workspaces-heading" class="sidebar-nav-section__header">workspaces</h2>
{#if workspaceError}
<p class="workspace-status error">Workspace list unavailable.</p>
{/if}
{#if workspaces.length > 0}
<div class="sidebar-list">
{#each workspaces as workspace (workspace.workspace_id)}
{@const href = workspaceHref(workspace.workspace_id)}
<a
class="sidebar-link"
class:active={currentPath === href}
{href}
aria-current={currentPath === href ? "page" : undefined}
>
<span>{workspace.display_name}</span>
</a>
{/each}
</div>
{:else if !workspaceError}
<p class="workspace-status">No accessible Workspaces.</p>
{/if}
</section>
{/if}
</nav>
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { SidebarSnippet } from './context'; import type { SidebarSnippet } from './context';
import GlobalNavSections from './GlobalNavSections.svelte';
import './sidebar.css'; import './sidebar.css';
type Props = { type Props = {
@@ -8,13 +9,6 @@
}; };
const { currentPath, content = null }: Props = $props(); const { currentPath, content = null }: Props = $props();
const items = [
{ href: '/', label: 'Workspaces' },
{ href: '/#workspace-create-title', label: 'Create Workspace' },
{ href: '/account', label: 'Account' },
{ href: '/login/device', label: 'Device Login' },
];
</script> </script>
{#if content} {#if content}
@@ -22,18 +16,7 @@
{:else} {:else}
<div class="global-sidebar" aria-label="Global navigation"> <div class="global-sidebar" aria-label="Global navigation">
<div class="global-sidebar-section"> <div class="global-sidebar-section">
<nav class="sidebar-list" aria-label="Global pages"> <GlobalNavSections {currentPath} />
{#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> </div>
</div> </div>
{/if} {/if}
+12
View File
@@ -8,6 +8,8 @@
type CreateWorkspaceRequest, type CreateWorkspaceRequest,
type WorkspaceCatalogItem, type WorkspaceCatalogItem,
} from "$lib/workspace/api/workspace-catalog"; } from "$lib/workspace/api/workspace-catalog";
import GlobalNavSections from "$lib/workspace/sidebar/GlobalNavSections.svelte";
import SidebarOverride from "$lib/workspace/sidebar/SidebarOverride.svelte";
import "$lib/workspace/styles/workspace-catalog.css"; import "$lib/workspace/styles/workspace-catalog.css";
let { data } = $props(); let { data } = $props();
@@ -87,6 +89,16 @@
} }
</script> </script>
{#snippet homeSidebar()}
<GlobalNavSections
currentPath="/"
{workspaces}
workspaceError={catalogError}
/>
{/snippet}
<SidebarOverride sidebar={homeSidebar} />
<svelte:head> <svelte:head>
<title>Workspaces · Yoi</title> <title>Workspaces · Yoi</title>
</svelte:head> </svelte:head>