From 13b1640f90181aed5938d117d4347009f20e3cf3 Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Fri, 3 Jul 2026 01:26:15 -0700 Subject: [PATCH 1/3] fix(desktop): keep macOS traffic-light clearance in fixed px under text zoom The top-chrome nav row cleared the native close/minimize/maximize buttons with rem-based padding (pl-20 / pl-8), so shrinking text with Cmd-minus scaled the clearance down (5rem -> 60px at 0.75) and slid the sidebar/back/forward buttons under the fixed-size traffic lights. The native controls do not scale with the app's text zoom, so this is one of the rare documented exceptions to the rem-first rule: the clearance is now pl-[80px] / pl-[32px] fixed px. Adds an e2e spec covering both the default zoom and the 0.75 min-zoom collision case (verified red against the old rem-based padding). Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- desktop/playwright.config.ts | 1 + desktop/src/app/AppTopChrome.tsx | 8 ++- .../e2e/top-chrome-zoom-clearance.spec.ts | 64 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 3b5f35d990..45d8fa0096 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -50,6 +50,7 @@ export default defineConfig({ "**/unread-pill.spec.ts", "**/sidebar-more-unread-overlap.spec.ts", "**/home-collapsed-top-chrome.spec.ts", + "**/top-chrome-zoom-clearance.spec.ts", "**/thread-unread.spec.ts", "**/workspace-rail.spec.ts", "**/thread-reply-anchor-roleplay.spec.ts", diff --git a/desktop/src/app/AppTopChrome.tsx b/desktop/src/app/AppTopChrome.tsx index 4270e16815..c40d15692e 100644 --- a/desktop/src/app/AppTopChrome.tsx +++ b/desktop/src/app/AppTopChrome.tsx @@ -64,11 +64,15 @@ export function AppTopChrome({ // x-position. When the workspace rail is present it already occupies the far // left, so the nav row only needs to clear the lights past the rail edge // rather than the full offset. In fullscreen those buttons hide. + // + // Fixed px on purpose: the native traffic lights do not scale with the app's + // Cmd +/- text zoom (rem), so rem-based clearance shrinks under them when + // zoomed out. This is a deliberate exception to the rem-first rule. const macChrome = isMacPlatform() && !isFullscreen; const navRowPaddingClass = macChrome ? hasWorkspaceRail - ? "pl-8" - : "pl-20" + ? "pl-[32px]" + : "pl-[80px]" : "pl-3"; const navRowAlignmentClass = macChrome ? "translate-y-[3px]" : null; diff --git a/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts b/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts new file mode 100644 index 0000000000..af6242aa54 --- /dev/null +++ b/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts @@ -0,0 +1,64 @@ +import { expect, test } from "@playwright/test"; + +import { installMockBridge } from "../helpers/bridge"; + +// The macOS traffic lights are native chrome: with `trafficLightPosition` +// x:16 they occupy roughly x 16–68 regardless of the app's Cmd +/- text +// zoom. The top-chrome nav row must clear that band in fixed px, so the +// clearance cannot shrink when the root font size scales down. +const TRAFFIC_LIGHT_RIGHT_EDGE = 72; + +async function spoofMacPlatform(page: import("@playwright/test").Page) { + await page.addInitScript(() => { + Object.defineProperty(navigator, "platform", { get: () => "MacIntel" }); + }); +} + +async function firstNavButtonX(page: import("@playwright/test").Page) { + const toggle = page.locator('[data-testid="app-top-chrome"] button').first(); + await expect(toggle).toBeVisible(); + const box = await toggle.boundingBox(); + expect(box).not.toBeNull(); + return box?.x ?? 0; +} + +test.describe("top chrome macOS traffic-light clearance under text zoom", () => { + test("nav buttons clear the traffic lights at default zoom", async ({ + page, + }) => { + await spoofMacPlatform(page); + await installMockBridge(page); + await page.goto("/"); + + expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual( + TRAFFIC_LIGHT_RIGHT_EDGE, + ); + }); + + test("nav buttons still clear the traffic lights when zoomed out", async ({ + page, + }) => { + await spoofMacPlatform(page); + // Seed the minimum Cmd- text scale (0.75). The old rem-based clearance + // (pl-20 = 5rem) shrank to 60px here, sliding the buttons under the + // fixed-position native controls. + await page.addInitScript(() => { + window.localStorage.setItem("buzz:text-scale", "0.75"); + }); + await installMockBridge(page); + await page.goto("/"); + + // Confirm the zoomed-out scale actually applied to the root font size. + await expect + .poll(() => + page.evaluate( + () => getComputedStyle(document.documentElement).fontSize, + ), + ) + .toBe("12px"); + + expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual( + TRAFFIC_LIGHT_RIGHT_EDGE, + ); + }); +}); From 06fa87566ddfb2d37dccf95c0c3227ab1d3b8cee Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Fri, 3 Jul 2026 01:49:00 -0700 Subject: [PATCH 2/3] fix(desktop): pin top-chrome nav button size in px under text zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nav buttons beside the macOS traffic lights were sized with rem (h-7 w-7, size-4 glyphs), so Cmd+ text zoom grew them visibly larger than the fixed-size native controls (42px box at 1.5 scale). Pin the button box and glyph to 28px/16px fixed px — the same documented chrome exception as the clearance padding — so the row always matches the traffic-light band. Extends the zoom-clearance e2e to assert a fixed 28px button box at default, 0.75, and 1.5 text scales (verified red against the old rem sizing). Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- desktop/src/app/AppTopChrome.tsx | 6 +- .../e2e/top-chrome-zoom-clearance.spec.ts | 68 ++++++++++++++++--- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/desktop/src/app/AppTopChrome.tsx b/desktop/src/app/AppTopChrome.tsx index c40d15692e..3758580ec4 100644 --- a/desktop/src/app/AppTopChrome.tsx +++ b/desktop/src/app/AppTopChrome.tsx @@ -21,8 +21,12 @@ type AppTopChromeProps = { hasWorkspaceRail?: boolean; }; +// Fixed px on purpose (button box + glyph): these controls sit beside the +// native macOS traffic lights, which ignore the app's Cmd +/- text zoom, so +// the row must not grow or shrink with the rem scale. Deliberate exception +// to the rem-first rule. const TOP_CHROME_ICON_BUTTON_CLASS = - "h-7 w-7 rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4"; + "h-[28px] w-[28px] rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-[16px]"; function preventTopChromeWheel(event: WheelEvent) { event.preventDefault(); diff --git a/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts b/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts index af6242aa54..121b17ecee 100644 --- a/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts +++ b/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts @@ -22,6 +22,44 @@ async function firstNavButtonX(page: import("@playwright/test").Page) { return box?.x ?? 0; } +// The chrome buttons are styled to visually match the fixed-size native +// controls, so their box must not follow the rem text scale either. +const NAV_BUTTON_SIZE = 28; + +async function expectNavButtonsFixedSize( + page: import("@playwright/test").Page, +) { + const buttons = page.locator('[data-testid="app-top-chrome"] button'); + const count = await buttons.count(); + expect(count).toBeGreaterThan(0); + for (let i = 0; i < count; i += 1) { + const box = await buttons.nth(i).boundingBox(); + expect(box).not.toBeNull(); + expect(box?.width ?? 0).toBe(NAV_BUTTON_SIZE); + expect(box?.height ?? 0).toBe(NAV_BUTTON_SIZE); + } +} + +async function seedTextScale( + page: import("@playwright/test").Page, + scale: number, +) { + await page.addInitScript((value) => { + window.localStorage.setItem("buzz:text-scale", String(value)); + }, scale); +} + +async function expectRootFontSize( + page: import("@playwright/test").Page, + fontSize: string, +) { + await expect + .poll(() => + page.evaluate(() => getComputedStyle(document.documentElement).fontSize), + ) + .toBe(fontSize); +} + test.describe("top chrome macOS traffic-light clearance under text zoom", () => { test("nav buttons clear the traffic lights at default zoom", async ({ page, @@ -33,6 +71,7 @@ test.describe("top chrome macOS traffic-light clearance under text zoom", () => expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual( TRAFFIC_LIGHT_RIGHT_EDGE, ); + await expectNavButtonsFixedSize(page); }); test("nav buttons still clear the traffic lights when zoomed out", async ({ @@ -42,23 +81,32 @@ test.describe("top chrome macOS traffic-light clearance under text zoom", () => // Seed the minimum Cmd- text scale (0.75). The old rem-based clearance // (pl-20 = 5rem) shrank to 60px here, sliding the buttons under the // fixed-position native controls. - await page.addInitScript(() => { - window.localStorage.setItem("buzz:text-scale", "0.75"); - }); + await seedTextScale(page, 0.75); await installMockBridge(page); await page.goto("/"); // Confirm the zoomed-out scale actually applied to the root font size. - await expect - .poll(() => - page.evaluate( - () => getComputedStyle(document.documentElement).fontSize, - ), - ) - .toBe("12px"); + await expectRootFontSize(page, "12px"); + + expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual( + TRAFFIC_LIGHT_RIGHT_EDGE, + ); + await expectNavButtonsFixedSize(page); + }); + + test("nav buttons keep their fixed size when zoomed in", async ({ page }) => { + await spoofMacPlatform(page); + // Seed the maximum Cmd+ text scale (1.5). Rem-sized buttons (h-7 = 42px + // here) grew visibly taller than the fixed-size traffic lights. + await seedTextScale(page, 1.5); + await installMockBridge(page); + await page.goto("/"); + + await expectRootFontSize(page, "24px"); expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual( TRAFFIC_LIGHT_RIGHT_EDGE, ); + await expectNavButtonsFixedSize(page); }); }); From ec66d1259b38bb6f4a0ff6a4c49fd7f31dc1ea36 Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Fri, 3 Jul 2026 02:18:07 -0700 Subject: [PATCH 3/3] fix(desktop): pin top-chrome bar height in px under text zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grabber/drag strip hosting the top-left nav buttons was sized by the --buzz-top-chrome-height token with a 2.5rem fallback, so Cmd +/- text zoom ballooned the bar to 60px (1.5) or collapsed it to 30px (0.75) around the now-fixed-size buttons. Pin the token default and the Tailwind fallbacks to 40px (== 2.5rem at default zoom) in chromeLayout.ts and the WorkspaceRail top inset — the same documented macOS chrome exception, one layer up. Extends the zoom-clearance e2e to assert a fixed 40px bar height at default, 0.75, and 1.5 text scales (verified red against the old rem token). Workspace-rail and chrome-backdrop consumers re-checked green. Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../src/features/sidebar/ui/WorkspaceRail.tsx | 2 +- desktop/src/shared/layout/chromeLayout.ts | 10 +++++++--- .../e2e/top-chrome-zoom-clearance.spec.ts | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/desktop/src/features/sidebar/ui/WorkspaceRail.tsx b/desktop/src/features/sidebar/ui/WorkspaceRail.tsx index 7f6f3bdcc4..4911514ac1 100644 --- a/desktop/src/features/sidebar/ui/WorkspaceRail.tsx +++ b/desktop/src/features/sidebar/ui/WorkspaceRail.tsx @@ -141,7 +141,7 @@ export function WorkspaceRail({ // macOS traffic lights overlay the top-left, so start buttons below them (they hide in fullscreen). const topPaddingClass = isMacPlatform() && !isFullscreen - ? "pt-(--buzz-top-chrome-height,2.5rem)" + ? "pt-(--buzz-top-chrome-height,40px)" : "pt-3"; return ( diff --git a/desktop/src/shared/layout/chromeLayout.ts b/desktop/src/shared/layout/chromeLayout.ts index 82ebdbb4e1..5ae0f2f1f6 100644 --- a/desktop/src/shared/layout/chromeLayout.ts +++ b/desktop/src/shared/layout/chromeLayout.ts @@ -1,4 +1,8 @@ -export const TOP_CHROME_HEIGHT_DEFAULT = "2.5rem"; +// Fixed px on purpose: the top chrome strip hosts the fixed-size macOS +// traffic lights and the px-pinned nav buttons (see `AppTopChrome`), so its +// height must not follow the Cmd +/- rem text scale either. Deliberate +// exception to the rem-first rule. 40px == the old 2.5rem at default zoom. +export const TOP_CHROME_HEIGHT_DEFAULT = "40px"; export const CHANNEL_CONTENT_TOP_PADDING_DEFAULT = "5.75rem"; export const chromeCssVars = { @@ -38,9 +42,9 @@ export const topChromeInset = { /** Tailwind class fragments for the global top chrome backdrop strip. */ export const topChromeBackdrop = { /** Height matching the global top chrome search/drag strip. */ - height: "h-(--buzz-top-chrome-height,2.5rem)", + height: "h-(--buzz-top-chrome-height,40px)", /** `after:` pseudo-element offset aligned to the bottom of top chrome. */ - dividerTop: "after:top-(--buzz-top-chrome-height,2.5rem)", + dividerTop: "after:top-(--buzz-top-chrome-height,40px)", } as const; /** Tailwind class fragments for measured channel header chrome. */ diff --git a/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts b/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts index 121b17ecee..bdf5a1193b 100644 --- a/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts +++ b/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts @@ -26,6 +26,21 @@ async function firstNavButtonX(page: import("@playwright/test").Page) { // controls, so their box must not follow the rem text scale either. const NAV_BUTTON_SIZE = 28; +// The grabber/drag strip hosting the buttons must hold its height too — +// otherwise Cmd+ balloons the bar around the fixed-size buttons and Cmd- +// collapses it. +const TOP_CHROME_BAR_HEIGHT = 40; + +async function expectTopChromeFixedHeight( + page: import("@playwright/test").Page, +) { + const bar = page.getByTestId("app-top-chrome"); + await expect(bar).toBeVisible(); + const box = await bar.boundingBox(); + expect(box).not.toBeNull(); + expect(box?.height ?? 0).toBe(TOP_CHROME_BAR_HEIGHT); +} + async function expectNavButtonsFixedSize( page: import("@playwright/test").Page, ) { @@ -72,6 +87,7 @@ test.describe("top chrome macOS traffic-light clearance under text zoom", () => TRAFFIC_LIGHT_RIGHT_EDGE, ); await expectNavButtonsFixedSize(page); + await expectTopChromeFixedHeight(page); }); test("nav buttons still clear the traffic lights when zoomed out", async ({ @@ -92,6 +108,7 @@ test.describe("top chrome macOS traffic-light clearance under text zoom", () => TRAFFIC_LIGHT_RIGHT_EDGE, ); await expectNavButtonsFixedSize(page); + await expectTopChromeFixedHeight(page); }); test("nav buttons keep their fixed size when zoomed in", async ({ page }) => { @@ -108,5 +125,6 @@ test.describe("top chrome macOS traffic-light clearance under text zoom", () => TRAFFIC_LIGHT_RIGHT_EDGE, ); await expectNavButtonsFixedSize(page); + await expectTopChromeFixedHeight(page); }); });