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
8 changes: 4 additions & 4 deletions frontend/src/components/Shell/Shell.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,15 @@
/* A docked sidebar reserves real workspace width. The class is applied only by
Shell's desktop layout state, so there is no duplicate CSS breakpoint to
drift from the interaction model. */
.shell--drawer-docked .shell__tabstrip,
.shell--drawer-docked .shell__content {
.shell--drawer-docked > .shell__tabstrip,
.shell--drawer-docked > .shell__content {
margin-left: var(--desktop-sidebar-width);
}

/* Immersive apps own the full viewport without overwriting the saved sidebar
preference; the reserved width returns when immersive mode exits. */
.shell--immersive.shell--drawer-docked .shell__tabstrip,
.shell--immersive.shell--drawer-docked .shell__content {
.shell--immersive.shell--drawer-docked > .shell__tabstrip,
.shell--immersive.shell--drawer-docked > .shell__content {
margin-left: 0;
}

Expand Down
11 changes: 11 additions & 0 deletions frontend/src/components/Shell/__tests__/workspaceUi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ test('keyboard pane focus is visible but stays off for mouse and touch', () => {
const viewModeToggleSrc = readFileSync(new URL('../ViewModeToggle.jsx', import.meta.url), 'utf8')
const shellCss = readFileSync(new URL('../Shell.css', import.meta.url), 'utf8')

test('the docked sidebar offsets only direct shell layout rows', () => {
// Pane strips reuse .shell__tabstrip inside .shell__content. A descendant
// selector would apply the 320px sidebar margin twice and detach every strip
// from the pane rectangle that owns it.
assert.match(shellCss, /\.shell--drawer-docked > \.shell__tabstrip,/)
assert.match(shellCss, /\.shell--drawer-docked > \.shell__content/)
assert.match(shellCss, /\.shell--immersive\.shell--drawer-docked > \.shell__tabstrip,/)
assert.match(shellCss, /\.shell--immersive\.shell--drawer-docked > \.shell__content/)
assert.doesNotMatch(shellCss, /\.shell--drawer-docked \.shell__tabstrip/)
})

test('the view-mode toggle is rendered in the shell top bar, in line with the logo, flag-gated', () => {
// It renders inside .shell__bar (the brand/logo cluster), flag-gated on splits.
assert.match(shell, /<header className="shell__bar"/)
Expand Down
39 changes: 39 additions & 0 deletions tests/tabs.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,28 @@ async function measure(page) {
})
}

/** Multi-pane strips and their active content wrappers share one projected
* pane rectangle: their left edges and widths match, and the strip ends where
* pane content begins. Report rounded deltas so the assertion remains about
* layout ownership rather than sub-pixel device-scale noise. */
async function paneChromeDeltas(page) {
return page.locator('.workspace__strip').evaluateAll(strips => strips.map((strip) => {
const active = strip.querySelector('.shell__tab--active .shell__tab-open')
const key = active?.dataset.dragKey
const pane = key
? document.querySelector(`.shell__view--paned[data-tab-key="${CSS.escape(key)}"]`)
: null
if (!pane) return null
const stripRect = strip.getBoundingClientRect()
const paneRect = pane.getBoundingClientRect()
return {
left: Math.round(stripRect.left - paneRect.left),
width: Math.round(stripRect.width - paneRect.width),
seam: Math.round(stripRect.bottom - paneRect.top),
}
}))
}

async function seedTabs(page, tabs) {
const workspace = paneModel.serializeWorkspace(paneModel.seedFromFlatTabs(tabs))
await page.addInitScript(([workspaceKey, workspaceRaw, legacyKey, t]) => {
Expand Down Expand Up @@ -236,6 +258,23 @@ test.describe('Tabs', () => {
&& Math.abs(rects[0].x - rects[1].x) > 100
}).toBe(true)

// The sidebar offsets the outer content container exactly once. Per-pane
// strips stay in that container's coordinate system through both toggle
// directions instead of inheriting the global 320px margin themselves.
const aligned = [
{ left: 0, width: 0, seam: 0 },
{ left: 0, width: 0, seam: 0 },
]
const navigationToggle = page.getByRole('button', { name: 'Toggle navigation' })
await expect(navigationToggle).toHaveAttribute('aria-expanded', 'true')
await expect.poll(() => paneChromeDeltas(page)).toEqual(aligned)
await navigationToggle.click()
await expect(navigationToggle).toHaveAttribute('aria-expanded', 'false')
await expect.poll(() => paneChromeDeltas(page)).toEqual(aligned)
await navigationToggle.click()
await expect(navigationToggle).toHaveAttribute('aria-expanded', 'true')
await expect.poll(() => paneChromeDeltas(page)).toEqual(aligned)

// Native chat content focuses its pane through wrapper capture.
await page.locator('.chat').dispatchEvent('pointerdown')
await expect(page.locator('.workspace__strip--focused')).toContainText(chat.title)
Expand Down
Loading