From 052be3423f18e28fd9ed0fe98689b015e39c59ec Mon Sep 17 00:00:00 2001 From: Elliot Drel Date: Fri, 30 Oct 2026 23:28:41 -0400 Subject: [PATCH 1/3] fix(web): restoring a minimized Windows window no longer shifts the layout Windows reports an empty window controls overlay while minimized, so the titlebar variables fell back to their defaults and the stale layout showed on restore. Keep the last overlay geometry as the env() fallback instead. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/index.css | 11 ++-- .../lib/windowControlsOverlay.browser.test.ts | 57 +++++++++++++++++++ apps/web/src/lib/windowControlsOverlay.ts | 23 +++++++- 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/lib/windowControlsOverlay.browser.test.ts diff --git a/apps/web/src/index.css b/apps/web/src/index.css index e186516791252..589e6f59a22b0 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -151,16 +151,19 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } } +/* --wco-* hold the last overlay geometry for when env() goes undefined in a minimized window. */ .wco { - --native-titlebar-height: env(titlebar-area-height, 0px); - --workspace-topbar-height: max(40px, env(titlebar-area-height, 52px)); + --native-titlebar-height: env(titlebar-area-height, var(--wco-height, 0px)); + --workspace-topbar-height: max(40px, env(titlebar-area-height, var(--wco-height, 52px))); --workspace-controls-top: env(titlebar-area-y, 0px); --workspace-controls-left: calc(env(titlebar-area-x, 0px) + 0.75rem); --workspace-controls-right: calc( - 100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px) + 0.75rem + 100vw - env(titlebar-area-width, calc(100vw - var(--wco-controls-width, 0px))) - + env(titlebar-area-x, 0px) + 0.75rem ); --workspace-native-controls-inset: calc( - 100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px) + 0.75rem + 100vw - env(titlebar-area-width, calc(100vw - var(--wco-controls-width, 0px))) - + env(titlebar-area-x, 0px) + 0.75rem ); } diff --git a/apps/web/src/lib/windowControlsOverlay.browser.test.ts b/apps/web/src/lib/windowControlsOverlay.browser.test.ts new file mode 100644 index 0000000000000..f86d8fe842db5 --- /dev/null +++ b/apps/web/src/lib/windowControlsOverlay.browser.test.ts @@ -0,0 +1,57 @@ +import { expect, it, vi } from "vite-plus/test"; + +import "~/index.css"; +import { syncDocumentWindowControlsOverlayClass } from "./windowControlsOverlay"; + +vi.mock("~/env", async (importOriginal) => ({ + ...(await importOriginal()), + isElectron: true, +})); + +function fakeOverlay(rect: Pick) { + return Object.assign(new EventTarget(), { + visible: true, + getTitlebarAreaRect: () => rect as DOMRect, + }); +} + +function paintedTitlebarLayout() { + const probe = document.createElement("div"); + probe.style.cssText = + "height: var(--workspace-topbar-height); width: var(--workspace-native-controls-inset)"; + document.body.append(probe); + try { + const { height, width } = probe.getBoundingClientRect(); + return { height, width }; + } finally { + probe.remove(); + } +} + +// The test page has no overlay, so env(titlebar-area-*) is undefined just as in a minimized window. +it("keeps the last overlay geometry while Windows reports an empty overlay", () => { + const overlay = fakeOverlay({ height: 40, right: window.innerWidth - 137 }); + using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Win32"); + using _overlay = vi + .spyOn( + navigator as unknown as { windowControlsOverlay: unknown }, + "windowControlsOverlay", + "get", + ) + .mockReturnValue(overlay); + const root = document.documentElement; + const stop = syncDocumentWindowControlsOverlayClass(); + try { + overlay.visible = false; + overlay.getTitlebarAreaRect = () => new DOMRect(); + overlay.dispatchEvent(new Event("geometrychange")); + + expect(root.classList.contains("wco")).toBe(true); + expect(paintedTitlebarLayout()).toEqual({ height: 40, width: 137 + 12 }); + } finally { + stop(); + root.classList.remove("wco"); + root.style.removeProperty("--wco-height"); + root.style.removeProperty("--wco-controls-width"); + } +}); diff --git a/apps/web/src/lib/windowControlsOverlay.ts b/apps/web/src/lib/windowControlsOverlay.ts index 9b2e90e6a754f..d51f26bc3cdc4 100644 --- a/apps/web/src/lib/windowControlsOverlay.ts +++ b/apps/web/src/lib/windowControlsOverlay.ts @@ -53,12 +53,31 @@ export function syncDocumentWindowControlsOverlayClass(): () => void { } const overlay = getWindowControlsOverlay(); + const root = document.documentElement; + // Windows empties the overlay while minimized, and that layout would flash on restore. + const keepsGeometry = isElectron && isWindowsPlatform(navigator.platform); const update = () => { - document.documentElement.classList.toggle(WCO_CLASS_NAME, overlay !== null && overlay.visible); + const visible = overlay !== null && overlay.visible; + if (!keepsGeometry) { + root.classList.toggle(WCO_CLASS_NAME, visible); + } else if (visible) { + root.classList.add(WCO_CLASS_NAME); + const { height, right } = overlay.getTitlebarAreaRect(); + root.style.setProperty("--wco-height", `${height}px`); + root.style.setProperty("--wco-controls-width", `${window.innerWidth - right}px`); + } }; update(); - return onWindowControlsOverlayGeometryChange(update); + if (keepsGeometry) { + // Maximizing reports the new overlay rect before innerWidth catches up. + window.addEventListener("resize", update); + } + const stopGeometryListener = onWindowControlsOverlayGeometryChange(update); + return () => { + window.removeEventListener("resize", update); + stopGeometryListener(); + }; } function subscribeTitlebarArea(listener: () => void): () => void { From 2917f571b0a8daf75a76ce0146c1c74e66394e9e Mon Sep 17 00:00:00 2001 From: Elliot Drel Date: Sat, 31 Oct 2026 00:03:47 -0400 Subject: [PATCH 2/3] fix(web): use fixed Windows titlebar defaults while minimized Reserve the known top bar height and window button width in CSS instead of saving measured values at runtime. env() still decides the layout whenever the overlay is visible. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/index.css | 13 ++++++---- .../lib/windowControlsOverlay.browser.test.ts | 21 +++++++--------- apps/web/src/lib/windowControlsOverlay.ts | 24 ++++--------------- 3 files changed, 21 insertions(+), 37 deletions(-) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 589e6f59a22b0..53608d1d98f54 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -151,18 +151,18 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } } -/* --wco-* hold the last overlay geometry for when env() goes undefined in a minimized window. */ +/* env() goes undefined in a minimized window, so Windows supplies --wco-fallback-* below. */ .wco { - --native-titlebar-height: env(titlebar-area-height, var(--wco-height, 0px)); - --workspace-topbar-height: max(40px, env(titlebar-area-height, var(--wco-height, 52px))); + --native-titlebar-height: env(titlebar-area-height, var(--wco-fallback-height, 0px)); + --workspace-topbar-height: max(40px, env(titlebar-area-height, var(--wco-fallback-height, 52px))); --workspace-controls-top: env(titlebar-area-y, 0px); --workspace-controls-left: calc(env(titlebar-area-x, 0px) + 0.75rem); --workspace-controls-right: calc( - 100vw - env(titlebar-area-width, calc(100vw - var(--wco-controls-width, 0px))) - + 100vw - env(titlebar-area-width, calc(100vw - var(--wco-fallback-controls-width, 0px))) - env(titlebar-area-x, 0px) + 0.75rem ); --workspace-native-controls-inset: calc( - 100vw - env(titlebar-area-width, calc(100vw - var(--wco-controls-width, 0px))) - + 100vw - env(titlebar-area-width, calc(100vw - var(--wco-fallback-controls-width, 0px))) - env(titlebar-area-x, 0px) + 0.75rem ); } @@ -1758,6 +1758,9 @@ body { .electron-windows { --desktop-window-right-resize-inset: 6px; + /* Matches DesktopWindow TITLEBAR_HEIGHT and the width of the native window buttons at 100% zoom. */ + --wco-fallback-height: 40px; + --wco-fallback-controls-width: 137px; } /* App-chrome grain. Baked into each surface's own background (behind diff --git a/apps/web/src/lib/windowControlsOverlay.browser.test.ts b/apps/web/src/lib/windowControlsOverlay.browser.test.ts index f86d8fe842db5..8752750ac994e 100644 --- a/apps/web/src/lib/windowControlsOverlay.browser.test.ts +++ b/apps/web/src/lib/windowControlsOverlay.browser.test.ts @@ -1,20 +1,16 @@ import { expect, it, vi } from "vite-plus/test"; import "~/index.css"; -import { syncDocumentWindowControlsOverlayClass } from "./windowControlsOverlay"; +import { + syncDocumentElectronPlatformClasses, + syncDocumentWindowControlsOverlayClass, +} from "./windowControlsOverlay"; vi.mock("~/env", async (importOriginal) => ({ ...(await importOriginal()), isElectron: true, })); -function fakeOverlay(rect: Pick) { - return Object.assign(new EventTarget(), { - visible: true, - getTitlebarAreaRect: () => rect as DOMRect, - }); -} - function paintedTitlebarLayout() { const probe = document.createElement("div"); probe.style.cssText = @@ -29,8 +25,8 @@ function paintedTitlebarLayout() { } // The test page has no overlay, so env(titlebar-area-*) is undefined just as in a minimized window. -it("keeps the last overlay geometry while Windows reports an empty overlay", () => { - const overlay = fakeOverlay({ height: 40, right: window.innerWidth - 137 }); +it("keeps the titlebar layout while Windows reports an empty overlay", () => { + const overlay = Object.assign(new EventTarget(), { visible: true }); using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Win32"); using _overlay = vi .spyOn( @@ -40,18 +36,17 @@ it("keeps the last overlay geometry while Windows reports an empty overlay", () ) .mockReturnValue(overlay); const root = document.documentElement; + const stopPlatformClasses = syncDocumentElectronPlatformClasses("Win32"); const stop = syncDocumentWindowControlsOverlayClass(); try { overlay.visible = false; - overlay.getTitlebarAreaRect = () => new DOMRect(); overlay.dispatchEvent(new Event("geometrychange")); expect(root.classList.contains("wco")).toBe(true); expect(paintedTitlebarLayout()).toEqual({ height: 40, width: 137 + 12 }); } finally { stop(); + stopPlatformClasses(); root.classList.remove("wco"); - root.style.removeProperty("--wco-height"); - root.style.removeProperty("--wco-controls-width"); } }); diff --git a/apps/web/src/lib/windowControlsOverlay.ts b/apps/web/src/lib/windowControlsOverlay.ts index d51f26bc3cdc4..ec270687ae2fc 100644 --- a/apps/web/src/lib/windowControlsOverlay.ts +++ b/apps/web/src/lib/windowControlsOverlay.ts @@ -53,31 +53,17 @@ export function syncDocumentWindowControlsOverlayClass(): () => void { } const overlay = getWindowControlsOverlay(); - const root = document.documentElement; - // Windows empties the overlay while minimized, and that layout would flash on restore. - const keepsGeometry = isElectron && isWindowsPlatform(navigator.platform); + // Windows empties the overlay while minimized, and dropping the class would flash on restore. + const keepsClass = isElectron && isWindowsPlatform(navigator.platform); const update = () => { const visible = overlay !== null && overlay.visible; - if (!keepsGeometry) { - root.classList.toggle(WCO_CLASS_NAME, visible); - } else if (visible) { - root.classList.add(WCO_CLASS_NAME); - const { height, right } = overlay.getTitlebarAreaRect(); - root.style.setProperty("--wco-height", `${height}px`); - root.style.setProperty("--wco-controls-width", `${window.innerWidth - right}px`); + if (visible || !keepsClass) { + document.documentElement.classList.toggle(WCO_CLASS_NAME, visible); } }; update(); - if (keepsGeometry) { - // Maximizing reports the new overlay rect before innerWidth catches up. - window.addEventListener("resize", update); - } - const stopGeometryListener = onWindowControlsOverlayGeometryChange(update); - return () => { - window.removeEventListener("resize", update); - stopGeometryListener(); - }; + return onWindowControlsOverlayGeometryChange(update); } function subscribeTitlebarArea(listener: () => void): () => void { From 094584600e53e916c0ea71fcfafb11a4480b4a17 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 11:33:51 +0000 Subject: [PATCH 3/3] fix(web): drop the Windows overlay class in fullscreen Keeping .wco whenever the overlay hides also kept it in fullscreen, which hides the native buttons for real and left an empty controls gap. Keep the class only while the window is not fullscreen. Co-Authored-By: Claude Opus 5.5 --- .../lib/windowControlsOverlay.browser.test.ts | 10 +++++++- apps/web/src/lib/windowControlsOverlay.ts | 23 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/web/src/lib/windowControlsOverlay.browser.test.ts b/apps/web/src/lib/windowControlsOverlay.browser.test.ts index 8752750ac994e..18386647d3f26 100644 --- a/apps/web/src/lib/windowControlsOverlay.browser.test.ts +++ b/apps/web/src/lib/windowControlsOverlay.browser.test.ts @@ -25,8 +25,11 @@ function paintedTitlebarLayout() { } // The test page has no overlay, so env(titlebar-area-*) is undefined just as in a minimized window. -it("keeps the titlebar layout while Windows reports an empty overlay", () => { +it("keeps the titlebar layout while a minimized Windows window reports an empty overlay", () => { const overlay = Object.assign(new EventTarget(), { visible: true }); + let fullscreen = false; + const bridgeWindow = window as { desktopBridge?: unknown }; + bridgeWindow.desktopBridge = { getWindowFullscreenState: () => fullscreen }; using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Win32"); using _overlay = vi .spyOn( @@ -44,7 +47,12 @@ it("keeps the titlebar layout while Windows reports an empty overlay", () => { expect(root.classList.contains("wco")).toBe(true); expect(paintedTitlebarLayout()).toEqual({ height: 40, width: 137 + 12 }); + + fullscreen = true; + window.dispatchEvent(new Event("resize")); + expect(root.classList.contains("wco")).toBe(false); } finally { + delete bridgeWindow.desktopBridge; stop(); stopPlatformClasses(); root.classList.remove("wco"); diff --git a/apps/web/src/lib/windowControlsOverlay.ts b/apps/web/src/lib/windowControlsOverlay.ts index ec270687ae2fc..d17fa744ff5fa 100644 --- a/apps/web/src/lib/windowControlsOverlay.ts +++ b/apps/web/src/lib/windowControlsOverlay.ts @@ -53,17 +53,30 @@ export function syncDocumentWindowControlsOverlayClass(): () => void { } const overlay = getWindowControlsOverlay(); - // Windows empties the overlay while minimized, and dropping the class would flash on restore. - const keepsClass = isElectron && isWindowsPlatform(navigator.platform); + // Windows also empties the overlay while minimized; dropping the class then shifts the layout + // on restore. Fullscreen empties it for real, so only fullscreen may drop it there. + const keepsClassUnlessFullscreen = isElectron && isWindowsPlatform(navigator.platform); const update = () => { const visible = overlay !== null && overlay.visible; - if (visible || !keepsClass) { - document.documentElement.classList.toggle(WCO_CLASS_NAME, visible); + if ( + !visible && + keepsClassUnlessFullscreen && + !window.desktopBridge?.getWindowFullscreenState() + ) { + return; } + document.documentElement.classList.toggle(WCO_CLASS_NAME, visible); }; update(); - return onWindowControlsOverlayGeometryChange(update); + const stopGeometry = onWindowControlsOverlayGeometryChange(update); + if (!keepsClassUnlessFullscreen) return stopGeometry; + // The overlay can hide before the window reports fullscreen; the fullscreen resize settles it. + window.addEventListener("resize", update); + return () => { + stopGeometry(); + window.removeEventListener("resize", update); + }; } function subscribeTitlebarArea(listener: () => void): () => void {