fix(zoom) desktop chrome clearance under text zoom (#1490)

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Taylor Ho
2026-07-05 09:37:16 -07:00
committed by GitHub
parent caf644eda7
commit ee4c9ef139
5 changed files with 159 additions and 8 deletions

View File

@@ -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",

View File

@@ -21,10 +21,14 @@ 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]";
const HISTORY_ICON_BUTTON_CLASS =
"h-7 w-6 rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4";
"h-[28px] w-[24px] rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-[16px]";
function preventTopChromeWheel(event: WheelEvent) {
event.preventDefault();
@@ -66,11 +70,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;

View File

@@ -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 (

View File

@@ -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. */

View File

@@ -0,0 +1,138 @@
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;
}
// The chrome buttons are styled to visually match the fixed-size native
// controls, so their box must not follow the rem text scale either. The
// sidebar toggle is 28px square; the back/forward history buttons share the
// height but are deliberately narrower (24px).
const NAV_BUTTON_SIZE = 28;
const HISTORY_BUTTON_WIDTH = 24;
// 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,
) {
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 button = buttons.nth(i);
const label = await button.getAttribute("aria-label");
const isHistoryButton = label === "Go back" || label === "Go forward";
const box = await button.boundingBox();
expect(box).not.toBeNull();
expect(box?.width ?? 0).toBe(
isHistoryButton ? HISTORY_BUTTON_WIDTH : 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,
}) => {
await spoofMacPlatform(page);
await installMockBridge(page);
await page.goto("/");
expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual(
TRAFFIC_LIGHT_RIGHT_EDGE,
);
await expectNavButtonsFixedSize(page);
await expectTopChromeFixedHeight(page);
});
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 seedTextScale(page, 0.75);
await installMockBridge(page);
await page.goto("/");
// Confirm the zoomed-out scale actually applied to the root font size.
await expectRootFontSize(page, "12px");
expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual(
TRAFFIC_LIGHT_RIGHT_EDGE,
);
await expectNavButtonsFixedSize(page);
await expectTopChromeFixedHeight(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);
await expectTopChromeFixedHeight(page);
});
});