feat(feed): removable active-filter chips above the feed

With the filter panel now collapsed to a tab, there was no way to see what was
narrowing the feed without opening it. Every applied filter is now a chip you
can drop on its own, with a Clear all when more than one is on.

The panel's count badge and the chip row read the SAME list (lib/useActiveFilters),
so they can't disagree about what's applied; that list also owns the reset both
"Clear all"s share. Replaces the hand-summed activeCount in Sidebar.
This commit is contained in:
2026-07-15 22:11:44 +02:00
parent 837b48cbfa
commit 644d65a3fd
6 changed files with 212 additions and 28 deletions
+5 -28
View File
@@ -7,6 +7,7 @@ import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
import { shareUrl } from "../lib/urlState";
import { notify } from "../lib/notifications";
import { useDebounced } from "../lib/useDebounced";
import { clearedFilters, useActiveFilters } from "../lib/useActiveFilters";
import TagManager from "./TagManager";
import SavedViewsWidget from "./SavedViewsWidget";
import SidePanel from "./SidePanel";
@@ -21,18 +22,6 @@ const DATE_PRESETS: { days: number; key: string }[] = [
{ days: 365, key: "1year" },
];
// Filter values owned by the sidebar (everything except the header search `q` and the
// `scope` mode, both preserved across a "Clear all").
const DEFAULT_SIDEBAR_FILTERS: Omit<FeedFilters, "q" | "scope"> = {
tags: [],
tagMode: "or",
sort: "newest",
includeNormal: true,
includeShorts: false,
includeLive: false,
show: "unwatched",
};
function TagChip({
tag,
count,
@@ -143,21 +132,9 @@ export default function Sidebar({
});
}
const dateActive = !!filters.maxAgeDays || !!filters.dateFrom || !!filters.dateTo;
const contentChanged =
!filters.includeNormal || filters.includeShorts || filters.includeLive;
const scopeActive = filters.scope === "all";
const sourceActive =
filters.scope === "all" && !!filters.librarySource && filters.librarySource !== "organic";
const activeCount =
filters.tags.length +
(filters.show !== "unwatched" ? 1 : 0) +
(filters.sort !== "newest" && filters.sort !== "relevance" ? 1 : 0) +
(contentChanged ? 1 : 0) +
(filters.channelId ? 1 : 0) +
(dateActive ? 1 : 0) +
(scopeActive ? 1 : 0) +
(sourceActive ? 1 : 0);
// The badge counts exactly what the chip row above the feed shows — same list, so they can't
// disagree about what's applied.
const activeCount = useActiveFilters(filters, setFilters).length;
const active = activeCount > 0;
async function shareView() {
@@ -171,7 +148,7 @@ export default function Sidebar({
function clearAll() {
setCustomDates(false);
setFilters({ ...DEFAULT_SIDEBAR_FILTERS, q: filters.q, scope: "my" });
setFilters(clearedFilters(filters));
}
const available: Record<string, boolean> = {