feat(channels): the band's tag chips and status row fold on a short window (B4)

Every other page band folds its filter row below ~600px of height; the Channels band never did, so
its tag chip lane and status / pager row kept a short laptop window for themselves. They fold behind
one "Filters · n" (n = picked tags + other active column filters); the tabs and the TAGS editor stay,
and a folded table keeps its controls at the list's end.
This commit is contained in:
2026-09-27 15:06:32 +02:00
parent 5d1065c1bc
commit eb339195c7
2 changed files with 29 additions and 5 deletions
+16 -5
View File
@@ -37,7 +37,9 @@ import ChannelStatsPopover from "./ChannelStatsPopover";
import Pager from "./ui/Pager";
import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout";
import { sortRows } from "../lib/columnSort";
import { filterRows } from "../lib/tableColumns";
import { filterRows, isActive } from "../lib/tableColumns";
import { useFilterFold } from "../lib/useFilterFold";
import { FilterFoldButton } from "./ui/FilterFold";
import { useCardPager } from "../lib/useCardPager";
import { PageToolbar } from "./ui/PageShell";
import { PageMenu } from "./ui/PageMenu";
@@ -243,6 +245,13 @@ export default function Channels() {
// funnel share ONE state and stay in sync (bidirectional). Toggling a chip rewrites `filters.tags`;
// filterRows / DataTable then applies it. Channel-name search stays separate (the shared top box).
const tagFilter = Array.isArray(filters.tags) ? filters.tags : [];
// B4 (E28 review L7): on a short window the band's filters — the tag chip lane and the status /
// pager row — fold behind one "Filters · n"; the tabs and the TAGS editor stay. n = each picked tag,
// plus every other active column filter (the status chips are the Sync column's).
const fold = useFilterFold();
const foldCount =
tagFilter.length +
Object.entries(filters).filter(([k, v]) => k !== "tags" && isActive(v)).length;
// Picking a real tag drops the "No tag" sentinel (item B) — they're mutually exclusive, as in the feed.
const toggleTagFilter = (tagId: number) => {
const id = String(tagId);
@@ -609,8 +618,9 @@ export default function Channels() {
{t("channels.tags.label")}
</Button>
</Tooltip>
<TagFilterChips chips={tagFilterChips} className="flex-1" />
{tagFilter.length > 0 && (
<FilterFoldButton fold={fold} label={t("sidebar.filters")} count={foldCount} />
{!fold.folded && <TagFilterChips chips={tagFilterChips} className="flex-1" />}
{!fold.folded && tagFilter.length > 0 && (
<Button
variant="link-muted"
onClick={() => setFilters({ ...filters, tags: [] })}
@@ -646,7 +656,7 @@ export default function Channels() {
)}
{/* In table mode the status chips + pager ride in DataTable's own in-band controls row. The card
layout has no such row, so surface the same status chips (left) and its pager (right) here. */}
{view === "subscribed" && layout === "cards" && (
{view === "subscribed" && layout === "cards" && !fold.folded && (
<div className="px-4 pb-2 max-w-384 mx-auto flex items-center justify-between gap-3 flex-wrap">
{statusChips}
{cardPager}
@@ -717,7 +727,8 @@ export default function Channels() {
columns={columns}
rowKey={(c) => c.id}
persistKey={accountKey(LS.channelsTable) ?? undefined}
controlsPosition="both"
// Folded, the status / pager row leaves the band; the list's own end keeps it.
controlsPosition={fold.folded ? "bottom" : "both"}
controlsLeading={statusChips}
controlsInBand
controlsBandClassName="px-4 max-w-384 mx-auto"
@@ -22,3 +22,16 @@ describe("the Channels tag row", () => {
expect(block).toContain("@max-[40rem]/tagrow:basis-full");
});
});
// E28 review L7: every other page band folds its filter row on a short window (B4, `useFilterFold`);
// the Channels band never did — its tag chips and the status / pager row kept a laptop's short window
// for themselves. They fold behind one "Filters · n", the tabs and the TAGS editor stay.
describe("the Channels band on a short window", () => {
it("folds the tag chip lane and the status / pager row behind the fold button", () => {
expect(src).toContain("const fold = useFilterFold();");
expect(src).toMatch(/<FilterFoldButton\s[^>]*fold=\{fold\}/);
expect(src).toMatch(/!fold\.folded && <TagFilterChips/);
expect(src).toMatch(/controlsPosition=\{fold\.folded \? "bottom" : "both"\}/);
expect(src).toMatch(/layout === "cards" && !fold\.folded/);
});
});