fix(app): give the collapsed sidebar toggle room next to the traffic lights (#56988)

This commit is contained in:
Kit Langton
2026-10-19 13:24:33 -07:00
committed by GitHub
Unverified
parent 3d9fec7f8c
commit 7920dfcb4b
5 changed files with 123 additions and 4 deletions
+28
View File
@@ -435,6 +435,34 @@ for (const [screen, href] of [
})
}
// macOS 26 traffic lights end 74px from the window edge. The collapsed toggle starts 16px past them, as the horizontal
// titlebar's first control does, and a fullscreen window has no traffic lights to clear.
for (const [window, fullscreen, start] of [
["windowed", false, 90],
["fullscreen", true, 18],
] as const) {
test(`collapsed sidebar toggle clears the macOS traffic lights (${window})`, async ({ page }) => {
await mockWorkspace(page, {
name: "Tabs",
sessions: [a],
seed: { settings: { appearance: { tabLayout: "vertical", sidebarOpen: false } }, tabs: [a.id] },
})
await page.goto(
`/e2e/utils/macos-window.html?${new URLSearchParams({ server: SERVER, route: sessionHref(a.id), fullscreen: String(fullscreen) })}`,
)
await expectSessionTitle(page, a.title)
const toggle = await page.locator("main").getByRole("button", { name: "Toggle sidebar", exact: true }).boundingBox()
const project = await page.locator('[data-session-title] [data-slot="session-project-trigger"]').boundingBox()
if (!toggle || !project) throw new Error("collapsed sidebar header has no bounding boxes")
expect(toggle.x).toBe(start)
// The header's contents follow the toggle on its center line.
expect(project.x).toBe(start + 32)
expect(project.y + project.height / 2).toBe(toggle.y + toggle.height / 2)
})
}
test("vertical tab gaps hover and select the nearest row", async ({ page }) => {
await mockWorkspace(page, {
name: "Tabs",
@@ -0,0 +1,65 @@
import { MemoryRouter, createMemoryHistory } from "@solidjs/router"
import { render } from "solid-js/web"
import type { Bridge } from "@opencode/gui-extensions/sdk/bridge"
import { AppBaseProviders, AppInterface } from "../../src/app"
import { PlatformProvider, type Platform } from "../../src/runtime/platform/platform"
// The app in a macOS desktop window at `route`, so its layout leaves room for the native traffic lights. A fullscreen
// window has none.
export function mount(input: { server: string; route: string; fullscreen: boolean }) {
const root = document.getElementById("root")
if (!root) throw new Error("Missing fixture root")
const history = createMemoryHistory()
history.set({ value: input.route })
render(() => {
const unused = async () => {
throw new Error("Unexpected fixture action")
}
// No main-process extensions run in this fixture.
const bridge: Bridge = {
packaged: false,
call: unused,
subscribe: async () => ({ available: false }),
on: () => () => undefined,
embed: () => undefined,
capture: async () => undefined,
runMenubarItem: () => undefined,
configure: () => undefined,
manager: {
list: async () => [],
enable: async () => undefined,
disable: async () => undefined,
reload: async () => undefined,
install: async () => undefined,
remove: async () => undefined,
source: async () => "",
asset: () => "",
},
}
const platform: Platform = {
platform: "desktop",
os: "macos",
windowID: "macos-window-test",
windowFullscreen: () => input.fullscreen,
openExternal: () => undefined,
openDirectoryPickerDialog: async () => null,
notify: async () => undefined,
restart: unused,
extensions: bridge,
}
return (
<PlatformProvider value={platform}>
<AppBaseProviders locale="en">
<AppInterface
servers={[{ type: "http", displayName: "Local Server", http: { url: input.server } }]}
router={(props) => <MemoryRouter {...props} history={history} />}
/>
</AppBaseProviders>
</PlatformProvider>
)
}, root)
}
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body class="antialiased overscroll-none overflow-hidden">
<div id="root" class="flex flex-col h-dvh"></div>
<script type="module">
import { mount } from "./macos-window.fixture.tsx"
const query = new URLSearchParams(location.search)
mount({
server: query.get("server"),
route: query.get("route") ?? "/",
fullscreen: query.get("fullscreen") === "true",
})
</script>
</body>
</html>
+11 -4
View File
@@ -9,8 +9,13 @@ import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { useSettings } from "@/settings/model"
// Native controls: 14px left inset, two 20px button pitches, and a 14px button.
export const macTrafficLightsBaseWidth = 68
// The desktop window sets `trafficLightPosition: { x: 14, y: 14 }`. macOS 26 and later draw 14px buttons on a 23px
// pitch, so the zoom button ends 74px from the window edge and the buttons center 21px from the top. macOS 15 and earlier
// end at 68px and center at 22px.
export const macTrafficLightsBaseWidth = 74
// Matches the horizontal titlebar, whose first control starts 16px past the traffic lights.
const macTrafficLightsGap = 16
// The toggle's 28px plus a 4px gap before the header contents.
const slotWidth = 32
@@ -65,9 +70,11 @@ function useCollapsedSlot() {
return {
visible: () => desktop() && !windows() && settings.appearance.tabLayout() !== "horizontal",
open: settings.appearance.sidebarOpen,
// Keep the toggle clear of the traffic lights, which end 68px from the window edge.
// Lays the toggle out as if the traffic lights were the header's first control.
width: () =>
mac() ? macTrafficLightsBaseWidth / (platform.webviewZoom?.() ?? 1) - contentStart + slotWidth : slotWidth,
mac()
? macTrafficLightsBaseWidth / (platform.webviewZoom?.() ?? 1) + macTrafficLightsGap - contentStart + slotWidth
: slotWidth,
}
}
+1
View File
@@ -43,6 +43,7 @@ export default defineConfig({
"e2e/utils/settings-wsl.html",
"e2e/utils/app-direction.html",
"e2e/utils/windows-menu.html",
"e2e/utils/macos-window.html",
],
},
}