Skip to content

Commit 8e92133

Browse files
fix(desktop): reserve the traffic-light lane from the platform
macOS draws the window controls itself, at a fixed physical size, above all web content. The page renders full-bleed beneath them, so it has to reserve that lane — and it did so with five hardcoded CSS pixel values. CSS pixels scale with page zoom and the OS-drawn lights do not, so zooming out shrank the reservation until the lights were drawn over the sidebar toggle, and the header row below sat inside their band. Electron's `titleBarOverlay` publishes the controls' real geometry to the page as the `titlebar-area-*` env vars, which Chromium rescales per zoom so a reservation derived from them holds its physical size. Measured across zoom 0.58-1.2, the reserved area stays within ~0.6 DIP, the residual coming from env values being quantized to whole CSS pixels. Every lane length now derives from those vars, so the login route and the mothership content offset were fixed without being touched — they already read `--desktop-title-bar-height`. Two of the replaced constants were also simply wrong: the platform reports the lane at 38px and the safe area at 81px, against the hand-measured 36 and 83. The toggle keeps a constant physical size beside the lights, expressed as a proportion of the lane rather than in pixels: a px literal would scale with zoom, and calc cannot divide a length by a length to recover a scale factor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C54QHj4WPV777Fq2yRwkcb
1 parent 6edd6e8 commit 8e92133

6 files changed

Lines changed: 87 additions & 32 deletions

File tree

apps/desktop/src/main/window.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ describe('createMainWindow', () => {
135135
>
136136

137137
expect(MockBrowserWindow.lastOptions?.title).toBe('Sim')
138+
// The overlay is what publishes `titlebar-area-*` to the page, so the web
139+
// app can reserve the traffic-light lane from the platform rather than from
140+
// pixels that shrink under page zoom while the OS-drawn lights do not.
141+
expect(MockBrowserWindow.lastOptions).toMatchObject({
142+
titleBarStyle: 'hiddenInset',
143+
titleBarOverlay: true,
144+
trafficLightPosition: { x: 12, y: 12 },
145+
})
138146

139147
const pageTitleHandler = windowEventCalls.find(
140148
([event]) => event === 'page-title-updated'

apps/desktop/src/main/window.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,16 @@ export function createMainWindow(deps: CreateMainWindowDeps): BrowserWindow {
170170
minWidth: MIN_WIDTH,
171171
minHeight: MIN_HEIGHT,
172172
// No separate title bar: the page renders full-bleed to the window's top
173-
// edge with the traffic lights inset over it (Codex-style). Position is
174-
// explicit so the web app can reserve a matching top-left area.
173+
// edge with the traffic lights inset over it (Codex-style).
175174
...(platform === 'darwin'
176175
? {
177176
titleBarStyle: 'hiddenInset' as const,
177+
// Explicit so the published `titlebar-area-*` geometry is stable.
178178
trafficLightPosition: { x: 12, y: 12 },
179+
// Publishes the traffic lights' geometry (81x38 DIP) as the
180+
// `titlebar-area-*` CSS env vars, which Chromium rescales under page
181+
// zoom so the reserved lane holds its physical size.
182+
titleBarOverlay: true,
179183
}
180184
: {}),
181185
show: false,

apps/sim/app/_shell/desktop-title-bar-surfaces.test.ts

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22
* @vitest-environment node
33
*/
44
import { readFileSync } from 'node:fs'
5-
import { resolve } from 'node:path'
65
import { describe, expect, it } from 'vitest'
76

7+
/** Anchored to this file, not `process.cwd()`, which only resolves from `apps/sim`. */
8+
const read = (relativePath: string) => readFileSync(new URL(relativePath, import.meta.url), 'utf8')
9+
10+
const authLayout = read('../(auth)/auth-layout-client.tsx')
11+
const authShell = read('../(auth)/components/auth-shell.tsx')
12+
const workspaceChrome = read(
13+
'../workspace/[workspaceId]/components/workspace-chrome/workspace-chrome.tsx'
14+
)
15+
const sidebar = read('../workspace/[workspaceId]/w/components/sidebar/sidebar.tsx')
16+
const globalStyles = read('../_styles/globals.css')
17+
818
describe('desktop title-bar surface audit', () => {
919
it('applies the safe-area shell only when the auth route is login', () => {
10-
const authLayout = readFileSync(
11-
resolve(process.cwd(), 'app/(auth)/auth-layout-client.tsx'),
12-
'utf8'
13-
)
14-
const authShell = readFileSync(
15-
resolve(process.cwd(), 'app/(auth)/components/auth-shell.tsx'),
16-
'utf8'
17-
)
18-
1920
expect(authLayout).toContain("usePathname() === '/login'")
2021
expect(authLayout).toContain('reserveDesktopTitleBar={isLogin}')
2122
expect(authShell).toContain(
@@ -24,25 +25,44 @@ describe('desktop title-bar surface audit', () => {
2425
})
2526

2627
it('mounts a real drag surface across login and workspace title-bar lanes', () => {
27-
const authShell = readFileSync(
28-
resolve(process.cwd(), 'app/(auth)/components/auth-shell.tsx'),
29-
'utf8'
30-
)
31-
const workspaceChrome = readFileSync(
32-
resolve(
33-
process.cwd(),
34-
'app/workspace/[workspaceId]/components/workspace-chrome/workspace-chrome.tsx'
35-
),
36-
'utf8'
37-
)
38-
const globalStyles = readFileSync(resolve(process.cwd(), 'app/_styles/globals.css'), 'utf8')
3928
const dragRegion = globalStyles.match(/\.desktop-window-drag-region\s*\{([^}]*)\}/)?.[1]
4029

4130
expect(authShell).toContain('desktop-login-window-drag-region')
4231
expect(workspaceChrome).toContain('desktop-workspace-window-drag-region')
43-
expect(workspaceChrome).toContain("isCollapsed ? 'h-9' : 'h-2'")
32+
expect(workspaceChrome).toContain("isCollapsed ? 'h-[var(--desktop-title-bar-height)]' : 'h-2'")
33+
// The sidebar's lane strip composes the same two classes instead of
34+
// re-declaring the drag region inline, which had dropped `user-select: none`.
35+
expect(sidebar).toContain('desktop-window-drag-region desktop-workspace-window-drag-region')
4436
expect(dragRegion).toContain('-webkit-app-region: drag')
4537
expect(globalStyles).toContain('.desktop-workspace-window-drag-region')
4638
expect(globalStyles).toContain('-webkit-app-region: no-drag')
4739
})
40+
41+
// Regression: a px clearance shrinks under page zoom while the OS-drawn lights
42+
// do not, so they end up drawn over the sidebar toggle.
43+
it('reserves the traffic-light lane from the platform, not hardcoded pixels', () => {
44+
expect(globalStyles).toContain('--desktop-title-bar-height: env(titlebar-area-height,')
45+
expect(globalStyles).toContain('--desktop-title-bar-inset-x: env(titlebar-area-x,')
46+
47+
// Scoped to the desktop block: the `:root` zeros are the deliberate
48+
// no-lane case, so only the overrides must stay derived.
49+
const insetBlock = globalStyles.match(
50+
/html\[data-sim-desktop-title-bar="inset"\]\s*\{([^}]*)\}/
51+
)?.[1]
52+
expect(insetBlock).toBeTypeOf('string')
53+
for (const name of [
54+
'--desktop-title-bar-control-size',
55+
'--desktop-title-bar-control-icon-size',
56+
'--desktop-title-bar-control-offset',
57+
]) {
58+
expect(insetBlock).toMatch(new RegExp(`${name}: calc\\(`))
59+
expect(insetBlock).not.toMatch(new RegExp(`${name}:\\s*[\\d.]+px`))
60+
}
61+
62+
// Both lane consumers read those vars; a literal in either is the bug.
63+
expect(workspaceChrome).toContain('left-[var(--desktop-title-bar-inset-x)]')
64+
expect(workspaceChrome).toContain('size-[var(--desktop-title-bar-control-size)]')
65+
expect(sidebar).toContain('[[data-sim-desktop-title-bar=inset]_&]:pt-[var(')
66+
expect(sidebar).not.toMatch(/\[\[data-sim-desktop-title-bar=inset\]_&\]:pt-\d/)
67+
})
4868
})

apps/sim/app/_styles/globals.css

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
--sidebar-width: 0px; /* 0 outside workspace; blocking script always sets actual value on workspace pages */
1414
--sidebar-collapsed-width: 51px; /* icon rail on web; desktop overrides to 0 before first paint */
1515
--desktop-title-bar-height: 0px; /* macOS traffic-light lane; desktop overrides before first paint */
16+
--desktop-title-bar-inset-x: 0px; /* clearance past the traffic lights; desktop overrides */
17+
--desktop-title-bar-control-offset: 0px; /* centres a lane control; desktop overrides */
18+
--desktop-title-bar-control-size: 0px; /* control seated in the lane; desktop overrides */
19+
--desktop-title-bar-control-icon-size: 0px; /* its glyph; desktop overrides */
1620
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
1721
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
1822
--terminal-height: 206px; /* TERMINAL_HEIGHT.DEFAULT */
@@ -41,9 +45,28 @@
4145
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04);
4246
}
4347

48+
/**
49+
* Electron's `titleBarOverlay` publishes the window controls' geometry as these
50+
* `titlebar-area-*` env vars, and Chromium rescales them under page zoom so the
51+
* lane holds its physical size. Fallbacks are the platform's measured values, for
52+
* a shell predating the overlay; the `:root` zeros cover the different case of the
53+
* attribute being absent entirely, where this block never matches.
54+
*/
4455
html[data-sim-desktop-title-bar="inset"] {
4556
--sidebar-collapsed-width: 0px;
46-
--desktop-title-bar-height: 36px;
57+
--desktop-title-bar-height: env(titlebar-area-height, 38px);
58+
--desktop-title-bar-inset-x: env(titlebar-area-x, 81px);
59+
/* 0.79 = 30px of the 38px lane, and 0.53 of that = 16px. Proportions rather
60+
than px because px would scale with page zoom while the OS-drawn lights
61+
would not — and calc cannot divide a length by a length to get a scale.
62+
`navigator.windowControlsOverlay` could, but reading it in JS would race
63+
first paint for a value the blocking script needs. */
64+
--desktop-title-bar-control-size: calc(var(--desktop-title-bar-height) * 0.79);
65+
--desktop-title-bar-control-icon-size: calc(var(--desktop-title-bar-control-size) * 0.53);
66+
--desktop-title-bar-control-offset: calc(
67+
(var(--desktop-title-bar-height) - var(--desktop-title-bar-control-size)) /
68+
2
69+
);
4770
}
4871

4972
/** The macOS desktop login shell reserves the native traffic-light lane. */

apps/sim/app/workspace/[workspaceId]/components/workspace-chrome/workspace-chrome.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export function WorkspaceChrome({
176176
aria-hidden
177177
className={cn(
178178
'desktop-window-drag-region desktop-workspace-window-drag-region',
179-
isCollapsed ? 'h-9' : 'h-2'
179+
isCollapsed ? 'h-[var(--desktop-title-bar-height)]' : 'h-2'
180180
)}
181181
/>
182182
<div
@@ -222,10 +222,10 @@ export function WorkspaceChrome({
222222
<button
223223
type='button'
224224
onClick={toggleSidebar}
225-
className='absolute top-1 left-[83px] z-30 hidden h-[30px] w-[30px] items-center justify-center rounded-lg [-webkit-app-region:no-drag] hover-hover:bg-[var(--surface-active)] [[data-sim-desktop-title-bar=inset]_&]:flex'
225+
className='absolute top-[var(--desktop-title-bar-control-offset)] left-[var(--desktop-title-bar-inset-x)] z-30 hidden size-[var(--desktop-title-bar-control-size)] items-center justify-center rounded-lg transition-colors [-webkit-app-region:no-drag] hover-hover:bg-[var(--surface-active)] [[data-sim-desktop-title-bar=inset]_&]:flex'
226226
aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
227227
>
228-
<PanelLeft className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
228+
<PanelLeft className='size-[var(--desktop-title-bar-control-icon-size)] text-[var(--text-icon)]' />
229229
</button>
230230
</SidebarTooltip>
231231
)}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,9 +1272,9 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) {
12721272
<div className='flex h-full flex-col'>
12731273
<div
12741274
aria-hidden
1275-
className='absolute inset-x-0 top-0 hidden h-9 [-webkit-app-region:drag] [[data-sim-desktop-title-bar=inset]_&]:block'
1275+
className='desktop-window-drag-region desktop-workspace-window-drag-region h-[var(--desktop-title-bar-height)]'
12761276
/>
1277-
<div className='relative flex flex-shrink-0 items-center px-2 pt-3 [[data-sim-desktop-title-bar=inset]_&]:pt-9'>
1277+
<div className='relative flex flex-shrink-0 items-center px-2 pt-3 [[data-sim-desktop-title-bar=inset]_&]:pt-[var(--desktop-title-bar-height)]'>
12781278
<WorkspaceHeader
12791279
activeWorkspace={activeWorkspace}
12801280
workspaceId={workspaceId}
@@ -1312,7 +1312,7 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) {
13121312
aria-label='Collapse sidebar'
13131313
tabIndex={isCollapsed ? -1 : undefined}
13141314
>
1315-
<PanelLeft className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
1315+
<PanelLeft className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
13161316
</button>
13171317
</SidebarTooltip>
13181318
</div>

0 commit comments

Comments
 (0)