From 45d8b47366d3f1016adddcd7d5a347141ba03b43 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 21 Aug 2026 20:33:55 +0200 Subject: [PATCH] feat(toolbar): fixed-width ItemCount pill, lead the toolbar with it, align chrome to the cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New ItemCount: a generic, FIXED-WIDTH count pill (glass, non-interactive) — "192,050 items" (neutral noun, dynamic plural), min-w-[9rem] so a changing number never shifts the controls beside it; a million or more collapses to "1M+" with the exact figure in the title tooltip. - Feed toolbar: ItemCount is the fixed leading element now; dropped the mx-1 separators so every gap is a uniform 1.5 (Content-types↔Source used to be wider than Watch-state↔Content-types). - Chrome-to-card alignment (app-wide, via the shared Header + PageShell): the floating header and the toolbar band now span exactly the card grid's width — left-4, and a right edge of 1rem + the scrollbar gutter (measured once into --sbw), so the fixed chrome lines up with the scrolled cards instead of running ~15px past them. Wraps cleanly to multiple rows on mobile. Renames the count testid feed-result-count → item-count (E2Es updated). --- frontend/e2e/channel.spec.ts | 2 +- frontend/e2e/feed.spec.ts | 4 +- frontend/src/App.tsx | 13 ++++++ frontend/src/components/Feed.tsx | 50 ++++++++++-------------- frontend/src/components/Header.tsx | 4 +- frontend/src/components/ItemCount.tsx | 35 +++++++++++++++++ frontend/src/components/PageShell.tsx | 4 +- frontend/src/i18n/locales/en/common.json | 2 + frontend/src/i18n/locales/hu/common.json | 2 + frontend/src/index.css | 6 +++ 10 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 frontend/src/components/ItemCount.tsx diff --git a/frontend/e2e/channel.spec.ts b/frontend/e2e/channel.spec.ts index 71479b98..b3b46db9 100644 --- a/frontend/e2e/channel.spec.ts +++ b/frontend/e2e/channel.spec.ts @@ -111,7 +111,7 @@ test.describe("channel view", () => { 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 count = page.getByTestId("item-count"); const title = page.getByTestId("channel-title"); await expect(count).toBeVisible(); await expect(page.getByTestId("video-card").first()).toBeVisible(); diff --git a/frontend/e2e/feed.spec.ts b/frontend/e2e/feed.spec.ts index 61c045d1..55700b6f 100644 --- a/frontend/e2e/feed.spec.ts +++ b/frontend/e2e/feed.spec.ts @@ -8,7 +8,7 @@ test.describe("feed", () => { await openApp(page); await expect(page.getByTestId("video-card").first()).toBeVisible(); - const count = page.getByTestId("feed-result-count"); + const count = page.getByTestId("item-count"); await expect(count).toHaveText(/\d/); const baseline = await count.innerText(); @@ -55,7 +55,7 @@ test.describe("feed", () => { // pins in the shell's toolbar band above the scroller, so it stays put while the cards scroll. test("the feed toolbar stays pinned while cards scroll", async ({ page }) => { await openApp(page); - const count = page.getByTestId("feed-result-count"); + const count = page.getByTestId("item-count"); await expect(count).toBeVisible(); const topBefore = await count.evaluate((el) => el.getBoundingClientRect().top); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5af82e6e..8f625ea6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -136,6 +136,19 @@ export default function App() { if (meQuery.data && getActiveAccount() == null) setActiveAccount(meQuery.data.id); }, [meQuery.data?.id]); + // Measure the scrollbar's gutter width once and publish it as --sbw, so the floating header and the + // toolbar band can pad their right edge to line up with the scrolled cards (the app scroller reserves + // that gutter via scrollbar-gutter: stable). It's a device constant, so a one-shot mount measure is enough. + useEffect(() => { + const probe = document.createElement("div"); + probe.style.cssText = + "position:absolute;top:-9999px;width:100px;height:100px;overflow:scroll;scrollbar-gutter:stable"; + document.body.appendChild(probe); + const w = probe.offsetWidth - probe.clientWidth; + document.body.removeChild(probe); + document.documentElement.style.setProperty("--sbw", `${w}px`); + }, []); + // Once signed in, drop any leftover ONE-TIME query params from the pre-login flow (?access, ?verify, // ?reset) — but NOT the nav/filter params, which the logged-in app now reads from and keeps in the // URL. Gated on auth so the logged-out Welcome page still reads its params first. diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index d9358ab2..2d46c475 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -41,6 +41,7 @@ import { PageToolbar } from "./PageShell"; import { useWizard } from "./WizardProvider"; import SortControl from "./SortControl"; import FilterSelect from "./FilterSelect"; +import ItemCount from "./ItemCount"; import ViewSwitcher from "./ViewSwitcher"; import VirtualFeed from "./VirtualFeed"; // Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened. @@ -570,6 +571,9 @@ export default function Feed({ const toolbar = (
+ {/* The item count is the fixed leading element — a fixed-width pill so a changing number + never shoves the controls beside it (see ItemCount). */} + {/* Watch state is single-select (always exactly one); content types are an independent multi-select. Both compress to one labeled dropdown pill via FilterSelect. */} -