Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/app/e2e/session/titlebar-right-rail-contract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,53 @@ import { modKey } from "../utils"
// selection overlay, 4pt grid pinning)

test.describe("titlebar right rail contract", () => {
test("expanded titlebar toggles change icon shape without selected-chip background", async ({
page,
gotoSession,
}) => {
await gotoSession()

const sidebarToggle = page.locator('[data-action="pawwork-sidebar-toggle"]')
const rightPanelToggle = page.getByRole("button", { name: "Right utility panel" })
if ((await sidebarToggle.getAttribute("aria-expanded")) !== "true") {
await sidebarToggle.click()
}
await expect(sidebarToggle).toHaveAttribute("aria-expanded", "true")
await openRightPanel(page)
Comment thread
Astro-Han marked this conversation as resolved.
await expect(rightPanelToggle).toHaveAttribute("aria-expanded", "true")
await page.mouse.move(400, 200)

const backgroundAlpha = (selector: string) =>
page.evaluate((target) => {
const el = document.querySelector<HTMLElement>(target)
if (!el) return null
const bg = getComputedStyle(el).backgroundColor
const rgba = bg.match(/^rgba\((.+)\)$/)
if (!rgba) return bg.startsWith("rgb(") ? 1 : null
const parts = rgba[1].split(",").map((part) => part.trim())
return Number(parts[3])
}, selector)

await expect
.poll(
async () => ({
sidebar: await backgroundAlpha('[data-action="pawwork-sidebar-toggle"]'),
rightPanel: await backgroundAlpha('button[aria-label="Right utility panel"]'),
}),
{ timeout: 2_000 },
)
.toEqual({
sidebar: 0,
rightPanel: 0,
})

await sidebarToggle.hover()
await expect.poll(() => backgroundAlpha('[data-action="pawwork-sidebar-toggle"]')).toBeGreaterThan(0)

await rightPanelToggle.hover()
await expect.poll(() => backgroundAlpha('button[aria-label="Right utility panel"]')).toBeGreaterThan(0)
})

test("right utility toggle stays clickable with the full tab set open", async ({
page,
gotoSession,
Expand Down
36 changes: 31 additions & 5 deletions packages/app/e2e/status/status-popover.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ test("desktop right-panel toggle opens the status tab by default", async ({ page
// Servers/MCP/LSP/Plugins render as collapsible SectionRow `<button aria-expanded>`
// inside the Status panel body (see SessionStatusConnections.SectionRow) —
// not as `role="tab"`. Mobile still uses tabs in the popover (separate test below).
await expect(rightPanel.getByRole("button", { name: /servers/i })).toBeVisible()
await expect(rightPanel.getByRole("button", { name: /mcp/i })).toBeVisible()
await expect(rightPanel.getByRole("button", { name: /lsp/i })).toBeVisible()
await expect(rightPanel.getByRole("button", { name: /plugins/i })).toBeVisible()
await expect(rightPanel.getByRole("button", { name: /^Servers\b/i })).toBeVisible()
await expect(rightPanel.getByRole("button", { name: /^MCP\b/i })).toBeVisible()
await expect(rightPanel.getByRole("button", { name: /^LSP\b/i })).toBeVisible()
await expect(rightPanel.getByRole("button", { name: /^Plugins\b/i })).toBeVisible()
})

test("session status panel can expand mcp section", async ({ page, gotoSession }) => {
Expand Down Expand Up @@ -57,10 +57,36 @@ test("mobile session status button still opens the status popover", async ({ pag
await page.setViewportSize({ width: 390, height: 844 })
await gotoSession()

const statusButton = page.getByRole("button", { name: "Status" }).first()
const statusButton = page.locator('[data-action="pawwork-status-popover-toggle"]')
const popoverBody = page.locator('[data-slot="popover-body"]').filter({ has: page.locator('[data-component="tabs"]') })

await statusButton.click()
await expect(statusButton).toHaveAttribute("aria-expanded", "true")
await expect(popoverBody).toBeVisible()
await expect(popoverBody.getByRole("tab", { name: /servers/i })).toBeVisible()
await page.mouse.move(10, 500, { steps: 5 })

const expandedStyles = () =>
statusButton.evaluate((el) => {
const icon = el.querySelector<HTMLElement>('[data-slot="icon-svg"]')
const style = getComputedStyle(el)
const probe = document.createElement("div")
probe.style.backgroundColor = style.getPropertyValue("--surface-base")
probe.style.color = style.getPropertyValue("--icon-strong")
document.body.appendChild(probe)
const expected = {
background: getComputedStyle(probe).backgroundColor,
iconColor: getComputedStyle(probe).color,
}
probe.remove()
return {
expected,
actual: {
background: style.backgroundColor,
iconColor: icon ? getComputedStyle(icon).color : null,
},
}
})
const expectedStyles = (await expandedStyles()).expected
await expect.poll(async () => (await expandedStyles()).actual).toEqual(expectedStyles)
})
1 change: 1 addition & 0 deletions packages/app/src/components/status-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function StatusPopover() {
triggerProps={{
variant: "ghost",
class: "titlebar-icon w-8 h-[30px] p-0 box-border",
"data-action": "pawwork-status-popover-toggle",
"aria-label": language.t("status.popover.trigger"),
style: { scale: 1 },
}}
Expand Down
8 changes: 8 additions & 0 deletions packages/app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@
background-color: var(--surface-sunken);
}

[data-component="titlebar-shell"][data-shell="desktop"][data-shell-os="macos"]
:is([data-component="button"], [data-component="icon-button"]).titlebar-icon[data-variant="ghost"]:is(
[data-action="pawwork-sidebar-toggle"],
[aria-controls="right-panel"]
)[aria-expanded="true"]:not(:hover):not(:active) {
background-color: transparent;
}

[data-component="desktop-shell-main"][data-shell="desktop"][data-shell-os="macos"] {
border-top-width: 0;
}
Expand Down
27 changes: 16 additions & 11 deletions packages/ui/src/components/icon-button.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,31 @@

/* titlebar-icon: 32×30 non-square, DESIGN.md §346.
Lives in icon-button.css for historical reasons but is intentionally NOT
scoped to `[data-component="icon-button"]` — the four titlebar usages in
`titlebar.tsx` + `session-header.tsx` render `<Button variant="ghost">`
(data-component="button"), not `<IconButton>`. Scoping to icon-button
silently dropped the `aria-expanded` selected-state chip on the sidebar
toggle and Right utility panel toggle. The class name carries the contract;
the selector should not narrow it. */
scoped to `[data-component="icon-button"]` — the live titlebar toggles render
`<Button variant="ghost">` (data-component="button"), not `<IconButton>`.
Expanded state is visualized by icon shape only; idle state has no selected
chip, while hover/press feedback still comes from the normal ghost button
rules. */
.titlebar-icon {
width: 32px;
height: 30px;
aspect-ratio: auto;
}

.titlebar-icon[aria-expanded="true"] {
background-color: var(--surface-base);

.titlebar-icon[data-variant="ghost"]:is([data-action="pawwork-sidebar-toggle"], [aria-controls="right-panel"])[aria-expanded="true"] {
[data-slot="icon-svg"] {
color: var(--icon-strong);
}
}

&:hover:not(:disabled) {
background-color: var(--surface-base);
.titlebar-icon[data-variant="ghost"]:is([data-action="pawwork-sidebar-toggle"], [aria-controls="right-panel"])[aria-expanded="true"]:not(:hover):not(:active) {
background-color: transparent;
}

.titlebar-icon[data-variant="ghost"][data-action="pawwork-status-popover-toggle"][aria-expanded="true"] {
background-color: var(--surface-base);

[data-slot="icon-svg"] {
color: var(--icon-strong);
}
}
Loading