Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 12 additions & 4 deletions desktop/src/app/AppTopChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/sidebar/ui/WorkspaceRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
10 changes: 7 additions & 3 deletions desktop/src/shared/layout/chromeLayout.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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. */
Expand Down
138 changes: 138 additions & 0 deletions desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading