feat(shell): channel chrome is fully fixed, only the grid scrolls

Reworks the channel detail per UAT: instead of the banner scrolling away
and a sticky sub-header (which felt odd to scroll and needed an opaque bar
that clashed with the design even at rest), the ENTIRE chrome — banner,
identity, tabs, toolbar — is fixed and only the video grid scrolls.

Mechanism: ChannelPage wraps its whole chrome in <PageToolbar> so it
portals into the shell's fixed band (like the feed toolbar); the
channelScoped Feed portals its toolbar into a slot at the bottom of that
chrome. No sticky, no opaque bar — the chrome is transparent over the page
bg because nothing scrolls under it (the band sits above the scroller).

E2E updated: the channel chrome (title + toolbar) stays at a fixed
viewport position while the grid scrolls. Suite 15/15, tsc clean.
This commit is contained in:
2026-07-18 04:53:49 +02:00
parent 0e03e561eb
commit ec807c8b31
3 changed files with 26 additions and 23 deletions
+9 -8
View File
@@ -56,15 +56,17 @@ test.describe("channel view", () => {
.toBeGreaterThan(saved - 500);
});
// Platform S4b: the channel page's banner/identity scroll away, but the tabs + feed toolbar stick
// to the top (a sticky sub-header) so only the video grid scrolls.
test("tabs + toolbar stay pinned while the channel videos scroll", async ({ page }) => {
// Platform S4b: the whole channel chrome (banner, identity, tabs, toolbar) is fixed — it stays
// put while only the video grid scrolls.
test("the channel chrome stays fixed while the video grid scrolls", async ({ page }) => {
await openApp(page);
await page.getByTestId("video-card-channel").first().click();
const count = page.getByTestId("feed-result-count");
const title = page.getByTestId("channel-title");
await expect(count).toBeVisible();
await expect(page.getByTestId("video-card").first()).toBeVisible();
await expect(page.getByTestId("channel-title")).toBeInViewport();
const titleTop = await title.evaluate((el) => el.getBoundingClientRect().top);
const countTop = await count.evaluate((el) => el.getBoundingClientRect().top);
// Keep nudging until the grid is tall enough to actually scroll (it may still be filling in).
await expect
@@ -74,9 +76,8 @@ test.describe("channel view", () => {
})
.toBeGreaterThan(400);
// Banner/identity scrolled away, but the tabs+toolbar stuck near the top.
await expect(page.getByTestId("channel-title")).not.toBeInViewport();
await expect(count).toBeInViewport();
expect(await count.evaluate((el) => el.getBoundingClientRect().top)).toBeLessThan(130);
// The chrome didn't move — banner title and toolbar are in exactly the same place.
expect(await title.evaluate((el) => el.getBoundingClientRect().top)).toBe(titleTop);
expect(await count.evaluate((el) => el.getBoundingClientRect().top)).toBe(countTop);
});
});