From 7f39e7dedaa57b795b2aa9a1f3b5c42482ce2148 Mon Sep 17 00:00:00 2001 From: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc Date: Fri, 19 Jun 2026 09:56:32 -0400 Subject: [PATCH] desktop: keep sidebar unread pill below top chrome strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "more unread above" pill in the sidebar (`MoreUnreadButton` `position="top"`) was anchored to `top-0` inside a column starting at window y=0. On macOS, the 40px top chrome strip with the traffic-light buttons lives at exactly that y range, so the centered pill overlapped the traffic lights when channels were unread above the viewport. Anchor the top variant to `topChromeInset.top` (`top-(--buzz-top-chrome-height,2.5rem)`) so it lines up with the existing `mt-(--buzz-top-chrome-height)` on `SidebarContent` and tracks any future chrome-height changes via the same CSS variable. Also adds a Playwright regression in the smoke project that injects two synthetic pill divs into the sidebar's relative wrapper — one with the legacy `top-0` and one with the fixed `topChromeInset.top` — and asserts the boundingBox y-coordinate sits below the 40px chrome strip in the fixed variant. A new `data-testid="app-sidebar-scroll-anchor"` on that wrapper gives the spec a stable selector without coupling to the shadcn Sidebar internals. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- desktop/playwright.config.ts | 1 + .../src/features/sidebar/ui/AppSidebar.tsx | 5 +- .../features/sidebar/ui/MoreUnreadButton.tsx | 3 +- .../e2e/sidebar-more-unread-overlap.spec.ts | 110 ++++++++++++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 desktop/tests/e2e/sidebar-more-unread-overlap.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index d7e39aeb9d..7f7ccce4fd 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -46,6 +46,7 @@ export default defineConfig({ "**/identity-archive-hide.spec.ts", "**/relay-connectivity-screenshots.spec.ts", "**/unread-pill-screenshots.spec.ts", + "**/sidebar-more-unread-overlap.spec.ts", "**/thread-unread-screenshots.spec.ts", "**/animated-avatar-screenshots.spec.ts", "**/reminders-screenshots.spec.ts", diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 98b8ee6052..8c2a8a8b9d 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -483,7 +483,10 @@ export function AppSidebar({ data-testid="app-sidebar" variant="sidebar" > -
+
{unreadAboveCount > 0 ? ( + ↑ 12 new + +`; + +async function injectSyntheticPill( + page: import("@playwright/test").Page, + topClass: string, + testId: string, +) { + await page.evaluate( + ({ topClass, base, html, testId }) => { + const container = document.querySelector( + '[data-testid="app-sidebar-scroll-anchor"]', + ) as HTMLElement | null; + if (!container) throw new Error("sidebar scroll anchor not found"); + + // Remove any prior injection so we can screenshot variants in sequence. + container + .querySelectorAll("[data-synthetic-more-unread]") + .forEach((el) => { + el.remove(); + }); + + const pill = document.createElement("div"); + pill.dataset.syntheticMoreUnread = "true"; + pill.dataset.testid = testId; + pill.className = `${base} ${topClass}`; + pill.innerHTML = html; + container.appendChild(pill); + }, + { topClass, base: PILL_BASE, html: PILL_INNER_HTML, testId }, + ); +} + +test.describe("sidebar MoreUnreadButton top chrome overlap", () => { + test.beforeEach(async ({ page }) => { + await installMockBridge(page); + await page.goto("/"); + await expect(page.getByTestId("app-sidebar")).toBeVisible(); + }); + + test("before fix: top-0 overlaps traffic-light strip", async ({ page }) => { + await injectSyntheticPill(page, LEGACY_TOP_CLASS, "synthetic-before"); + const pill = page.getByTestId("synthetic-before"); + await expect(pill).toBeVisible(); + + const box = await pill.boundingBox(); + expect(box).not.toBeNull(); + // Pre-fix: pill is anchored at y=0, inside the 40px chrome strip. + expect(box?.y ?? Number.NaN).toBeLessThan(20); + + // Screenshot the top-left corner of the sidebar with the chrome strip + // visible above it. + await page.screenshot({ + path: `${SHOTS}/before-top-0-overlap.png`, + clip: { x: 0, y: 0, width: 320, height: 120 }, + }); + }); + + test("after fix: top-(--buzz-top-chrome-height) clears chrome strip", async ({ + page, + }) => { + await injectSyntheticPill(page, FIXED_TOP_CLASS, "synthetic-after"); + const pill = page.getByTestId("synthetic-after"); + await expect(pill).toBeVisible(); + + const box = await pill.boundingBox(); + expect(box).not.toBeNull(); + // Post-fix: pill sits below the 40px chrome strip. + expect(box?.y ?? Number.NaN).toBeGreaterThanOrEqual(40); + + await page.screenshot({ + path: `${SHOTS}/after-top-chrome-inset.png`, + clip: { x: 0, y: 0, width: 320, height: 120 }, + }); + }); +});