feat(feed): saved views as a header popover + Personal Views modal + a view chip

Retire the standalone 'Saved views' sidebar panel. The saved-views feature now
lives in three lighter surfaces, coordinated by a new SavedViewsProvider (query +
CRUD + the active/sticky view + modal state):

- the filter panel's header ICON opens a quick-pick popover (apply a view, active
  one checked, plus 'Manage views…');
- the header TITLE ('Filters') opens a new Personal Views modal — a master/detail
  manager: left = the reorderable list + a save-current-filters input, right = the
  selected view's name, its captured filters (shown as the same chips the toolbar
  row uses), default toggle, apply, and delete;
- a SelectedViewChip leads the applied-filter row, naming the active view with a
  warn-coloured 'modified' dot once the live filters diverge from it; clicking it
  reopens the picker.

SidePanel/FilterRail gain optional iconExpanded + onTitleClick slots (feed-only,
hidden for the demo account); the collapsed tab keeps its plain pin behaviour.
SavedViewsWidget is deleted and dropped from the panel layout.

Verified live: icon popover lists HH + Manage; applying HH surfaces the chip and
switches scope; removing a filter lights the dirty dot; the title opens the
master/detail modal with the captured-filters summary. 374 vitest, tsc, eslint
(0 err).
This commit is contained in:
2026-08-22 04:03:58 +02:00
parent 829cadf5fb
commit 7ebbf45921
13 changed files with 598 additions and 380 deletions
+13 -4
View File
@@ -9,7 +9,9 @@ import { useDebounced } from "../lib/useDebounced";
import { clearedFilters, useActiveFilters } from "../lib/useActiveFilters";
import { applyDateChip, deriveDateKey } from "../lib/dateFilter";
import TagManager from "./TagManager";
import SavedViewsWidget from "./SavedViewsWidget";
import SavedViewsPopover from "./SavedViewsPopover";
import PersonalViewsModal from "./PersonalViewsModal";
import { useSavedViews } from "./SavedViewsProvider";
import FilterRail from "./FilterRail";
import { type PanelItem } from "./PanelGroups";
import { type MediaSourceKind } from "../lib/mediaSource";
@@ -42,6 +44,7 @@ export default function Sidebar({
}) {
const { t } = useTranslation();
const isDemo = useMe().is_demo;
const { modalOpen, openModal } = useSavedViews();
const filters = useFeedFilters();
const { setFilters } = useFeedFiltersActions();
// "Focus this channel in the manager" (from the tag manager here) lives in ChannelsProvider.
@@ -111,7 +114,6 @@ export default function Sidebar({
}
const available: Record<string, boolean> = {
savedviews: !isDemo,
date: true,
channel: true,
language: languages.length > 0,
@@ -121,8 +123,6 @@ export default function Sidebar({
function widgetBody(id: string): ReactNode {
switch (id) {
case "savedviews":
return <SavedViewsWidget filters={filters} onApply={setFilters} />;
case "channel":
return <ChannelPicker filters={filters} setFilters={setFilters} />;
case "date": {
@@ -278,10 +278,19 @@ export default function Sidebar({
setLayout={setLayout}
items={items}
actions={actions}
// Saved ("personal") views: the header icon opens a quick-pick popover, the title opens the
// full manager modal. Hidden for the demo account (no per-user views). Feed rail only.
iconExpanded={
!isDemo && source === "feed" ? (
<SavedViewsPopover trigger={<SlidersHorizontal className="w-4 h-4" />} />
) : undefined
}
onTitleClick={!isDemo && source === "feed" ? openModal : undefined}
>
{tagManagerOpen && (
<TagManager onClose={() => setTagManagerOpen(false)} onFocusChannel={onFocusChannel} />
)}
{modalOpen && source === "feed" && <PersonalViewsModal />}
</FilterRail>
);
}