authorgravatar for 91240327+shivamhwp@users.noreply.github.comshivam <91240327+shivamhwp@users.noreply.github.com> 2026-09-15 21:37:03+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-15 21:37:03+05:30
logc1b2210411da79350a3f9f5a65a4a64971e4acad
tree4a9b21f1d20b80f776b79a781c319a5b93280cf5
parentae53072af0560c0606202b87b5fdb7e52d48ac5c
signature Signed by PGP key B5690EEEBB952194

fix(desktop): keep the sidebar brand and window buttons aligned (#11906)

Co-authored-by: T3 Code Test <t3code-test@example.com>

5 files changed, 78 insertions(+), 6 deletions(-)

apps/desktop/src/preload.ts+14-1
...@@ -6,7 +6,7 @@ import type {...@@ -6,7 +6,7 @@ import type {
6 DesktopSnapShotEvent,6 DesktopSnapShotEvent,
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import { exposeClerkBridge } from "@clerk/electron/preload";8import { exposeClerkBridge } from "@clerk/electron/preload";
9import { contextBridge, ipcRenderer } from "electron";9import { contextBridge, ipcRenderer, webFrame } from "electron";
1010
11import * as IpcChannels from "./ipc/channels.ts";11import * as IpcChannels from "./ipc/channels.ts";
1212
...@@ -32,6 +32,19 @@ exposeClerkBridge({ passkeys: true });...@@ -32,6 +32,19 @@ exposeClerkBridge({ passkeys: true });
32// oxlint-disable-next-line t3code/no-global-process-runtime -- Electron exposes the client platform in its sandboxed preload process.32// oxlint-disable-next-line t3code/no-global-process-runtime -- Electron exposes the client platform in its sandboxed preload process.
33const clientPlatform = process.platform;33const clientPlatform = process.platform;
3434
35if (clientPlatform === "darwin") {
36 // Native window buttons do not scale with Chromium zoom. Keep their reserved
37 // space in native points, including when a zoomed page is reloaded.
38 const syncWindowControlInset = () => {
39 document.documentElement.style.setProperty(
40 "--desktop-window-controls-inset",
41 `${90 / webFrame.getZoomFactor()}px`,
42 );
43 };
44 window.addEventListener("DOMContentLoaded", syncWindowControlInset, { once: true });
45 window.addEventListener("resize", syncWindowControlInset);
46}
47
35function unwrapEnsureSshEnvironmentResult(result: unknown) {48function unwrapEnsureSshEnvironmentResult(result: unknown) {
36 if (49 if (
37 typeof result === "object" &&50 typeof result === "object" &&
apps/desktop/src/window/DesktopWindow.test.ts+36
...@@ -77,6 +77,7 @@ function makeFakeBrowserWindow() {...@@ -77,6 +77,7 @@ function makeFakeBrowserWindow() {
77 isDestroyed: vi.fn(() => false),77 isDestroyed: vi.fn(() => false),
78 getURL: vi.fn(() => "t3code-dev://app/"),78 getURL: vi.fn(() => "t3code-dev://app/"),
79 getZoomLevel: vi.fn(() => zoomLevel),79 getZoomLevel: vi.fn(() => zoomLevel),
80 getZoomFactor: vi.fn(() => 1.2 ** zoomLevel),
80 setZoomLevel: vi.fn((level: number) => {81 setZoomLevel: vi.fn((level: number) => {
81 zoomLevel = level;82 zoomLevel = level;
82 }),83 }),
...@@ -118,6 +119,7 @@ function makeFakeBrowserWindow() {...@@ -118,6 +119,7 @@ function makeFakeBrowserWindow() {
118 setOpacity: vi.fn(),119 setOpacity: vi.fn(),
119 setTitle: vi.fn(),120 setTitle: vi.fn(),
120 setTitleBarOverlay: vi.fn(),121 setTitleBarOverlay: vi.fn(),
122 setWindowButtonPosition: vi.fn(),
121 show: vi.fn(),123 show: vi.fn(),
122 webContents,124 webContents,
123 };125 };
...@@ -136,6 +138,7 @@ function makeFakeBrowserWindow() {...@@ -136,6 +138,7 @@ function makeFakeBrowserWindow() {
136 reload: webContents.reload,138 reload: webContents.reload,
137 send: webContents.send,139 send: webContents.send,
138 setZoomLevel: webContents.setZoomLevel,140 setZoomLevel: webContents.setZoomLevel,
141 setWindowButtonPosition: window.setWindowButtonPosition,
139 setBackgroundThrottling: webContents.setBackgroundThrottling,142 setBackgroundThrottling: webContents.setBackgroundThrottling,
140 setAutoHideCursor: window.setAutoHideCursor,143 setAutoHideCursor: window.setAutoHideCursor,
141 setFullScreen: window.setFullScreen,144 setFullScreen: window.setFullScreen,
...@@ -742,6 +745,39 @@ describe("DesktopWindow", () => {...@@ -742,6 +745,39 @@ describe("DesktopWindow", () => {
742 }),745 }),
743 );746 );
744747
748 it.effect("keeps macOS window buttons centered when zooming and leaving fullscreen", () =>
749 Effect.gen(function* () {
750 const fakeWindow = makeFakeBrowserWindow();
751 const createCount = yield* Ref.make(0);
752 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
753 const layer = makeTestLayer({ window: fakeWindow.window, createCount, mainWindow });
754
755 yield* Effect.gen(function* () {
756 const desktopWindow = yield* DesktopWindow.DesktopWindow;
757 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
758
759 for (const direction of ["in", "in", "out", "reset", "out"] as const) {
760 yield* desktopWindow.zoomMain(direction);
761 const position = fakeWindow.setWindowButtonPosition.mock.lastCall?.[0];
762 assert.isDefined(position);
763 // The 14-point native buttons should share the zoomed 52px header's center.
764 const headerCenter = 26 * fakeWindow.window.webContents.getZoomFactor();
765 assert.isAtMost(Math.abs(position.y + 7 - headerCenter), 0.5);
766 assert.equal(position.x, 16);
767 }
768
769 fakeWindow.isFullScreen.mockReturnValue(true);
770 fakeWindow.setWindowButtonPosition.mockClear();
771 yield* desktopWindow.zoomMain("reset");
772 assert.equal(fakeWindow.setWindowButtonPosition.mock.calls.length, 0);
773
774 fakeWindow.isFullScreen.mockReturnValue(false);
775 fakeWindow.windowListeners.get("leave-full-screen")?.();
776 assert.deepEqual(fakeWindow.setWindowButtonPosition.mock.lastCall, [{ x: 16, y: 19 }]);
777 }).pipe(Effect.provide(layer));
778 }),
779 );
780
745 it.effect("uses the persisted main window bounds when opening the window", () =>781 it.effect("uses the persisted main window bounds when opening the window", () =>
746 Effect.gen(function* () {782 Effect.gen(function* () {
747 const fakeWindow = makeFakeBrowserWindow();783 const fakeWindow = makeFakeBrowserWindow();
apps/desktop/src/window/DesktopWindow.ts+23-1
...@@ -31,6 +31,22 @@ import * as ElectronApp from "../electron/ElectronApp.ts";...@@ -31,6 +31,22 @@ import * as ElectronApp from "../electron/ElectronApp.ts";
31import { makeQuitShortcutHandler } from "./QuitHold.ts";31import { makeQuitShortcutHandler } from "./QuitHold.ts";
3232
33const TITLEBAR_HEIGHT = 40;33const TITLEBAR_HEIGHT = 40;
34// Matches --workspace-topbar-height in apps/web/src/index.css. Native macOS
35// buttons are 14 points tall and do not scale with the renderer's zoom.
36const MACOS_WORKSPACE_TOPBAR_HEIGHT = 52;
37const MACOS_WINDOW_BUTTON_RADIUS = 7;
38
39function syncMacosWindowButtons(window: Electron.BrowserWindow): void {
40 if (window.isDestroyed() || window.isFullScreen()) return;
41 window.setWindowButtonPosition({
42 x: 16,
43 y: Math.round(
44 (MACOS_WORKSPACE_TOPBAR_HEIGHT * window.webContents.getZoomFactor()) / 2 -
45 MACOS_WINDOW_BUTTON_RADIUS,
46 ),
47 });
48}
49
34const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux50const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux
35const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";51const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";
36const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";52const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";
...@@ -241,7 +257,10 @@ function getWindowTitleBarOptions(...@@ -241,7 +257,10 @@ function getWindowTitleBarOptions(
241 if (platform === "darwin") {257 if (platform === "darwin") {
242 return {258 return {
243 titleBarStyle: "hiddenInset",259 titleBarStyle: "hiddenInset",
244 trafficLightPosition: { x: 16, y: 18 },260 trafficLightPosition: {
261 x: 16,
262 y: MACOS_WORKSPACE_TOPBAR_HEIGHT / 2 - MACOS_WINDOW_BUTTON_RADIUS,
263 },
245 };264 };
246 }265 }
247266
...@@ -660,6 +679,7 @@ export const make = Effect.gen(function* () {...@@ -660,6 +679,7 @@ export const make = Effect.gen(function* () {
660 window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true);679 window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true);
661 });680 });
662 window.on("leave-full-screen", () => {681 window.on("leave-full-screen", () => {
682 syncMacosWindowButtons(window);
663 window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, false);683 window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, false);
664 });684 });
665 }685 }
...@@ -720,6 +740,7 @@ export const make = Effect.gen(function* () {...@@ -720,6 +740,7 @@ export const make = Effect.gen(function* () {
720 clearDevelopmentLoadRetry();740 clearDevelopmentLoadRetry();
721 developmentLoadRetryIndex = 0;741 developmentLoadRetryIndex = 0;
722 window.setTitle(environment.displayName);742 window.setTitle(environment.displayName);
743 if (environment.platform === "darwin") syncMacosWindowButtons(window);
723 });744 });
724 window.webContents.on(745 window.webContents.on(
725 "did-fail-load",746 "did-fail-load",
...@@ -990,6 +1011,7 @@ export const make = Effect.gen(function* () {...@@ -990,6 +1011,7 @@ export const make = Effect.gen(function* () {
990 webContents.setZoomLevel(1011 webContents.setZoomLevel(
991 direction === "reset" ? 0 : webContents.getZoomLevel() + (direction === "in" ? 0.5 : -0.5),1012 direction === "reset" ? 0 : webContents.getZoomLevel() + (direction === "in" ? 0.5 : -0.5),
992 );1013 );
1014 if (environment.platform === "darwin") syncMacosWindowButtons(window.value);
993 // Chromium pushes the new level down to embedded guests, which would zoom1015 // Chromium pushes the new level down to embedded guests, which would zoom
994 // the previewed page along with the app UI. The preview browser keeps its1016 // the previewed page along with the app UI. The preview browser keeps its
995 // own zoom, so put each guest back where the preview left it.1017 // own zoom, so put each guest back where the preview left it.
apps/web/src/components/AppSidebarLayout.tsx+1-1
...@@ -46,7 +46,7 @@ import {...@@ -46,7 +46,7 @@ import {
46} from "./ui/sidebar";46} from "./ui/sidebar";
47import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";47import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
4848
49const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px";49const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "var(--desktop-window-controls-inset, 90px)";
5050
51function subscribeToViewportWidth(onChange: () => void): () => void {51function subscribeToViewportWidth(onChange: () => void): () => void {
52 window.addEventListener("resize", onChange);52 window.addEventListener("resize", onChange);
apps/web/src/components/sidebar/SidebarChrome.tsx+4-3
...@@ -91,11 +91,12 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) {...@@ -91,11 +91,12 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) {
91 )}91 )}
92 to="/"92 to="/"
93 >93 >
94 <span className="inline-flex min-w-0 items-baseline gap-1">94 {/* Center the visible capitals, without the font's ascender/descender space. */}
95 <T3Wordmark aria-label="T3" className="h-2.5 w-auto shrink-0" />95 <span className="inline-flex min-w-0 items-baseline gap-1 text-sm font-medium tracking-tight">
96 <T3Wordmark aria-label="T3" className="h-[1cap] w-auto shrink-0" />
96 <span97 <span
97 className={cn(98 className={cn(
98 "truncate text-sm font-medium tracking-tight",99 "truncate [text-box:trim-both_cap_alphabetic]",
99 onBackdrop ? "text-white/70" : "text-muted-foreground",100 onBackdrop ? "text-white/70" : "text-muted-foreground",
100 )}101 )}
101 >102 >