fix: fence shared chrome overrides by route
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
"dev": "deno run -A npm:vite@7.2.7 dev",
|
"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",
|
"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",
|
"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,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.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-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.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/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
|
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.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-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.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/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||||
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
|
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
|
||||||
"build": "deno run -A npm:vite@7.2.7 build",
|
"build": "deno run -A npm:vite@7.2.7 build",
|
||||||
"preview": "deno run -A npm:vite@7.2.7 preview",
|
"preview": "deno run -A npm:vite@7.2.7 preview",
|
||||||
|
|||||||
@@ -179,6 +179,13 @@ Deno.test("design lab demonstrates nested sidebar slots without flattening level
|
|||||||
workspaceLayout.includes("content={sidebarContent}"),
|
workspaceLayout.includes("content={sidebarContent}"),
|
||||||
"Workspace design-lab layout must expose its child override slot",
|
"Workspace design-lab layout must expose its child override slot",
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
workspaceLayout.includes(
|
||||||
|
"ownsRoutePath(designLabBasePath, page.url.pathname)",
|
||||||
|
) &&
|
||||||
|
workspaceLayout.includes("active={ownsCurrentRoute}"),
|
||||||
|
"Workspace design-lab chrome must unregister outside its owned route",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
!workspaceLayout.includes("<GlobalNavSections"),
|
!workspaceLayout.includes("<GlobalNavSections"),
|
||||||
"Workspace design-lab layout must not flatten Global navigation",
|
"Workspace design-lab layout must not flatten Global navigation",
|
||||||
@@ -200,6 +207,11 @@ Deno.test("design lab demonstrates nested sidebar slots without flattening level
|
|||||||
settingsLayout.includes("controller={parentSidebarController}"),
|
settingsLayout.includes("controller={parentSidebarController}"),
|
||||||
"Settings design-lab layout must register into its parent slot",
|
"Settings design-lab layout must register into its parent slot",
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
settingsLayout.includes("ownsRoutePath(`${designLabBasePath}/settings`") &&
|
||||||
|
settingsLayout.includes("active={ownsCurrentRoute}"),
|
||||||
|
"Settings design-lab chrome must unregister outside its owned route",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
!settingsLayout.includes("<WorkspaceSidebar"),
|
!settingsLayout.includes("<WorkspaceSidebar"),
|
||||||
"Settings design-lab layout must not duplicate WorkspaceSidebar",
|
"Settings design-lab layout must not duplicate WorkspaceSidebar",
|
||||||
@@ -1011,6 +1023,9 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
|||||||
const sidebarOverride = await Deno.readTextFile(
|
const sidebarOverride = await Deno.readTextFile(
|
||||||
new URL("../sidebar/SidebarOverride.svelte", import.meta.url),
|
new URL("../sidebar/SidebarOverride.svelte", import.meta.url),
|
||||||
);
|
);
|
||||||
|
const headerOverride = await Deno.readTextFile(
|
||||||
|
new URL("../header/HeaderOverride.svelte", 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),
|
||||||
);
|
);
|
||||||
@@ -1104,10 +1119,20 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
|||||||
"Sidebar styles should define their layer order before component rules so base link styles do not win by import order",
|
"Sidebar styles should define their layer order before component rules so base link styles do not win by import order",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
|
sidebarOverride.includes("if (!active) return") &&
|
||||||
sidebarOverride.includes("controller.registerSidebar(sidebar)") &&
|
sidebarOverride.includes("controller.registerSidebar(sidebar)") &&
|
||||||
|
workspaceLayout.includes("ownsRoutePath(workspaceRoute(workspaceId)") &&
|
||||||
|
workspaceLayout.includes("active={ownsCurrentRoute}") &&
|
||||||
rootLayout.includes("createOverrideStack<SidebarSnippet>") &&
|
rootLayout.includes("createOverrideStack<SidebarSnippet>") &&
|
||||||
rootLayout.includes("registerSidebar: sidebarOverrides.register"),
|
rootLayout.includes("registerSidebar: sidebarOverrides.register"),
|
||||||
"SidebarOverride should register a nested sidebar whose cleanup restores the parent override",
|
"Sidebar overrides should register only while their route owns the shared chrome slot",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
headerOverride.includes("if (!active) return") &&
|
||||||
|
headerOverride.includes("controller.registerContent(content)") &&
|
||||||
|
rootLayout.includes("createOverrideStack<HeaderSnippet>") &&
|
||||||
|
rootLayout.includes("registerContent: headerOverrides.register"),
|
||||||
|
"Header overrides should share the route-scoped LIFO registration contract",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
rootLayoutLoad.includes("export const load") &&
|
rootLayoutLoad.includes("export const load") &&
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { getHeaderController } from './context';
|
import { getHeaderController } from './context';
|
||||||
|
|
||||||
let { content }: { content: Snippet<[]> } = $props();
|
let {
|
||||||
|
content,
|
||||||
|
active = true,
|
||||||
|
}: {
|
||||||
|
content: Snippet<[]>;
|
||||||
|
active?: boolean;
|
||||||
|
} = $props();
|
||||||
const controller = getHeaderController();
|
const controller = getHeaderController();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
controller.content = content;
|
if (!active) return;
|
||||||
return () => {
|
return controller.registerContent(content);
|
||||||
if (controller.content === content) controller.content = null;
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import type { Snippet } from "svelte";
|
|||||||
|
|
||||||
const HEADER_CONTEXT_KEY = Symbol("workspace-header");
|
const HEADER_CONTEXT_KEY = Symbol("workspace-header");
|
||||||
|
|
||||||
export type HeaderContent = Snippet<[]> | null;
|
export type HeaderSnippet = Snippet<[]>;
|
||||||
export type HeaderController = {
|
export type HeaderController = {
|
||||||
content: HeaderContent;
|
registerContent(content: HeaderSnippet): () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function provideHeaderController(controller: HeaderController): void {
|
export function provideHeaderController(controller: HeaderController): void {
|
||||||
|
|||||||
@@ -8,10 +8,18 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
sidebar: SidebarSnippet;
|
sidebar: SidebarSnippet;
|
||||||
controller?: SidebarController;
|
controller?: SidebarController;
|
||||||
|
active?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const inheritedController = getSidebarController();
|
const inheritedController = getSidebarController();
|
||||||
const { sidebar, controller = inheritedController }: Props = $props();
|
const {
|
||||||
|
sidebar,
|
||||||
|
controller = inheritedController,
|
||||||
|
active = true,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
$effect(() => controller.registerSidebar(sidebar));
|
$effect(() => {
|
||||||
|
if (!active) return;
|
||||||
|
return controller.registerSidebar(sidebar);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import './sidebar.css';
|
import './sidebar.css';
|
||||||
import { workspaceRoute } from '$lib/workspace/api/http';
|
import { workspaceRoute } from '$lib/workspace/api/http';
|
||||||
|
import { ownsRoutePath } from './route-ownership';
|
||||||
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
||||||
import MemoryNavSection from './MemoryNavSection.svelte';
|
import MemoryNavSection from './MemoryNavSection.svelte';
|
||||||
import MergeRequestsNavSection from './MergeRequestsNavSection.svelte';
|
import MergeRequestsNavSection from './MergeRequestsNavSection.svelte';
|
||||||
@@ -50,11 +51,11 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="workspace-sidebar-shortcut"
|
class="workspace-sidebar-shortcut"
|
||||||
class:active={currentPath.startsWith(workspaceSettingsHref)}
|
class:active={ownsRoutePath(workspaceSettingsHref, currentPath)}
|
||||||
href={workspaceSettingsHref}
|
href={workspaceSettingsHref}
|
||||||
aria-label="Workspace settings"
|
aria-label="Workspace settings"
|
||||||
title="Workspace settings"
|
title="Workspace settings"
|
||||||
aria-current={currentPath.startsWith(workspaceSettingsHref) ? 'page' : undefined}
|
aria-current={ownsRoutePath(workspaceSettingsHref, currentPath) ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M4 5h16M4 12h16M4 19h16"></path>
|
<path d="M4 5h16M4 12h16M4 19h16"></path>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import { cleanup, render, waitFor } from "@testing-library/svelte";
|
||||||
|
import { afterEach, expect, test, vi } from "vitest";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import HeaderOverride from "../header/HeaderOverride.svelte";
|
||||||
|
import SidebarOverride from "./SidebarOverride.svelte";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => {
|
||||||
|
const disposeHeader = vi.fn();
|
||||||
|
const disposeSidebar = vi.fn();
|
||||||
|
return {
|
||||||
|
disposeHeader,
|
||||||
|
disposeSidebar,
|
||||||
|
registerHeader: vi.fn(() => disposeHeader),
|
||||||
|
registerSidebar: vi.fn(() => disposeSidebar),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("./context", () => ({
|
||||||
|
getSidebarController: () => ({
|
||||||
|
registerSidebar: mocks.registerSidebar,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../header/context", () => ({
|
||||||
|
getHeaderController: () => ({
|
||||||
|
registerContent: mocks.registerHeader,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const firstSnippet = (() => {}) as Snippet<[]>;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
mocks.disposeHeader.mockClear();
|
||||||
|
mocks.disposeSidebar.mockClear();
|
||||||
|
mocks.registerHeader.mockClear();
|
||||||
|
mocks.registerHeader.mockReturnValue(mocks.disposeHeader);
|
||||||
|
mocks.registerSidebar.mockClear();
|
||||||
|
mocks.registerSidebar.mockReturnValue(mocks.disposeSidebar);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sidebar override registers only while its route is active", async () => {
|
||||||
|
const view = render(SidebarOverride, {
|
||||||
|
sidebar: firstSnippet,
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.registerSidebar).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await view.rerender({ sidebar: firstSnippet, active: true });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mocks.registerSidebar).toHaveBeenCalledOnce();
|
||||||
|
expect(mocks.registerSidebar).toHaveBeenCalledWith(firstSnippet);
|
||||||
|
});
|
||||||
|
|
||||||
|
await view.rerender({ sidebar: firstSnippet, active: false });
|
||||||
|
await waitFor(() => expect(mocks.disposeSidebar).toHaveBeenCalledOnce());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("header override registers only while its route is active", async () => {
|
||||||
|
const view = render(HeaderOverride, {
|
||||||
|
content: firstSnippet,
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.registerHeader).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await view.rerender({ content: firstSnippet, active: true });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mocks.registerHeader).toHaveBeenCalledOnce();
|
||||||
|
expect(mocks.registerHeader).toHaveBeenCalledWith(firstSnippet);
|
||||||
|
});
|
||||||
|
|
||||||
|
await view.rerender({ content: firstSnippet, active: false });
|
||||||
|
await waitFor(() => expect(mocks.disposeHeader).toHaveBeenCalledOnce());
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
declare const Deno: {
|
||||||
|
test(name: string, fn: () => void): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
import { ownsRoutePath } from "./route-ownership.ts";
|
||||||
|
|
||||||
|
function assertEquals(actual: unknown, expected: unknown): void {
|
||||||
|
if (!Object.is(actual, expected)) {
|
||||||
|
throw new Error(`Expected ${String(expected)}, got ${String(actual)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("route ownership includes only the base route and its descendants", () => {
|
||||||
|
assertEquals(
|
||||||
|
ownsRoutePath(
|
||||||
|
"/design-lab/workspace-web-ux",
|
||||||
|
"/design-lab/workspace-web-ux",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
ownsRoutePath(
|
||||||
|
"/design-lab/workspace-web-ux",
|
||||||
|
"/design-lab/workspace-web-ux/settings",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
ownsRoutePath(
|
||||||
|
"/design-lab/workspace-web-ux",
|
||||||
|
"/w/0197a949-4b6b-7f2a-9d9a-1f87e3a4c5b6/workers",
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("route ownership rejects prefix collisions and empty authorities", () => {
|
||||||
|
assertEquals(ownsRoutePath("/w/alpha", "/w/alphabet/workers"), false);
|
||||||
|
assertEquals(
|
||||||
|
ownsRoutePath(
|
||||||
|
"/design-lab/workspace-web-ux/settings",
|
||||||
|
"/design-lab/workspace-web-ux/settings-preview",
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assertEquals(ownsRoutePath("", "/w/alpha"), false);
|
||||||
|
assertEquals(ownsRoutePath("/w/alpha", ""), false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export function ownsRoutePath(basePath: string, currentPath: string): boolean {
|
||||||
|
if (!basePath || !currentPath) return false;
|
||||||
|
return currentPath === basePath || currentPath.startsWith(`${basePath}/`);
|
||||||
|
}
|
||||||
@@ -3,7 +3,11 @@
|
|||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
|
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
|
||||||
import Bevel from '$lib/workspace/ui/Bevel.svelte';
|
import Bevel from '$lib/workspace/ui/Bevel.svelte';
|
||||||
import { provideHeaderController, type HeaderController } from '$lib/workspace/header/context';
|
import {
|
||||||
|
provideHeaderController,
|
||||||
|
type HeaderController,
|
||||||
|
type HeaderSnippet,
|
||||||
|
} from '$lib/workspace/header/context';
|
||||||
import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte';
|
import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte';
|
||||||
import SidebarFrame from '$lib/workspace/sidebar/SidebarFrame.svelte';
|
import SidebarFrame from '$lib/workspace/sidebar/SidebarFrame.svelte';
|
||||||
import { SIDEBAR_CONTEXT, type SidebarController, type SidebarSnippet } from '$lib/workspace/sidebar/context';
|
import { SIDEBAR_CONTEXT, type SidebarController, type SidebarSnippet } from '$lib/workspace/sidebar/context';
|
||||||
@@ -16,14 +20,19 @@
|
|||||||
const sidebarOverrides = createOverrideStack<SidebarSnippet>((activeSidebar) => {
|
const sidebarOverrides = createOverrideStack<SidebarSnippet>((activeSidebar) => {
|
||||||
sidebar = activeSidebar;
|
sidebar = activeSidebar;
|
||||||
});
|
});
|
||||||
const headerController = $state<HeaderController>({ content: null });
|
let header = $state<HeaderSnippet | null>(null);
|
||||||
|
const headerOverrides = createOverrideStack<HeaderSnippet>((activeHeader) => {
|
||||||
|
header = activeHeader;
|
||||||
|
});
|
||||||
let sidebarFolded = $state(false);
|
let sidebarFolded = $state(false);
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
sidebarFolded = !sidebarFolded;
|
sidebarFolded = !sidebarFolded;
|
||||||
}
|
}
|
||||||
|
|
||||||
provideHeaderController(headerController);
|
provideHeaderController({
|
||||||
|
registerContent: headerOverrides.register,
|
||||||
|
} satisfies HeaderController);
|
||||||
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
||||||
registerSidebar: sidebarOverrides.register,
|
registerSidebar: sidebarOverrides.register,
|
||||||
});
|
});
|
||||||
@@ -43,7 +52,7 @@
|
|||||||
<Bevel as="div" class="app-shell__topbar-bevel" top={false} right={false} left={false} fill>
|
<Bevel as="div" class="app-shell__topbar-bevel" top={false} right={false} left={false} fill>
|
||||||
<header class="app-shell__topbar">
|
<header class="app-shell__topbar">
|
||||||
<div class="app-shell__topbar-location">
|
<div class="app-shell__topbar-location">
|
||||||
{#if headerController.content}{@render headerController.content()}{/if}
|
{#if header}{@render header()}{/if}
|
||||||
</div>
|
</div>
|
||||||
<nav class="app-shell__topbar-actions" aria-label="Global navigation">
|
<nav class="app-shell__topbar-actions" aria-label="Global navigation">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
type SidebarSnippet,
|
type SidebarSnippet,
|
||||||
} from '$lib/workspace/sidebar/context';
|
} from '$lib/workspace/sidebar/context';
|
||||||
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
||||||
|
import { ownsRoutePath } from '$lib/workspace/sidebar/route-ownership';
|
||||||
import WorkspaceSidebarFixture from './WorkspaceSidebarFixture.svelte';
|
import WorkspaceSidebarFixture from './WorkspaceSidebarFixture.svelte';
|
||||||
import { designLabBasePath } from './workspace-navigation';
|
import { designLabBasePath } from './workspace-navigation';
|
||||||
import './showroom.css';
|
import './showroom.css';
|
||||||
@@ -26,7 +27,12 @@
|
|||||||
registerSidebar: sidebarContentOverrides.register,
|
registerSidebar: sidebarContentOverrides.register,
|
||||||
});
|
});
|
||||||
|
|
||||||
const inSettings = $derived(page.url.pathname.startsWith(`${designLabBasePath}/settings`));
|
const ownsCurrentRoute = $derived(
|
||||||
|
ownsRoutePath(designLabBasePath, page.url.pathname),
|
||||||
|
);
|
||||||
|
const inSettings = $derived(
|
||||||
|
ownsRoutePath(`${designLabBasePath}/settings`, page.url.pathname),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet designLabHeader()}
|
{#snippet designLabHeader()}
|
||||||
@@ -45,7 +51,11 @@
|
|||||||
<WorkspaceSidebarFixture currentPath={page.url.pathname} content={sidebarContent} />
|
<WorkspaceSidebarFixture currentPath={page.url.pathname} content={sidebarContent} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<HeaderOverride content={designLabHeader} />
|
<HeaderOverride content={designLabHeader} active={ownsCurrentRoute} />
|
||||||
<SidebarOverride controller={parentSidebarController} sidebar={workspaceSidebar} />
|
<SidebarOverride
|
||||||
|
controller={parentSidebarController}
|
||||||
|
sidebar={workspaceSidebar}
|
||||||
|
active={ownsCurrentRoute}
|
||||||
|
/>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import { ownsRoutePath } from '$lib/workspace/sidebar/route-ownership';
|
||||||
import { designLabBasePath, workspaceNavigation, workspaceWorkers } from './workspace-navigation';
|
import { designLabBasePath, workspaceNavigation, workspaceWorkers } from './workspace-navigation';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="workspace-sidebar-shortcut"
|
class="workspace-sidebar-shortcut"
|
||||||
class:active={currentPath.startsWith(settingsPath)}
|
class:active={ownsRoutePath(settingsPath, currentPath)}
|
||||||
href={settingsPath}
|
href={settingsPath}
|
||||||
aria-label="Workspace settings"
|
aria-label="Workspace settings"
|
||||||
title="Workspace settings"
|
title="Workspace settings"
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
type SidebarSnippet,
|
type SidebarSnippet,
|
||||||
} from '$lib/workspace/sidebar/context';
|
} from '$lib/workspace/sidebar/context';
|
||||||
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
||||||
|
import { ownsRoutePath } from '$lib/workspace/sidebar/route-ownership';
|
||||||
import SettingsSidebarFixture from './SettingsSidebarFixture.svelte';
|
import SettingsSidebarFixture from './SettingsSidebarFixture.svelte';
|
||||||
|
import { designLabBasePath } from '../workspace-navigation';
|
||||||
import type { LayoutProps } from './$types';
|
import type { LayoutProps } from './$types';
|
||||||
|
|
||||||
let { children }: LayoutProps = $props();
|
let { children }: LayoutProps = $props();
|
||||||
@@ -22,12 +24,20 @@
|
|||||||
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
||||||
registerSidebar: sidebarContentOverrides.register,
|
registerSidebar: sidebarContentOverrides.register,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ownsCurrentRoute = $derived(
|
||||||
|
ownsRoutePath(`${designLabBasePath}/settings`, page.url.pathname),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet settingsSidebar()}
|
{#snippet settingsSidebar()}
|
||||||
<SettingsSidebarFixture currentPath={page.url.pathname} content={sidebarContent} />
|
<SettingsSidebarFixture currentPath={page.url.pathname} content={sidebarContent} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<SidebarOverride controller={parentSidebarController} sidebar={settingsSidebar} />
|
<SidebarOverride
|
||||||
|
controller={parentSidebarController}
|
||||||
|
sidebar={settingsSidebar}
|
||||||
|
active={ownsCurrentRoute}
|
||||||
|
/>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
type SidebarSnippet,
|
type SidebarSnippet,
|
||||||
} from '$lib/workspace/sidebar/context';
|
} from '$lib/workspace/sidebar/context';
|
||||||
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
||||||
|
import { ownsRoutePath } from '$lib/workspace/sidebar/route-ownership';
|
||||||
|
import { workspaceRoute } from '$lib/workspace/api/http';
|
||||||
import { disposeWorkspaceMultiplexer } from '$lib/workspace/multiplexer';
|
import { disposeWorkspaceMultiplexer } from '$lib/workspace/multiplexer';
|
||||||
import { disposeWorkspaceWorkersStore } from '$lib/workspace/sidebar/worker-subscription';
|
import { disposeWorkspaceWorkersStore } from '$lib/workspace/sidebar/worker-subscription';
|
||||||
import WorkspaceSidebar from '$lib/workspace/sidebar/WorkspaceSidebar.svelte';
|
import WorkspaceSidebar from '$lib/workspace/sidebar/WorkspaceSidebar.svelte';
|
||||||
@@ -30,6 +32,11 @@
|
|||||||
registerSidebar: sidebarContentOverrides.register,
|
registerSidebar: sidebarContentOverrides.register,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const workspaceId = $derived(data.workspace?.workspace_id ?? page.params.workspaceId ?? '');
|
||||||
|
const ownsCurrentRoute = $derived(
|
||||||
|
workspaceId !== '' && ownsRoutePath(workspaceRoute(workspaceId), page.url.pathname),
|
||||||
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const workspaceId = data.workspace?.workspace_id;
|
const workspaceId = data.workspace?.workspace_id;
|
||||||
if (!workspaceId) return;
|
if (!workspaceId) return;
|
||||||
@@ -57,7 +64,11 @@
|
|||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<HeaderOverride content={workspaceHeader} />
|
<HeaderOverride content={workspaceHeader} active={ownsCurrentRoute} />
|
||||||
<SidebarOverride controller={parentSidebarController} sidebar={workspaceSidebar} />
|
<SidebarOverride
|
||||||
|
controller={parentSidebarController}
|
||||||
|
sidebar={workspaceSidebar}
|
||||||
|
active={ownsCurrentRoute}
|
||||||
|
/>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
type SidebarSnippet,
|
type SidebarSnippet,
|
||||||
} from '$lib/workspace/sidebar/context';
|
} from '$lib/workspace/sidebar/context';
|
||||||
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
||||||
|
import { ownsRoutePath } from '$lib/workspace/sidebar/route-ownership';
|
||||||
|
import { workspaceRoute } from '$lib/workspace/api/http';
|
||||||
import '$lib/workspace/styles/settings.css';
|
import '$lib/workspace/styles/settings.css';
|
||||||
import type { LayoutProps } from './$types';
|
import type { LayoutProps } from './$types';
|
||||||
|
|
||||||
let { children }: LayoutProps = $props();
|
let { data, children }: LayoutProps = $props();
|
||||||
const parentSidebarController = getSidebarController();
|
const parentSidebarController = getSidebarController();
|
||||||
let sidebarContent = $state<SidebarSnippet | null>(null);
|
let sidebarContent = $state<SidebarSnippet | null>(null);
|
||||||
const sidebarContentOverrides = createOverrideStack<SidebarSnippet>((activeContent) => {
|
const sidebarContentOverrides = createOverrideStack<SidebarSnippet>((activeContent) => {
|
||||||
@@ -23,6 +25,12 @@
|
|||||||
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
||||||
registerSidebar: sidebarContentOverrides.register,
|
registerSidebar: sidebarContentOverrides.register,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const workspaceId = $derived(data.workspace?.workspace_id ?? page.params.workspaceId ?? '');
|
||||||
|
const ownsCurrentRoute = $derived(
|
||||||
|
workspaceId !== '' &&
|
||||||
|
ownsRoutePath(workspaceRoute(workspaceId, '/settings'), page.url.pathname),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet settingsSidebar()}
|
{#snippet settingsSidebar()}
|
||||||
@@ -33,7 +41,11 @@
|
|||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<SidebarOverride controller={parentSidebarController} sidebar={settingsSidebar} />
|
<SidebarOverride
|
||||||
|
controller={parentSidebarController}
|
||||||
|
sidebar={settingsSidebar}
|
||||||
|
active={ownsCurrentRoute}
|
||||||
|
/>
|
||||||
|
|
||||||
<section class="settings-page">
|
<section class="settings-page">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
Reference in New Issue
Block a user