diff --git a/apps/electron-backend-e2e/src/window-controls.e2e.ts b/apps/electron-backend-e2e/src/window-controls.e2e.ts index fba81345f..da90fec62 100644 --- a/apps/electron-backend-e2e/src/window-controls.e2e.ts +++ b/apps/electron-backend-e2e/src/window-controls.e2e.ts @@ -217,10 +217,37 @@ test.describe('macOS traffic lights', () => { const lights = await app.electronApp.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.getWindowButtonPosition() ); - // The buttons are about 14pt tall; keep a visible gap below them. - expect(linkBox?.y ?? 0).toBeGreaterThanOrEqual( - (lights?.y ?? 0) + 14 + 16 + expect(lights, 'native window button position').toBeTruthy(); + const lightsY = lights?.y ?? Number.NaN; + // The buttons are about 14pt tall; keep a visible gap below them, + // measured in window pixels (CSS pixels times the zoom factor). + const linkTopInWindowPixels = async (): Promise => { + const [box, zoom] = await Promise.all([ + firstLink.boundingBox(), + page.evaluate(() => window.outerWidth / window.innerWidth), + ]); + return (box?.y ?? Number.NaN) * zoom; + }; + expect(await linkTopInWindowPixels()).toBeGreaterThanOrEqual( + lightsY + 14 + 16 ); + + // App zoom scales CSS pixels but not the native buttons: at the + // smallest zoom the inset must still clear them. + for (let step = 0; step < 8; step++) { + await page.evaluate(() => + window.electron.adjustZoomLevel('out') + ); + } + await expect + .poll(() => + page.evaluate(() => window.outerWidth / window.innerWidth) + ) + .toBeLessThan(0.6); + await expect + .poll(linkTopInWindowPixels) + .toBeGreaterThanOrEqual(lightsY + 14 + 8); + await page.evaluate(() => window.electron.adjustZoomLevel('reset')); } finally { await closeElectronApp(app); } diff --git a/docs/architecture/workspace-shell.md b/docs/architecture/workspace-shell.md index 6eafed5a5..31eaaec18 100644 --- a/docs/architecture/workspace-shell.md +++ b/docs/architecture/workspace-shell.md @@ -330,7 +330,11 @@ The Electron window hides the native title bar on all desktop platforms sit in the 56 px header band above the rail, so the macOS rail (`.app-rail.is-macos`) starts its first link at 56 px: level with the content area and the dashboard hero, with its hover surface clear of the - lights. `window-controls.e2e.ts` checks both on macOS. + lights. App zoom scales CSS pixels but not the lights, so the rail + publishes the page zoom factor (`outerWidth / innerWidth`, refreshed on + `resize`) as `--rail-zoom-factor` and keeps at least 48 window pixels when + zoomed out. `window-controls.e2e.ts` checks the alignment and the gap at + default and minimum zoom on macOS. 2. Windows and Linux use renderer-drawn window controls (`app-window-controls`, `libs/ui/components/src/lib/window-controls/`). `frame` is intentionally left untouched so native resize borders and diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.html index 04dadb780..13e259f50 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.html @@ -1,6 +1,10 @@ -