feat: add workspace switcher menu

This commit is contained in:
2026-08-21 11:30:23 +09:00
parent cd9f0f009e
commit ccb9d03865
9 changed files with 423 additions and 268 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test --allow-env=LOG,VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/tasks.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/sidebar/repository-nav.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts",
"test": "deno test --allow-read=src,test --allow-env=LOG,VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/tasks.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts",
"build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview"
},
@@ -1,47 +0,0 @@
<script lang="ts">
import { projectRepositoryNav } from './repository-nav';
import type { RepositoryListResponse } from './types';
type Props = {
repositories: RepositoryListResponse | null;
repositoriesError?: string | null;
currentPath?: string;
workspaceId: string;
};
let { repositories, repositoriesError = null, currentPath = '/', workspaceId }: Props = $props();
let navigation = $derived(projectRepositoryNav(repositories, currentPath, workspaceId));
</script>
<section class="nav-section" aria-labelledby="repositories-heading">
<div class="section-heading-row">
<h2 id="repositories-heading">repositories</h2>
<span class="section-count">{navigation.count}</span>
</div>
{#if repositoriesError}
<p class="nav-empty error">Repository registry unavailable.</p>
{:else if !repositories}
<p class="nav-empty">Loading repositories…</p>
{:else if navigation.items.length === 0}
<p class="nav-empty">No repositories configured.</p>
{#if navigation.diagnostics.length > 0}
<ul class="diagnostics" aria-label="Repository diagnostics">
{#each navigation.diagnostics as diagnostic}
<li><code>{diagnostic.code}</code>: {diagnostic.message}</li>
{/each}
</ul>
{/if}
{:else}
<ul class="nav-list" aria-label="Repositories">
{#each navigation.items as item (item.id)}
<li>
<a class="nav-item" class:active={item.active} href={item.href} aria-current={item.active ? 'page' : undefined}>
<span class="item-title">{item.title}</span>
<span class="item-meta">{item.meta}</span>
</a>
</li>
{/each}
</ul>
{/if}
</section>
@@ -1,86 +1,43 @@
<script lang="ts">
import { workspaceRoute } from '$lib/workspace/api/http';
import './sidebar.css';
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
import MemoryNavSection from './MemoryNavSection.svelte';
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
import TicketsNavSection from './TicketsNavSection.svelte';
import WorkersNavSection from './WorkersNavSection.svelte';
import WorkspaceSwitcher from './WorkspaceSwitcher.svelte';
import type { RepositoryListResponse, WorkspaceResponse } from './types';
import type { WorkspaceResponse } from './types';
type Props = {
workspace: WorkspaceResponse | null;
workspaceError?: string | null;
repositories?: RepositoryListResponse | null;
repositoriesError?: string | null;
currentPath?: string;
};
let {
workspace,
workspaceError = null,
repositories = null,
repositoriesError = null,
currentPath = '/'
}: Props = $props();
let { workspace, workspaceError = null, currentPath = '/' }: Props = $props();
let workspaceId = $derived(workspace?.workspace_id ?? '');
let homeHref = $derived(workspaceId ? workspaceRoute(workspaceId) : '/');
let settingsHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/settings') : '/settings');
</script>
<div class="workspace-sidebar">
<header class="sidebar-header">
<div class="sidebar-title-row">
<div class="workspace-label">
{#if workspace}
<div class="workspace-name">{workspace.display_name}</div>
<WorkspaceSwitcher
currentWorkspaceId={workspaceId}
currentWorkspaceName={workspace.display_name}
/>
{:else}
<div class="workspace-label">
<div class="workspace-name">Yoi workspace</div>
{#if workspaceError}
<p class="workspace-status error">Workspace summary unavailable.</p>
{:else}
<p class="workspace-status">Loading workspace…</p>
{/if}
</div>
{/if}
</div>
</div>
<div class="sidebar-actions-row">
<a
class="sidebar-icon-button"
href={homeHref}
aria-label="Open workspace overview"
title="Workspace overview"
>
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
<path
d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
/>
</svg>
</a>
<a
class="sidebar-icon-button"
href={settingsHref}
aria-label="Open Settings / Admin"
title="Settings / Admin"
>
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
<path
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
/>
<circle cx="12" cy="12" r="4" />
</svg>
</a>
</div>
</header>
{#if workspaceId}<WorkspaceSwitcher currentWorkspaceId={workspaceId} />{/if}
<nav class="sidebar-sections" aria-label="Workspace sections">
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} {workspaceId} />
<TicketsNavSection {currentPath} {workspaceId} />
<ObjectivesNavSection {currentPath} {workspaceId} />
<MemoryNavSection {currentPath} {workspaceId} />
@@ -1,55 +1,196 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { onMount } from "svelte";
import { onMount, tick } from "svelte";
import {
listWorkspaces,
type WorkspaceCatalogRecord,
} from "$lib/workspace/api/workspace-catalog";
import "$lib/workspace/styles/workspace-catalog.css";
let { currentWorkspaceId } = $props<{ currentWorkspaceId: string }>();
let {
currentWorkspaceId,
currentWorkspaceName,
}: {
currentWorkspaceId: string;
currentWorkspaceName: string;
} = $props();
let workspaces = $state<WorkspaceCatalogRecord[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
let error = $state("");
let open = $state(false);
let root = $state.raw<HTMLDivElement>();
let trigger = $state.raw<HTMLButtonElement>();
let menu = $state.raw<HTMLDivElement>();
onMount(async () => {
const menuWorkspaces = $derived.by(() => {
const entries = workspaces.map((workspace) => ({
workspace_id: workspace.workspace_id,
display_name: workspace.display_name,
}));
if (!entries.some((workspace) => workspace.workspace_id === currentWorkspaceId)) {
entries.unshift({
workspace_id: currentWorkspaceId,
display_name: currentWorkspaceName,
});
}
return entries;
});
async function loadWorkspaces() {
loading = true;
error = "";
try {
workspaces = await listWorkspaces(fetch);
} catch (cause) {
error = cause instanceof Error ? cause.message : String(cause);
error = cause instanceof Error ? cause.message : "Failed to load Workspaces.";
} finally {
loading = false;
}
});
async function switchWorkspace(event: Event) {
const workspaceId = (event.currentTarget as HTMLSelectElement).value;
if (!workspaceId || workspaceId === currentWorkspaceId) return;
await goto(`/w/${encodeURIComponent(workspaceId)}`);
}
function closeMenu() {
open = false;
}
async function openMenu(focus: "none" | "first" | "last" = "none") {
open = true;
if (focus === "none") return;
await tick();
const items = menu?.querySelectorAll<HTMLElement>("[role='menuitem']");
if (!items?.length) return;
items[focus === "first" ? 0 : items.length - 1]?.focus();
}
function toggleMenu() {
if (open) {
closeMenu();
} else {
void openMenu();
}
}
function handleTriggerKeydown(event: KeyboardEvent) {
if (event.key === "ArrowDown") {
event.preventDefault();
void openMenu("first");
} else if (event.key === "ArrowUp") {
event.preventDefault();
void openMenu("last");
}
}
function handleMenuKeydown(event: KeyboardEvent) {
if (event.key === "Escape") {
event.preventDefault();
closeMenu();
trigger?.focus();
return;
}
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key) || !menu) return;
const items = [...menu.querySelectorAll<HTMLElement>("[role='menuitem']")];
if (!items.length) return;
event.preventDefault();
const currentIndex = items.indexOf(document.activeElement as HTMLElement);
let nextIndex = currentIndex;
if (event.key === "Home") nextIndex = 0;
if (event.key === "End") nextIndex = items.length - 1;
if (event.key === "ArrowDown") nextIndex = (currentIndex + 1) % items.length;
if (event.key === "ArrowUp") {
nextIndex = (currentIndex - 1 + items.length) % items.length;
}
items[nextIndex]?.focus();
}
function handleDocumentPointerDown(event: PointerEvent) {
if (open && root && !root.contains(event.target as Node)) closeMenu();
}
onMount(() => {
document.addEventListener("pointerdown", handleDocumentPointerDown);
void loadWorkspaces();
return () => document.removeEventListener("pointerdown", handleDocumentPointerDown);
});
</script>
<div class="workspace-switcher">
<label for="workspace-switcher-select">Workspace</label>
<select
id="workspace-switcher-select"
value={currentWorkspaceId}
onchange={switchWorkspace}
disabled={loading}
aria-label="Switch Workspace"
<div class="workspace-menu" bind:this={root}>
<button
bind:this={trigger}
type="button"
class="workspace-menu-trigger"
aria-haspopup="menu"
aria-expanded={open}
aria-controls="workspace-menu-popover"
onclick={toggleMenu}
onkeydown={handleTriggerKeydown}
>
{#if !workspaces.some((workspace) => workspace.workspace_id === currentWorkspaceId)}
<option value={currentWorkspaceId}>
{loading ? "Loading current Workspace…" : "Current Workspace unavailable"}
</option>
{/if}
{#each workspaces as workspace (workspace.workspace_id)}
<option value={workspace.workspace_id}>{workspace.display_name}</option>
{/each}
</select>
<div class="workspace-switcher-actions">
<a href="/">All Workspaces</a>
<a href="/#workspace-create-title">Create</a>
<span>{currentWorkspaceName}</span>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m8 10 4 4 4-4" />
</svg>
</button>
{#if open}
<div
bind:this={menu}
id="workspace-menu-popover"
class="workspace-menu-popover"
role="menu"
tabindex="-1"
aria-label="Workspace menu"
onkeydown={handleMenuKeydown}
>
<a
class="workspace-menu-item"
href={`/w/${encodeURIComponent(currentWorkspaceId)}/settings`}
role="menuitem"
onclick={closeMenu}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z" />
<path d="M19.4 15a1.8 1.8 0 0 0 .36 1.98l.06.06-2.12 2.12-.06-.06a1.8 1.8 0 0 0-1.98-.36 1.8 1.8 0 0 0-1.1 1.64v.12h-3v-.12a1.8 1.8 0 0 0-1.1-1.64 1.8 1.8 0 0 0-1.98.36l-.06.06-2.12-2.12.06-.06A1.8 1.8 0 0 0 6.6 15a1.8 1.8 0 0 0-1.64-1.1h-.12v-3h.12A1.8 1.8 0 0 0 6.6 9a1.8 1.8 0 0 0-.36-1.98l-.06-.06 2.12-2.12.06.06A1.8 1.8 0 0 0 10.34 5a1.8 1.8 0 0 0 1.1-1.64v-.12h3v.12A1.8 1.8 0 0 0 15.54 5a1.8 1.8 0 0 0 1.98-.36l.06-.06 2.12 2.12-.06.06A1.8 1.8 0 0 0 19.4 9a1.8 1.8 0 0 0 1.64 1.1h.12v3h-.12A1.8 1.8 0 0 0 19.4 15Z" />
</svg>
<span>Settings</span>
</a>
<div class="workspace-menu-separator" role="separator"></div>
<div class="workspace-menu-heading">
<span>Workspaces</span>
<a
class="workspace-menu-add"
href="/#workspace-create-title"
role="menuitem"
aria-label="Create Workspace"
title="Create Workspace"
onclick={closeMenu}
>+</a>
</div>
{#if error}<span class="workspace-switcher-error">Selector unavailable: {error}</span>{/if}
<div class="workspace-menu-list">
{#each menuWorkspaces as workspace (workspace.workspace_id)}
<a
class:current={workspace.workspace_id === currentWorkspaceId}
class="workspace-menu-item workspace-menu-workspace"
href={`/w/${encodeURIComponent(workspace.workspace_id)}`}
role="menuitem"
aria-current={workspace.workspace_id === currentWorkspaceId ? "page" : undefined}
onclick={closeMenu}
>
<span>{workspace.display_name}</span>
{#if workspace.workspace_id === currentWorkspaceId}
<svg class="workspace-menu-check" viewBox="0 0 24 24" aria-hidden="true">
<path d="m6 12 4 4 8-8" />
</svg>
{/if}
</a>
{/each}
</div>
{#if loading}
<p class="workspace-menu-status">Loading Workspaces…</p>
{:else if error}
<p class="workspace-menu-status error">{error}</p>
{/if}
</div>
{/if}
</div>
@@ -1,75 +0,0 @@
import { projectRepositoryNav } from "./repository-nav.ts";
import type { RepositoryListResponse } from "./types.ts";
declare const Deno: {
test(name: string, fn: () => void): void;
};
function assertEquals<T>(actual: T, expected: T): void {
const actualJson = JSON.stringify(actual);
const expectedJson = JSON.stringify(expected);
if (actualJson !== expectedJson) {
throw new Error(`Expected ${expectedJson}, got ${actualJson}`);
}
}
function repositories(
items: RepositoryListResponse["items"],
): RepositoryListResponse {
return {
workspace_id: "workspace-1",
items,
source: "workspace_backend_config",
diagnostics: [],
};
}
Deno.test("repository nav does not invent main for an empty registry", () => {
const projection = projectRepositoryNav(
{
workspace_id: "workspace-1",
items: [],
source: "workspace_backend_config",
diagnostics: [
{
code: "repository_config_empty",
severity: "warning",
message: "No repositories configured",
},
],
},
"/w/workspace-1",
"workspace-1",
);
assertEquals(projection.count, 0);
assertEquals(projection.items, []);
assertEquals(projection.diagnostics[0].code, "repository_config_empty");
});
Deno.test("repository nav links configured non-main repository ids", () => {
const projection = projectRepositoryNav(
repositories([
{
id: "infra",
display_name: "Infrastructure",
kind: "git",
provider: "git",
record_authority: "workspace-backend-config",
git: null,
diagnostics: [],
},
]),
"/w/workspace-1/repositories/infra",
"workspace-1",
);
assertEquals(projection.count, 1);
assertEquals(projection.items[0], {
id: "infra",
title: "Infrastructure",
href: "/w/workspace-1/repositories/infra",
meta: "git repository · read-only",
active: true,
});
});
@@ -1,41 +0,0 @@
import { workspaceRoute } from "$lib/workspace/api/http";
import type { Diagnostic, RepositoryListResponse } from "./types";
export type RepositoryNavItem = {
id: string;
title: string;
href: string;
meta: string;
active: boolean;
};
export type RepositoryNavProjection = {
count: number;
items: RepositoryNavItem[];
diagnostics: Diagnostic[];
};
export function projectRepositoryNav(
repositories: RepositoryListResponse | null,
currentPath: string,
workspaceId: string,
): RepositoryNavProjection {
const summaries = repositories?.items ?? [];
return {
count: summaries.length,
diagnostics: repositories?.diagnostics ?? [],
items: summaries.map((repository) => {
const href = workspaceRoute(
workspaceId,
`/repositories/${encodeURIComponent(repository.id)}`,
);
return {
id: repository.id,
title: repository.display_name || repository.id,
href,
meta: `${repository.provider} repository · read-only`,
active: currentPath === href || currentPath.startsWith(`${href}/`),
};
}),
};
}
@@ -42,8 +42,7 @@
margin-bottom: var(--space-2);
min-width: 0;
}
.sidebar-control-row,
.sidebar-actions-row {
.sidebar-control-row {
display: flex;
justify-content: flex-end;
gap: var(--space-1);
@@ -57,12 +56,6 @@
padding-inline: var(--space-2);
padding-top: 0;
}
.sidebar-title-row {
display: flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
}
.workspace-label {
flex: 1 1 auto;
min-width: 0;
@@ -80,6 +73,157 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.workspace-menu {
position: relative;
min-width: 0;
}
.workspace-menu-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
width: 100%;
min-width: 0;
border: 0;
border-radius: var(--radius-soft);
padding: 0.45rem 0.6rem;
background: transparent;
color: var(--text-strong);
font: inherit;
font-size: 1.05rem;
font-weight: 800;
line-height: 1.25;
text-align: left;
cursor: pointer;
}
.workspace-menu-trigger > span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workspace-menu-trigger > svg,
.workspace-menu-item > svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.workspace-menu-trigger > svg {
transition: transform 140ms ease;
}
.workspace-menu-trigger[aria-expanded="true"] > svg {
transform: rotate(180deg);
}
.workspace-menu-trigger:hover,
.workspace-menu-trigger:focus-visible {
background: var(--interactive-hover);
}
.workspace-menu-trigger:focus-visible,
.workspace-menu-add:focus-visible,
.workspace-menu-item:focus-visible {
outline: 1px solid var(--accent);
outline-offset: 1px;
}
.workspace-menu-popover {
position: absolute;
z-index: 20;
inset-inline: 0;
top: calc(100% + var(--space-1));
display: grid;
gap: var(--space-1);
min-width: 0;
max-height: min(26rem, calc(100vh - 6rem));
overflow-y: auto;
border: 1px solid var(--line);
border-radius: calc(var(--radius-soft) + 0.2rem);
padding: var(--space-2);
background: var(--bg-raised);
box-shadow: var(--shadow-soft);
}
.workspace-menu-item {
display: flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
border-radius: var(--radius-soft);
padding: var(--space-2);
color: var(--text-strong);
font-size: 0.82rem;
font-weight: 650;
line-height: 1.3;
text-decoration: none;
}
.workspace-menu-item > span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workspace-menu-item:hover,
.workspace-menu-item:focus-visible,
.workspace-menu-item.current {
background: var(--interactive-hover);
}
.workspace-menu-item.current {
color: var(--accent);
}
.workspace-menu-separator {
height: 1px;
margin: var(--space-1) calc(-1 * var(--space-2));
background: var(--line);
}
.workspace-menu-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-1) var(--space-2) 0;
color: var(--text-faint);
font-size: 0.68rem;
font-weight: 750;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.workspace-menu-add {
display: grid;
width: 1.5rem;
height: 1.5rem;
place-items: center;
border-radius: var(--radius-soft);
color: var(--text-muted);
font-size: 1rem;
line-height: 1;
text-decoration: none;
}
.workspace-menu-add:hover,
.workspace-menu-add:focus-visible {
background: var(--interactive-hover);
color: var(--text-strong);
}
.workspace-menu-list {
display: grid;
gap: 1px;
}
.workspace-menu-workspace {
justify-content: space-between;
}
.workspace-menu-check {
margin-left: auto;
}
.workspace-menu-status {
margin: var(--space-1) var(--space-2);
color: var(--text-muted);
font-size: 0.72rem;
line-height: 1.35;
}
.workspace-menu-status.error {
color: var(--danger);
}
.workspace-status {
margin: var(--space-1) 0 0;
color: var(--text-muted);
@@ -91,7 +235,6 @@
.error {
color: var(--danger);
}
.sidebar-icon-button,
.sidebar-fold-button {
flex: 0 0 auto;
display: grid;
@@ -118,8 +261,6 @@
stroke-linecap: round;
stroke-linejoin: round;
}
.sidebar-icon-button:hover,
.sidebar-icon-button:focus-visible,
.sidebar-fold-button:hover,
.sidebar-fold-button:focus-visible {
background: var(--interactive-hover);
@@ -0,0 +1,81 @@
declare const Deno: {
test(name: string, fn: () => Promise<void> | void): void;
readTextFile(path: string | URL): Promise<string>;
};
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
const switcherSource = await Deno.readTextFile(
new URL("./WorkspaceSwitcher.svelte", import.meta.url),
);
const sidebarSource = await Deno.readTextFile(
new URL("./WorkspaceSidebar.svelte", import.meta.url),
);
Deno.test("workspace name opens the settings and workspace menu", () => {
assert(
switcherSource.includes('class="workspace-menu-trigger"'),
"missing name trigger",
);
assert(
switcherSource.includes('aria-haspopup="menu"'),
"trigger is not a menu button",
);
assert(
switcherSource.includes("<span>Settings</span>"),
"missing Settings action",
);
assert(
switcherSource.includes("<span>Workspaces</span>"),
"missing Workspaces heading",
);
assert(
switcherSource.includes('aria-label="Create Workspace"'),
"missing create action",
);
assert(
switcherSource.includes("currentWorkspaceName"),
"trigger does not use the name",
);
assert(!switcherSource.includes("<select"), "legacy select switcher remains");
});
Deno.test("workspace menu lists catalog entries and marks the current workspace", () => {
assert(
switcherSource.includes("listWorkspaces(fetch)"),
"catalog is not loaded",
);
assert(
switcherSource.includes("{#each menuWorkspaces as workspace"),
"Workspace catalog is not rendered",
);
assert(
switcherSource.includes("aria-current="),
"current Workspace is not identified",
);
assert(
switcherSource.includes("workspace.display_name"),
"Workspace name is not rendered",
);
});
Deno.test("workspace sidebar uses the workspace name menu as its header", () => {
assert(
sidebarSource.includes("<WorkspaceSwitcher"),
"sidebar omits the menu",
);
assert(
sidebarSource.includes("currentWorkspaceName={workspace.display_name}"),
"sidebar does not pass the current Workspace name",
);
assert(
!sidebarSource.includes('class="sidebar-actions-row"'),
"old action row remains",
);
assert(
!sidebarSource.includes("RepositoriesNavSection"),
"Repositories should not be rendered in the Workspace sidebar",
);
});
@@ -26,8 +26,6 @@
<WorkspaceSidebar
workspace={data.workspace ?? null}
workspaceError={data.workspaceError ?? null}
repositories={data.repositories ?? null}
repositoriesError={data.repositoriesError ?? null}
currentPath={page.url.pathname}
/>
{/snippet}