diff --git a/.changes/workspace-rail-traffic-light-gap.md b/.changes/workspace-rail-traffic-light-gap.md new file mode 100644 index 000000000..dcb03ea74 --- /dev/null +++ b/.changes/workspace-rail-traffic-light-gap.md @@ -0,0 +1,6 @@ +--- +type: fix +area: workspace +--- + +On macOS the side rail now starts level with the page content and the dashboard banner, so its first button no longer crowds the window's close, minimize and zoom buttons. diff --git a/apps/electron-backend-e2e/src/window-controls.e2e.ts b/apps/electron-backend-e2e/src/window-controls.e2e.ts index d65b463bd..fba81345f 100644 --- a/apps/electron-backend-e2e/src/window-controls.e2e.ts +++ b/apps/electron-backend-e2e/src/window-controls.e2e.ts @@ -187,3 +187,42 @@ test.describe('Custom window controls', () => { } }); }); + +test.describe('macOS traffic lights', () => { + test.skip( + process.platform !== 'darwin', + 'Only macOS draws the native traffic lights over the rail' + ); + + test('@electron the first rail link starts with the content area, clear of the lights', async ({ + dataDir, + }) => { + const app = await launchElectronApp(dataDir); + + try { + const page = app.mainWindow; + await expect(page.locator('.app-rail')).toHaveClass(/is-macos/); + const firstLink = page.locator('.app-rail a').first(); + await expect(firstLink).toBeVisible(); + + const [linkBox, contentBox] = await Promise.all([ + firstLink.boundingBox(), + page.locator('.workspace-content').boundingBox(), + ]); + // Aligned with the content area, where the dashboard hero starts. + expect( + Math.abs((linkBox?.y ?? 0) - (contentBox?.y ?? -100)) + ).toBeLessThanOrEqual(1); + + 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 + ); + } finally { + await closeElectronApp(app); + } + }); +}); diff --git a/docs/architecture/workspace-shell.md b/docs/architecture/workspace-shell.md index bf70c49cf..6eafed5a5 100644 --- a/docs/architecture/workspace-shell.md +++ b/docs/architecture/workspace-shell.md @@ -326,7 +326,11 @@ The Electron window hides the native title bar on all desktop platforms (`titleBarStyle: 'hidden'` in `apps/electron-backend/src/app/app.ts`): 1. macOS keeps the native traffic lights (`titleBarOverlay: true`, - `trafficLightPosition`); the renderer draws no window buttons. + `trafficLightPosition`); the renderer draws no window buttons. The lights + 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. 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.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.scss index d3e056d90..cad7c1cf6 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-rail/workspace-shell-rail.component.scss @@ -29,8 +29,11 @@ --rail-link-active-border: var(--app-selection-border); --rail-link-active-glow: var(--app-selection-glow); + // The traffic lights sit in the header band (56px, see the shell grid). + // The first link starts below that band, where the content area and the + // dashboard hero start, so its hover surface never touches the lights. &.is-macos { - padding-top: 36px; + padding-top: 56px; } &::-webkit-scrollbar {