diff --git a/.changeset/yellow-hoops-grin.md b/.changeset/yellow-hoops-grin.md new file mode 100644 index 0000000..cc960b0 --- /dev/null +++ b/.changeset/yellow-hoops-grin.md @@ -0,0 +1,5 @@ +--- +"sideshow": patch +--- + +Add a minimal control to collapse the desktop session sidebar into a narrow rail. diff --git a/e2e/embed-aside-head-slot.spec.ts b/e2e/embed-aside-head-slot.spec.ts index ab63ebd..53e7354 100644 --- a/e2e/embed-aside-head-slot.spec.ts +++ b/e2e/embed-aside-head-slot.spec.ts @@ -86,5 +86,15 @@ test.describe("embedded engine: ss:aside-head slot", () => { expect(headBox).not.toBeNull(); expect(listBox).not.toBeNull(); expect(headBox!.y).toBeLessThan(listBox!.y); + + // Collapsing hides host-projected sidebar content without unmounting it; + // expanding restores the projection intact. + const aside = page.locator("aside"); + await page.getByRole("button", { name: "Collapse sidebar" }).click(); + await expect(aside).toHaveCSS("width", "40px"); + await expect(hostHead).toBeHidden(); + await page.getByRole("button", { name: "Expand sidebar" }).click(); + await expect(aside).toHaveCSS("width", "248px"); + await expect(hostHead).toBeVisible(); }); }); diff --git a/e2e/embed-main-slot.spec.ts b/e2e/embed-main-slot.spec.ts index 59e39cb..7f55ffb 100644 --- a/e2e/embed-main-slot.spec.ts +++ b/e2e/embed-main-slot.spec.ts @@ -79,8 +79,14 @@ test("embedded engine: ss:main slot takes over the main pane while the sidebar s await expect(page.locator("main slot[name='ss:main']")).toHaveCount(1); // The sidebar (full layout) stays — the override is the main pane only, not the - // whole viewport. - await expect(page.locator("aside")).toBeVisible(); + // whole viewport — and its engine-owned collapse control works across the shadow root. + const aside = page.locator("aside"); + await expect(aside).toBeVisible(); + await page.getByRole("button", { name: "Collapse sidebar" }).click(); + await expect(aside).toHaveCSS("width", "40px"); + await expect(hostMain).toBeVisible(); + await page.getByRole("button", { name: "Expand sidebar" }).click(); + await expect(aside).toHaveCSS("width", "248px"); // The engine's own board content is replaced: with a child assigned to ss:main, // the slot's fallback (#sessionView stream) stays in the DOM — native diff --git a/e2e/viewer.spec.ts b/e2e/viewer.spec.ts index c0c236b..1f5ed98 100644 --- a/e2e/viewer.spec.ts +++ b/e2e/viewer.spec.ts @@ -374,6 +374,40 @@ test("Cmd+Option+Up/Down switches between sessions, wrapping at the ends", async await expect(page.locator(".sess.sel .sess-title")).toContainText("one session"); }); +test("the desktop sidebar can collapse to a minimal rail and expand again", async ({ + page, + server, +}) => { + await publish(server.url, { html: "

x

", title: "Compact", agent: "e2e" }); + await page.setViewportSize({ width: 701, height: 800 }); + await page.goto(server.url); + + const aside = page.locator("aside"); + const main = page.locator("main"); + const toggle = page.getByRole("button", { name: "Collapse sidebar" }); + const expandedMainWidth = (await main.boundingBox())!.width; + + await expect(toggle).toBeVisible(); + await expect(toggle).toHaveAttribute("aria-expanded", "true"); + await toggle.click(); + + const expand = page.getByRole("button", { name: "Expand sidebar" }); + await expect(expand).toHaveAttribute("aria-expanded", "false"); + await expect(aside).toHaveCSS("width", "40px"); + await expect(page.locator("#sessionList")).toBeHidden(); + expect((await main.boundingBox())!.width).toBeGreaterThan(expandedMainWidth + 150); + + await expand.click(); + await expect(page.getByRole("button", { name: "Collapse sidebar" })).toBeVisible(); + await expect(aside).toHaveCSS("width", "248px"); + await expect(page.locator("#sessionList")).toBeVisible(); + + // One pixel narrower hands control back to the existing mobile drawer. + await page.setViewportSize({ width: 700, height: 800 }); + await expect(page.getByRole("button", { name: "Collapse sidebar" })).toBeHidden(); + await expect(aside).not.toBeInViewport(); +}); + test("at phone width the sidebar collapses into a drawer and actions stay visible", async ({ page, server, @@ -409,6 +443,7 @@ test("at phone width the sidebar collapses into a drawer and actions stay visibl const card = page.locator(".card:not(#whatsNew)"); await expect(card).toBeVisible(); await expect(page.locator("aside")).not.toBeInViewport(); + await expect(page.getByRole("button", { name: "Collapse sidebar" })).toBeHidden(); expect((await card.boundingBox())!.width).toBeGreaterThan(300); // hover-only card actions are always visible at narrow widths diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index cc98a46..9eb9508 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -18,7 +18,14 @@ import { ConnectInstructions } from "./Connect.tsx"; import { renderNotes } from "./notes.ts"; import { SessionTimeline } from "./SessionTimeline.tsx"; import { StreamSkeleton } from "./Skeleton.tsx"; -import { MoonIcon, PlugIcon, SunIcon, SystemIcon } from "./icons.tsx"; +import { + MoonIcon, + PanelLeftCloseIcon, + PanelLeftOpenIcon, + PlugIcon, + SunIcon, + SystemIcon, +} from "./icons.tsx"; import { activeTheme, colorModePreference, @@ -112,6 +119,8 @@ function pageTitle( } export default function App() { + const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false); + onMount(() => { // Await the initial route resolution (the standalone post fetch, or the // first session fetch), then mark the workspace decided and tell the host @@ -243,10 +252,26 @@ export default function App() { -