fix(desktop): hide the offcanvas-collapsed sidebar so it stops painting over the community rail (#5947)

## Summary

Collapsing the sidebar left a phantom copy of it painted over the
community/relay rail — opaquely on flat themes (vesper et al., which
made the rail look *removed*), and as ghost fragments (muted search-box
fill, truncated channel-name tails) on the Buzz themes whose chrome is
intentionally transparent for the gradient.

**Cause:** #4281 made the app-sidebar layer `overflow-visible` (the
huddle drawer needs to escape it). That removed the ancestor clipping
the offcanvas collapse relied on: the sidebar slides to `left:
-sidebar-width` but kept painting, exactly over the `z-0` rail (`z-10`
sidebar layer).

**Fix:** the offcanvas-collapsed sidebar container is now `invisible` +
`pointer-events-none`, with `visibility` added to the transition list so
the 200 ms slide-out still animates and the flip happens only at the
transition's end. Theme-independent; no per-theme CSS touched; the
huddle drawer's `overflow-visible` is preserved.

## Before / after

Left 420px of the app with the sidebar collapsed. Before = unpatched
`origin/main` @ 69107dc3b; after = this branch. Same seeded state, same
build pipeline (`build:e2e` between checkouts).

| theme | before (ghost sidebar over the rail) | after (rail clean: A /
B / + visible) |
|---|---|---|
| vesper |
![before-vesper](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--before-vesper.png)
|
![after-vesper](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--after-vesper.png)
|
| buzz |
![before-buzz](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--before-buzz.png)
|
![after-buzz](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--after-buzz.png)
|
| buzz-dark |
![before-buzz-dark](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--before-buzz-dark.png)
|
![after-buzz-dark](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--after-buzz-dark.png)
|

Before shots: ghost `⌘K` search chip + blue active-item pill painted
over the rail column; on vesper the opaque panel hides the rail buttons
entirely. After: the rail's community buttons (A, B) and `+` are visible
and clickable in all three themes.

Reported by Thomas P in #buzz-bugs:
buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=9ea401ca1d009f555ca4324e136f8d8d8156db2f8afa3ff89fd038d2c16260f7

cc @klopez4212 — this touches the layout your #4281/#5478 work shaped;
please confirm it doesn't defeat the huddle drawer or glass intentions.
The change deliberately hides only the *offcanvas-collapsed* container,
nothing in the expanded path.

## Test plan

- [x] New Playwright regression spec `sidebar-offcanvas-rail.spec.ts`
(buzz / buzz-dark / vesper): collapsed sidebar must be `visibility:
hidden` + `pointer-events: none`, community rail stays visible and
interactive. **Fails on unpatched build** (verified), passes with the
fix.
- [x] Full desktop unit suite: 4,954 pass / 0 fail
- [x] `pnpm typecheck`, `pnpm check` (biome + file-size ratchet +
px-text + pubkey-truncation) green
- [x] Before/after screenshots above captured via the e2e harness on
both builds

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Co-authored-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
This commit is contained in:
thomaspblock
2026-08-15 07:49:41 -07:00
committed by GitHub
co-authored by Wintermute
parent 69107dc3bf
commit 78cbffeb64
3 changed files with 81 additions and 3 deletions
+1
View File
@@ -20,6 +20,7 @@ export default defineConfig({
name: "smoke",
testMatch: [
"**/smoke.spec.ts",
"**/sidebar-offcanvas-rail.spec.ts",
"**/search-scope-screenshots.spec.ts",
"**/onboarding-docked-cta-screenshots.spec.ts",
"**/identity-key-help.spec.ts",
+3 -3
View File
@@ -351,7 +351,7 @@ const Sidebar = React.forwardRef<
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
{/* Sidebar gap on desktop; the offcanvas sibling below also goes invisible, else it paints over the community rail past the overflow-visible shell. */}
<div
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
@@ -365,8 +365,8 @@ const Sidebar = React.forwardRef<
/>
<div
className={cn(
"absolute inset-y-0 z-10 hidden h-full w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
"group-data-[resizing=true]:transition-none",
"absolute inset-y-0 z-10 hidden h-full w-(--sidebar-width) transition-[left,right,width,visibility] duration-200 ease-linear md:flex",
"group-data-[resizing=true]:transition-none group-data-[collapsible=offcanvas]:pointer-events-none group-data-[collapsible=offcanvas]:invisible",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
@@ -0,0 +1,77 @@
import { expect, test, type Page } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/sidebar-offcanvas-rail";
const THEME_STORAGE_KEY = "buzz-theme";
const RELAY_URL = "ws://localhost:3000";
const COMMUNITY_A = {
id: "ws-a",
name: "Alpha",
relayUrl: RELAY_URL,
addedAt: "2026-01-01T00:00:00.000Z",
};
const COMMUNITY_B = {
id: "ws-b",
name: "Bravo",
relayUrl: "ws://localhost:3001",
addedAt: "2026-01-02T00:00:00.000Z",
};
async function setup(page: Page, theme: string) {
await page.setViewportSize({ width: 1280, height: 800 });
await page.addInitScript(
({ key, value }) => {
window.localStorage.setItem(key, value);
},
{ key: THEME_STORAGE_KEY, value: theme },
);
await installMockBridge(page, undefined, { skipCommunitySeed: true });
await page.addInitScript(
({ list, active }) => {
window.localStorage.setItem("buzz-communities", JSON.stringify(list));
window.localStorage.setItem("buzz-active-community-id", active);
},
{ list: [COMMUNITY_A, COMMUNITY_B], active: COMMUNITY_A.id },
);
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("community-rail")).toBeVisible();
await expect(page.getByTestId("app-sidebar")).toBeVisible();
}
/**
* Regression: the app-sidebar layer is overflow-visible (huddle drawer), so
* the offcanvas-collapsed sidebar slides out of its container but kept
* painting over the community rail — opaquely on flat themes, as ghost
* fragments on the transparent Buzz chrome. The collapsed sidebar must be
* invisible and non-interactive, leaving the rail clean in every theme.
*/
for (const theme of ["buzz", "buzz-dark", "vesper"]) {
test(`collapsed sidebar leaves the community rail clean — ${theme}`, async ({
page,
}) => {
await setup(page, theme);
await page.screenshot({ path: `${SHOTS}/${theme}-expanded.png` });
await page.locator('[data-sidebar="trigger"]').first().click();
const shell = page.locator(
'[data-state="collapsed"][data-collapsible="offcanvas"]',
);
await expect(shell).toHaveCount(1);
// Let the 200ms slide finish; visibility flips at the transition's end.
await page.waitForTimeout(500);
// Second direct child = the sliding sidebar container (first is the gap).
const offscreenSidebar = shell.locator("> div").nth(1);
await expect(offscreenSidebar).toHaveCSS("visibility", "hidden");
await expect(offscreenSidebar).toHaveCSS("pointer-events", "none");
// The community rail stays visible and interactive beneath it.
await expect(page.getByTestId("community-rail")).toBeVisible();
await expect(
page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`),
).toBeVisible();
await page.screenshot({ path: `${SHOTS}/${theme}-collapsed.png` });
});
}