diff --git a/packages/client/ui-dockkit/src/components/dockkit.module.css b/packages/client/ui-dockkit/src/components/dockkit.module.css index 82541eea1e..fab875c5b6 100644 --- a/packages/client/ui-dockkit/src/components/dockkit.module.css +++ b/packages/client/ui-dockkit/src/components/dockkit.module.css @@ -223,15 +223,18 @@ /* macOS desktop: an embedder may pin this surface under the window's top drag band (ui-layout .leadingBand — full-window geometry that Electron composes - in DOM order, not by stacking). The chip run, the strip-end chrome, and the - pane body subtract their whole boxes so tabs, controls, and content stay - clickable even where they overlap the band; the strip's blank fill and top - inset keep the band's drag, so the row moves the window like a native tab - bar. Hidden surfaces contribute nothing: Blink skips non-visible boxes when - it collects draggable regions. */ + in DOM order, not by stacking). The chip run, the strip-end chrome, the + pane body, and the split divider subtract their whole boxes so tabs, + controls, content, and the resize grab stay usable even where they overlap + the band; the strip's blank fill and top inset keep the band's drag, so the + row moves the window like a native tab bar. The divider must be named here: + it is a plain div, so ui-web base.css's interactive-element subtraction + never matches it. Hidden surfaces contribute nothing: Blink skips + non-visible boxes when it collects draggable regions. */ :global(html[data-platform='darwin']) .stripTabs, :global(html[data-platform='darwin']) .stripChrome, -:global(html[data-platform='darwin']) .paneBody { +:global(html[data-platform='darwin']) .paneBody, +:global(html[data-platform='darwin']) .divider { -webkit-app-region: no-drag; } diff --git a/packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts b/packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts new file mode 100644 index 0000000000..4793b7e617 --- /dev/null +++ b/packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts @@ -0,0 +1,28 @@ +/** + * macOS drag-band subtraction, asserted against dockkit.module.css on disk: + * an embedder may pin the surface under the window's top drag band, and + * Electron composes app-regions from geometry in DOM order — every box that + * must stay usable subtracts itself. The divider must be listed explicitly: + * it is a plain div, so ui-web base.css's interactive-element subtraction + * never matches it, and an unsubtracted vertical divider's top run would + * drag the window instead of resizing the split. + */ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const css = readFileSync(resolve(import.meta.dirname, '../src/components/dockkit.module.css'), 'utf8') + +describe('macOS app-region subtraction', () => { + it('subtracts the chip run, strip-end chrome, pane body, and divider; never declares drag', () => { + const rules = [...css.matchAll(/(?[^{}]+)\{(?[^{}]*)\}/g)] + .map(match => [match.groups!['sel']!.trim(), match.groups!['body']!] as const) + .filter(([, body]) => body.includes('-webkit-app-region')) + expect(rules).toHaveLength(1) + const [selector, body] = rules[0]! + expect(/-webkit-app-region:\s*([^;]+);/.exec(body)?.[1]?.trim()).toBe('no-drag') + for (const part of ['.stripTabs', '.stripChrome', '.paneBody', '.divider']) { + expect(selector, part).toContain(`:global(html[data-platform='darwin']) ${part}`) + } + }) +}) diff --git a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css index 8d1c325f95..890b897cc8 100644 --- a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css +++ b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css @@ -49,9 +49,12 @@ /* One-frame app-region pulse at each open/close edge (RightbarSeat): changing this computed value is what makes Electron recollect the window's drag rects, which the slide's transform/visibility flips alone never dirty - (electron#32341). While set, the whole panel is out of the drag band for - that frame, which is also the desired end state everywhere but the strip's - blank fill. */ + (electron#32341). The panel's steady state sets no app-region in either + presentation, so this write is a real change at both edges — including a + hidden panel opening straight into fullscreen (a narrow window's first + open), whose rects were skipped while hidden. While set, the whole panel is + out of the drag band for that frame; the removal frame recollects the + steady-state geometry below. */ :global(html[data-platform='darwin']) .panel[data-sidebar-right-region-nudge] { -webkit-app-region: no-drag; } @@ -63,15 +66,16 @@ border: none; } -/* Electron drag regions are window geometry, not stacking: the frame's leading - band and the sidebar logo row's drag rects underneath keep taking the pointer - through this fixed overlay, turning non-interactive panel content (file - preview text, images, webviews) into window drag. The panel appears after - both in DOM order, so this subtracts its whole box; the strips' blank runs - give up the drag affordance in exchange. */ -:global(html[data-platform='darwin']) .panel[data-sidebar-right-panel='fullscreen'] { - -webkit-app-region: no-drag; -} +/* No whole-box subtraction here: Electron drag regions are window geometry, + not stacking, so the frame's leading band does keep acting through this + fixed overlay — but the kit already subtracts every box that must stay + usable (tabs, strip-end chrome, pane bodies, dividers; ui-dockkit), and + ui-web base.css subtracts interactive elements and portalled overlays. + What remains of the band under the strips' blank runs is the window drag + affordance, the same native-tab-bar reading as the push presentation. A + panel-wide no-drag would trade it away and, worse, make no-drag the + steady state, turning the open/close pulse above into a computed-value + no-op exactly when a hidden panel opens straight into fullscreen. */ /* Windows keeps caption controls above fullscreen file and terminal panels. */ :global([data-windows-titlebar]) .panel[data-sidebar-right-panel='fullscreen'] { diff --git a/packages/client/ui-sidebar-right/tests/region-nudge-styles.client.spec.ts b/packages/client/ui-sidebar-right/tests/region-nudge-styles.client.spec.ts new file mode 100644 index 0000000000..e095849e1d --- /dev/null +++ b/packages/client/ui-sidebar-right/tests/region-nudge-styles.client.spec.ts @@ -0,0 +1,46 @@ +/** + * App-region nudge pulse, asserted against SidebarRight.module.css on disk: + * Electron recollects the window's drag rects only when a style pass changes + * a computed app-region value (electron#32341), so the panel's steady state + * must declare NO app-region in either presentation — the open/close no-drag + * pulse is then a real computed-value change at both edges, including a + * hidden panel opening straight into fullscreen (a narrow window's first + * open, whose rects were skipped while hidden). A steady-state value on the + * panel (the old fullscreen whole-box no-drag) makes the pulse a silent + * no-op and leaves the frame's drag band swallowing the strip's top-right + * controls; fine-grained subtraction belongs to ui-dockkit (tabs, strip-end + * chrome, pane bodies, dividers), leaving the strips' blank runs draggable. + */ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' + +const css = readFileSync(new URL('../src/client/shell/SidebarRight.module.css', import.meta.url), 'utf8') + +/** + * Every ` -> value` pair of rules declaring `-webkit-app-region`. + * @returns one entry per declaring rule, in source order. + */ +function appRegionRules(): [selector: string, value: string][] { + const rules: [string, string][] = [] + for (const match of css.matchAll(/(?[^{}]+)\{(?[^{}]*)\}/g)) { + const value = /-webkit-app-region:\s*([^;]+);/.exec(match.groups!['body']!)?.[1] + if (value !== undefined) rules.push([match.groups!['sel']!.trim(), value.trim()]) + } + return rules +} + +describe('app-region nudge pulse styles', () => { + it('pulses the panel to no-drag', () => { + const pulses = appRegionRules() + .filter(([selector]) => selector.includes('[data-sidebar-right-region-nudge]')) + expect(pulses).toHaveLength(1) + expect(pulses[0]![0]).toMatch(/\.panel\[data-sidebar-right-region-nudge\]\s*$/) + expect(pulses[0]![1]).toBe('no-drag') + }) + + it('declares no steady-state app-region, so the pulse is a real change', () => { + const steady = appRegionRules() + .filter(([selector]) => !selector.includes('[data-sidebar-right-region-nudge]')) + expect(steady).toEqual([]) + }) +})