feat(toolbar): fixed-width ItemCount pill, lead the toolbar with it, align chrome to the cards

- 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).
This commit is contained in:
2026-08-21 20:33:55 +02:00
parent c3820a0b44
commit 45d8b47366
10 changed files with 88 additions and 34 deletions
+21 -29
View File
@@ -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 = (
<div className="pb-1.5">
<div className="flex items-center gap-1.5 gap-y-2 flex-wrap">
{/* The item count is the fixed leading element — a fixed-width pill so a changing number
never shoves the controls beside it (see ItemCount). */}
<ItemCount count={summaryQuery.data?.count} />
{/* Watch state is single-select (always exactly one); content types are an independent
multi-select. Both compress to one labeled dropdown pill via FilterSelect. */}
<FilterSelect
@@ -596,36 +600,24 @@ export default function Feed({
of this one channel's videos (librarySource is pinned to "all"), so the selector would
only offer a near-empty, confusing slice — hide it here. */}
{!channelScoped && (
<>
<span className="mx-1 h-5 w-px bg-border" aria-hidden="true" />
<FilterSelect
label={t("feed.source.label")}
testId="feed-source"
options={[
{ value: "organic", label: t("feed.source.organic") },
{ value: "all", label: t("feed.source.all") },
{ value: "search", label: t("feed.source.only") },
]}
selected={[filters.librarySource ?? "organic"]}
onChange={(next) =>
setFilters({
...filters,
librarySource: next[0] as FeedFilters["librarySource"],
})
}
/>
</>
)}
<span className="mx-1 h-5 w-px bg-border" aria-hidden="true" />
<span data-testid="feed-result-count" className="text-sm text-muted whitespace-nowrap">
{summaryQuery.data
? t("feed.videoCount", {
count: summaryQuery.data.count,
formattedCount: summaryQuery.data.count.toLocaleString(),
<FilterSelect
label={t("feed.source.label")}
testId="feed-source"
options={[
{ value: "organic", label: t("feed.source.organic") },
{ value: "all", label: t("feed.source.all") },
{ value: "search", label: t("feed.source.only") },
]}
selected={[filters.librarySource ?? "organic"]}
onChange={(next) =>
setFilters({
...filters,
librarySource: next[0] as FeedFilters["librarySource"],
})
: " "}
</span>
<div className="ml-auto flex items-center gap-2">
}
/>
)}
<div className="ml-auto flex items-center gap-1.5">
<SortControl
options={sortOptions}
sort={{ key: sortKey, dir: sortDir }}