diff --git a/packages/app/e2e/regression/session-header.spec.ts b/packages/app/e2e/regression/session-header.spec.ts index 50b7bdb5a7e..9c6ab38cbb6 100644 --- a/packages/app/e2e/regression/session-header.spec.ts +++ b/packages/app/e2e/regression/session-header.spec.ts @@ -130,6 +130,66 @@ test.describe("location", () => { sessions: [{ id: "ses_location", title: "Location session", directory: worktree }], } + test("only collapses location names when the timeline header is narrow", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }) + await openSession(page, { + ...workspace, + sessions: [{ id: "ses_location", title: "Investigate session header layout regression", directory: worktree }], + }) + const header = page.locator("[data-session-title]") + const names = header.locator('[data-slot="session-location-project"], [data-slot="session-location-worktree"]') + const review = page.getByRole("button", { name: "Toggle review", exact: true }) + await expect(names).toHaveText(["Location", "quiet-falcon"]) + + for (const panel of [false, true, false]) { + if (panel || (await review.getAttribute("aria-expanded")) === "true") await review.click() + await expect(review).toHaveAttribute("aria-expanded", String(panel)) + await expect + .poll(() => + names.evaluateAll((elements) => elements.every((element) => getComputedStyle(element).clipPath === "none")), + ) + .toBe(!panel) + await expect(header.getByRole("button", { name: "More options", exact: true })).toBeInViewport() + await expect + .poll(() => header.getByRole("heading").evaluate((element) => element.scrollWidth <= element.clientWidth)) + .toBe(true) + if (panel) { + await expect + .poll(() => + header.evaluate((element) => { + const icon = element.querySelector('svg:has(use[href="#opencode-v2-icon-outline-worktree"])') + const before = icon?.previousElementSibling?.getBoundingClientRect() + const separator = element.querySelector('[data-slot="session-title-separator"]') + const after = separator?.getBoundingClientRect() + const title = element.querySelector("h1") + const avatar = element + .querySelector('[data-slot="session-location-trigger"]') + ?.firstElementChild?.getBoundingClientRect() + const bounds = icon?.getBoundingClientRect() + + return bounds && before && after && avatar && separator && title + ? [ + before.left - avatar.right, + bounds.left - before.right, + after.left - bounds.right, + title.getBoundingClientRect().left + + parseFloat(getComputedStyle(title).paddingLeft) - + after.right + + parseFloat(getComputedStyle(separator).paddingRight), + ] + : [] + }), + ) + .toEqual([10, 8, 8, 10]) + const location = header.locator('[data-slot="session-location-trigger"]') + await location.hover() + await expect(location).toHaveCSS("padding-inline-start", "4px") + await expect(location).toHaveCSS("padding-inline-end", "4px") + } + await header.screenshot({ path: test.info().outputPath(`location-${panel ? "compact" : "expanded"}.png`) }) + } + }) + test("names the project and worktree, and the card copies the full path", async ({ page, context }) => { await context.grantPermissions(["clipboard-read", "clipboard-write"]) await openSession(page, workspace) diff --git a/packages/app/src/session/session-identity-header.css b/packages/app/src/session/session-identity-header.css index f9b5f37705b..6f217dc8c80 100644 --- a/packages/app/src/session/session-identity-header.css +++ b/packages/app/src/session/session-identity-header.css @@ -3,6 +3,7 @@ } .session-header-layout { + container: session-header / inline-size; display: flex; align-items: center; gap: 2px; @@ -20,20 +21,42 @@ .session-header-location > [data-slot="session-location-trigger"] { max-inline-size: 100%; - gap: 6px; - padding-inline: 4px; } -/* The header shows the location as the project avatar and, in a worktree, the worktree icon, so the title keeps the - row. The names stay for assistive tech, and the location card shows them in full. */ -.session-header-location [data-slot="session-location-project"], -.session-header-location [data-slot="session-location-worktree"] { - position: absolute; - inline-size: 1px; - block-size: 1px; - overflow: hidden; - clip-path: inset(50%); - white-space: nowrap; +/* Use the header's available width, not the window: opening a side panel also narrows the timeline. */ +@container session-header (max-width: 640px) { + .session-header-location { + gap: 4px; + } + + .session-header-location > [data-slot="session-location-trigger"] { + gap: 8px; + padding-inline: 4px; + } + + .session-header-location [data-component="project-avatar-v2"] { + margin-inline-end: 2px; + } + + .session-header-location [data-slot="session-location-project"] + span { + padding-inline: 0; + } + + .session-header-location > .session-location-separator[data-variant="slash"] { + /* Together with the row's 2px gap and the title's 4px padding, leave 10px before the title. */ + padding-inline: 0 4px; + } + + /* Keep the names available to assistive tech while the icons leave room for the title. */ + .session-header-location [data-slot="session-location-project"], + .session-header-location [data-slot="session-location-worktree"] { + position: absolute; + inline-size: 1px; + block-size: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + } } .session-header-title {