From e975c648c21aa7ef008afe85a85a1efc792faa93 Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 23 Jul 2026 21:59:47 +0900 Subject: [PATCH] web: move sidebar fold into frame --- .../console/worker-console.ui.test.ts | 32 ++++++-- .../workspace/sidebar/GlobalSidebar.svelte | 4 +- .../lib/workspace/sidebar/SidebarFrame.svelte | 46 +++++++++++ .../workspace/sidebar/WorkspaceSidebar.svelte | 51 +++---------- .../src/lib/workspace/sidebar/sidebar.css | 76 ++++++++----------- web/workspace/src/routes/+layout.svelte | 13 ++-- .../src/routes/w/[workspaceId]/+layout.svelte | 7 -- 7 files changed, 121 insertions(+), 108 deletions(-) create mode 100644 web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 1c8705f0..1eb10522 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -393,6 +393,12 @@ Deno.test("Account UI owns browser passkey session state without workspace autho const globalSidebar = await Deno.readTextFile( new URL("../sidebar/GlobalSidebar.svelte", import.meta.url), ); + const sidebarFrame = await Deno.readTextFile( + new URL("../sidebar/SidebarFrame.svelte", import.meta.url), + ); + const sidebarCss = await Deno.readTextFile( + new URL("../sidebar/sidebar.css", import.meta.url), + ); const workspaceLayout = await Deno.readTextFile( new URL("./../../../routes/w/[workspaceId]/+layout.svelte", import.meta.url), ); @@ -432,6 +438,7 @@ Deno.test("Account UI owns browser passkey session state without workspace autho assert( rootLayout.includes("SIDEBAR_CONTEXT") && rootLayout.includes("GlobalSidebar") && + rootLayout.includes("SidebarFrame") && rootLayout.includes("{@render sidebar()}") && !rootLayout.includes("WorkspaceSidebar") && rootLayout.includes("workspace-topbar") && @@ -459,14 +466,23 @@ Deno.test("Account UI owns browser passkey session state without workspace autho "Workspace layout should load workspace data and register a WorkspaceSidebar snippet", ); assert( - workspaceLayout.includes("sidebarFolded") && - workspaceLayout.includes("onToggleFold={toggleSidebarFold}") && - sidebar.includes("folded?: boolean") && - sidebar.includes("onToggleFold?: () => void") && - sidebar.includes("sidebar-fold-button") && - sidebar.includes("Fold sidebar") && - sidebar.includes("Unfold sidebar"), - "Workspace sidebar should keep a visible fold button with fold/folded naming", + sidebarFrame.includes("let folded = $state(false)") && + sidebarFrame.includes("sidebar-fold-button") && + sidebarFrame.includes("Fold sidebar") && + sidebarFrame.includes("Unfold sidebar") && + !workspaceLayout.includes("sidebarFolded") && + !workspaceLayout.includes("onToggleFold") && + !sidebar.includes("folded?: boolean") && + !sidebar.includes("onToggleFold?: () => void") && + !sidebar.includes("sidebar-fold-button"), + "Sidebar fold control should belong to SidebarFrame, not WorkspaceSidebar", + ); + assert( + sidebarCss.startsWith("@layer reset, tokens, base, layout, components;") && + sidebarCss.includes(".sidebar-frame") && + sidebarCss.includes(".sidebar-link") && + sidebarCss.includes("text-decoration: none"), + "Sidebar styles should define their layer order before component rules so base link styles do not win by import order", ); assert( sidebarOverride.includes("controller.setSidebar(sidebar)") && diff --git a/web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte b/web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte index 19233289..9130b1a1 100644 --- a/web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte +++ b/web/workspace/src/lib/workspace/sidebar/GlobalSidebar.svelte @@ -13,7 +13,7 @@ ]; - + diff --git a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte new file mode 100644 index 00000000..9eb3796a --- /dev/null +++ b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte @@ -0,0 +1,46 @@ + + + diff --git a/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte b/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte index 5560a5ae..cd910725 100644 --- a/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte +++ b/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte @@ -13,8 +13,6 @@ repositories?: RepositoryListResponse | null; repositoriesError?: string | null; currentPath?: string; - folded?: boolean; - onToggleFold?: () => void; }; let { @@ -22,9 +20,7 @@ workspaceError = null, repositories = null, repositoriesError = null, - currentPath = '/', - folded = false, - onToggleFold + currentPath = '/' }: Props = $props(); let workspaceId = $derived(workspace?.workspace_id ?? ''); @@ -32,35 +28,8 @@ let settingsHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/settings') : '/settings'); - + + diff --git a/web/workspace/src/lib/workspace/sidebar/sidebar.css b/web/workspace/src/lib/workspace/sidebar/sidebar.css index a845b208..ff6733b4 100644 --- a/web/workspace/src/lib/workspace/sidebar/sidebar.css +++ b/web/workspace/src/lib/workspace/sidebar/sidebar.css @@ -1,5 +1,7 @@ +@layer reset, tokens, base, layout, components; + @layer components { - .global-sidebar { + .sidebar-frame { grid-column: 1; grid-row: 1 / 3; width: clamp(220px, 20vw, 280px); @@ -9,24 +11,22 @@ padding: var(--space-4) var(--space-3); border-right: 1px solid var(--line); } - .global-sidebar-section { + .sidebar-frame.folded { + width: max-content; + overflow: hidden; + padding-inline: var(--space-2); + } + .sidebar-frame-content, + .global-sidebar, + .workspace-sidebar { + min-width: 0; + } + .global-sidebar, + .global-sidebar-section, + .workspace-sidebar { display: grid; gap: var(--space-2); } - .workspace-sidebar { - grid-column: 1; - grid-row: 1 / 3; - width: clamp(220px, 20vw, 280px); - align-self: stretch; - min-width: 0; - min-height: 0; - overflow-y: auto; - padding: var(--space-4) var(--space-3); - border-right: 1px solid var(--line); - } - .workspace-sidebar.folded { - width: max-content; - } .sidebar-header { display: grid; gap: var(--space-2); @@ -39,6 +39,13 @@ justify-content: flex-end; gap: var(--space-1); } + .sidebar-control-row { + margin-bottom: var(--space-2); + } + .sidebar-frame.folded .sidebar-control-row { + justify-content: center; + margin-bottom: 0; + } .sidebar-title-row { display: flex; align-items: center; @@ -84,13 +91,13 @@ color: var(--text-muted); text-decoration: none; } - .sidebar-fold-button { - border: 1px solid var(--line); - background: var(--bg-raised); - box-shadow: var(--shadow-soft); - color: var(--text-strong); - cursor: pointer; - } + .sidebar-fold-button { + border: 1px solid var(--line); + background: var(--bg-raised); + box-shadow: var(--shadow-soft); + color: var(--text-strong); + cursor: pointer; + } .sidebar-icon { width: 18px; height: 18px; @@ -107,25 +114,6 @@ background: var(--interactive-hover); color: var(--text-muted); } - .workspace-sidebar.folded { - overflow: hidden; - padding-inline: var(--space-2); - } - .workspace-sidebar.folded .sidebar-header { - margin-bottom: 0; - } - .workspace-sidebar.folded .sidebar-control-row, - .workspace-sidebar.folded .sidebar-title-row, - .workspace-sidebar.folded .sidebar-actions-row { - justify-content: center; - } - .workspace-sidebar.folded .sidebar-actions-row { - align-items: center; - flex-direction: column; - } - .workspace-sidebar.folded .workspace-label { - display: none; - } .sidebar-sections { display: grid; gap: var(--space-5); @@ -249,8 +237,8 @@ } @media (max-width: 760px) { - .global-sidebar, - .workspace-sidebar { + .sidebar-frame, + .sidebar-frame.folded { grid-column: 1; grid-row: 1; width: auto; diff --git a/web/workspace/src/routes/+layout.svelte b/web/workspace/src/routes/+layout.svelte index 07fbcb1c..f77e2e83 100644 --- a/web/workspace/src/routes/+layout.svelte +++ b/web/workspace/src/routes/+layout.svelte @@ -3,6 +3,7 @@ import { setContext } from 'svelte'; import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte'; import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte'; + import SidebarFrame from '$lib/workspace/sidebar/SidebarFrame.svelte'; import { SIDEBAR_CONTEXT, type SidebarSnippet } from '$lib/workspace/sidebar/context'; import '../app.css'; import type { LayoutProps } from './$types'; @@ -23,11 +24,13 @@