From 021bfb1ff3caab200ba14e426525880177690ca9 Mon Sep 17 00:00:00 2001 From: thiscallnet Date: Mon, 10 Aug 2026 02:04:20 +0200 Subject: [PATCH] fix(web): prevent Windows titlebar shift after restore --- apps/desktop/src/window/DesktopWindow.ts | 2 +- apps/web/src/components/DiffPanelShell.tsx | 2 +- .../settings/ProjectSettingsPanel.tsx | 2 +- apps/web/src/components/usage/UsagePage.tsx | 2 +- apps/web/src/index.css | 12 +++++ apps/web/src/lib/windowControlsOverlay.ts | 50 +++++++++++++++++-- apps/web/src/rightPanelLayout.ts | 2 +- apps/web/src/routes/settings.tsx | 2 +- 8 files changed, 64 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index bf8c681448f..e975bf5ee41 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -553,7 +553,7 @@ export const make = Effect.gen(function* () { runFork(flushBoundsPersist); }); - if (environment.platform === "darwin") { + if (environment.platform === "darwin" || environment.platform === "win32") { window.on("enter-full-screen", () => { window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true); }); diff --git a/apps/web/src/components/DiffPanelShell.tsx b/apps/web/src/components/DiffPanelShell.tsx index c13af4d9560..8601200369f 100644 --- a/apps/web/src/components/DiffPanelShell.tsx +++ b/apps/web/src/components/DiffPanelShell.tsx @@ -13,7 +13,7 @@ function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) { "flex items-center justify-between gap-2", mode === "embedded" ? "px-2" : "px-4", shouldUseDragRegion - ? "drag-region h-[52px] border-b border-border wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]" + ? "workspace-topbar drag-region border-b border-border wco:pr-[var(--workspace-native-controls-inset)]" : "surface-subheader", ); } diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.tsx b/apps/web/src/components/settings/ProjectSettingsPanel.tsx index 50cf9c31804..6ace78db1a3 100644 --- a/apps/web/src/components/settings/ProjectSettingsPanel.tsx +++ b/apps/web/src/components/settings/ProjectSettingsPanel.tsx @@ -187,7 +187,7 @@ export function ProjectSettingsPage({ projectKey }: { projectKey: string }) { {isElectron && (
diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index b550d673f6e..e20ef5f4cea 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -81,7 +81,7 @@ export function UsagePage() { {isElectron && (
diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 72983930aaf..a7c69c3b969 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1407,6 +1407,18 @@ body { .electron-windows { --desktop-window-right-resize-inset: 6px; + /* Matches DesktopWindow TITLEBAR_HEIGHT; WCO height can be transitional during restore. */ + --workspace-topbar-height: 40px; + --workspace-controls-top: 0px; + --workspace-controls-left: 0.75rem; + --workspace-native-controls-width: calc( + 100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px) + ); +} + +.electron-windows.wco { + --workspace-controls-right: calc(var(--workspace-native-controls-width) + 0.75rem); + --workspace-native-controls-inset: var(--workspace-controls-right); } /* App-chrome grain. Baked into each surface's own background (behind diff --git a/apps/web/src/lib/windowControlsOverlay.ts b/apps/web/src/lib/windowControlsOverlay.ts index 42f9f13c7cd..16a4c2a4249 100644 --- a/apps/web/src/lib/windowControlsOverlay.ts +++ b/apps/web/src/lib/windowControlsOverlay.ts @@ -6,6 +6,7 @@ const ELECTRON_WINDOWS_CLASS_NAME = "electron-windows"; interface WindowControlsOverlayLike { readonly visible: boolean; + readonly getTitlebarAreaRect: () => Pick; addEventListener(type: "geometrychange", listener: EventListener): void; removeEventListener(type: "geometrychange", listener: EventListener): void; } @@ -28,18 +29,59 @@ export function syncDocumentWindowControlsOverlayClass(): () => void { } const overlay = getWindowControlsOverlay(); + if (!overlay) return () => {}; + + const root = document.documentElement; + const isWindows = isWindowsPlatform(navigator.platform); + let wasVisible = overlay.visible; + let hasGeometry = false; + let fullscreen = isWindows && window.desktopBridge?.getWindowFullscreenState?.() === true; + + const applyGeometry = () => { + const rect = overlay.getTitlebarAreaRect(); + const rightInset = window.innerWidth - rect.right; + if (!(rect.width > 0 && rightInset > 0 && rightInset <= window.innerWidth)) return; + + root.style.setProperty("--workspace-native-controls-width", `${rightInset}px`); + hasGeometry = true; + }; + const update = () => { - document.documentElement.classList.toggle(WCO_CLASS_NAME, overlay !== null && overlay.visible); + const visible = overlay.visible; + const preserveWindowsLayout = isWindows && !visible && hasGeometry && !fullscreen; + root.classList.toggle(WCO_CLASS_NAME, visible || preserveWindowsLayout); + + if (!isWindows) return; + + if (!visible) { + wasVisible = false; + return; + } + + if (!wasVisible) { + wasVisible = true; + if (!hasGeometry) applyGeometry(); + return; + } + + applyGeometry(); }; + const stopFullscreenListener = isWindows + ? window.desktopBridge?.onWindowFullscreenStateChange?.((value) => { + fullscreen = value; + update(); + }) + : undefined; + update(); - if (!overlay) { - return () => {}; - } overlay.addEventListener("geometrychange", update); return () => { + stopFullscreenListener?.(); overlay.removeEventListener("geometrychange", update); + root.classList.remove(WCO_CLASS_NAME); + root.style.removeProperty("--workspace-native-controls-width"); }; } diff --git a/apps/web/src/rightPanelLayout.ts b/apps/web/src/rightPanelLayout.ts index 5528072e57b..5768a80041a 100644 --- a/apps/web/src/rightPanelLayout.ts +++ b/apps/web/src/rightPanelLayout.ts @@ -1,3 +1,3 @@ export const RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 980px)"; export const RIGHT_PANEL_SHEET_CLASS_NAME = - "w-[min(42vw,28rem)] min-w-80 max-w-[28rem] p-0 max-[760px]:w-[min(88vw,24rem)] max-[760px]:min-w-0 wco:mt-[env(titlebar-area-height)] wco:h-[calc(100%-env(titlebar-area-height))] wco:max-h-[calc(100%-env(titlebar-area-height))]"; + "w-[min(42vw,28rem)] min-w-80 max-w-[28rem] p-0 max-[760px]:w-[min(88vw,24rem)] max-[760px]:min-w-0 wco:mt-[var(--workspace-topbar-height)] wco:h-[calc(100%-var(--workspace-topbar-height))] wco:max-h-[calc(100%-var(--workspace-topbar-height))]"; diff --git a/apps/web/src/routes/settings.tsx b/apps/web/src/routes/settings.tsx index f14793ba544..99a4c28e984 100644 --- a/apps/web/src/routes/settings.tsx +++ b/apps/web/src/routes/settings.tsx @@ -93,7 +93,7 @@ function SettingsContentLayout() { {isElectron && (