feat(feed): move the Mine/Library scope toggle beside the header search box

The scope toggle used to sit at the top of the filter rail, which is buried when
the rail is collapsed/unpinned. Relocate it to the header, just right of the feed
search box — mirroring Plex's scope toggle placement — so the primary 'what am I
browsing' control is always visible.

New FeedScopeToggle adapter (SegmentedToggle over filters.scope, same as the Plex
one over its scope); rendered in Header on the feed page between the search box and
the sync chip. Removed the toggle from the Sidebar rail's now-unused `top` slot and
dropped the dead `top` prop from FilterRail. Same filters.scope field, so behaviour
is unchanged.

Verified live: the toggle sits beside the feed search and switches scope; the rail
no longer carries it.
This commit is contained in:
2026-08-22 03:29:15 +02:00
parent af08a7850b
commit 8f08073564
4 changed files with 47 additions and 32 deletions
@@ -0,0 +1,39 @@
import { useTranslation } from "react-i18next";
import { Library, User } from "lucide-react";
import SegmentedToggle from "./SegmentedToggle";
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
// The feed's Mine/Library scope, relocated from the filter sidebar to sit just right of the header
// search box — mirroring PlexScopeToggle's placement, so the primary "what am I browsing" control is
// always in reach instead of buried in the (possibly collapsed) filter rail. Same `filters.scope`
// field, so behaviour is unchanged. The `pointer-events-auto`/`shrink-0` wrapper matches the other
// header pills: the header band is `pointer-events-none`, and the pill must not collapse in the flex row.
export default function FeedScopeToggle() {
const { t } = useTranslation();
const filters = useFeedFilters();
const { setFilters } = useFeedFiltersActions();
return (
<div className="pointer-events-auto shrink-0">
<SegmentedToggle
size="md"
ariaLabel={t("header.scope.label")}
options={[
{
value: "my",
label: t("header.scope.my"),
icon: <User className="w-3.5 h-3.5" />,
title: t("header.scope.myTip"),
},
{
value: "all",
label: t("header.scope.all"),
icon: <Library className="w-3.5 h-3.5" />,
title: t("header.scope.allTip"),
},
]}
value={filters.scope}
onChange={(scope) => setFilters({ ...filters, scope })}
/>
</div>
);
}
+3 -7
View File
@@ -8,10 +8,9 @@ import PanelGroups, { type PanelItem } from "./PanelGroups";
// chrome around a PanelGroups body: the feed Sidebar, the PlexSidebar, and the PlaylistsRail. It owns
// the customize (reorder/hide) `editing` state and the fixed title-bar wiring (edit toggle, reset to
// the layout default, edit hint); each rail supplies only what actually differs — its identity
// (title/icon), the active-filter count + Clear-all action, its `items`, and any content above the
// groups (`top`, e.g. the feed's scope toggle) or below (`children`, e.g. the feed's tag manager).
// `kind` is the PanelId (also the layout key), so `top`/`children` default to nothing for the rails
// that need neither.
// (title/icon), the active-filter count + Clear-all action, its `items`, and any content below the
// groups (`children`, e.g. the feed's tag manager). `kind` is the PanelId (also the layout key), so
// `children` defaults to nothing for the rails that need none.
export default function FilterRail({
kind,
title,
@@ -23,7 +22,6 @@ export default function FilterRail({
layout,
setLayout,
items,
top,
children,
}: {
kind: PanelId;
@@ -36,7 +34,6 @@ export default function FilterRail({
layout: PanelLayout;
setLayout: (l: PanelLayout) => void;
items: PanelItem[];
top?: ReactNode;
children?: ReactNode;
}) {
const { t } = useTranslation();
@@ -54,7 +51,6 @@ export default function FilterRail({
editHint={t("sidebar.editHint")}
actions={actions}
>
{top}
<PanelGroups items={items} layout={layout} setLayout={setLayout} editing={editing} />
{children}
</SidePanel>
+4
View File
@@ -5,6 +5,7 @@ import type { Me } from "../lib/api";
import SearchBar from "./SearchBar";
import PlexSearchBox from "./PlexSearchBox";
import PlexScopeToggle from "./PlexScopeToggle";
import FeedScopeToggle from "./FeedScopeToggle";
import SyncStatus from "./SyncStatus";
import { useNavigation } from "./NavigationProvider";
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
@@ -98,6 +99,9 @@ const Header = forwardRef<
className="pointer-events-none absolute top-3 left-4 right-[calc(1rem+var(--sbw))] mx-auto max-w-[var(--content-mw,none)] z-chrome flex flex-wrap items-center justify-center gap-x-3 gap-y-2"
>
{search}
{/* Mine/Library scope sits right of the feed search box — relocated here from the filter sidebar
so the primary "what am I browsing" control is always in reach (mirrors Plex's scope toggle). */}
{page === "feed" && <FeedScopeToggle />}
{/* Library scope (movie/show/both) sits right of the search box on Plex — relocated here from
the filter sidebar (S3) so the primary "what am I browsing" control is always in reach. */}
{page === "plex" && me.plex_enabled && <PlexScopeToggle />}
+1 -25
View File
@@ -2,7 +2,7 @@ import { type ReactNode, useState } from "react";
import { useTranslation } from "react-i18next";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import { Library, Pencil, SlidersHorizontal, User } from "lucide-react";
import { Pencil, SlidersHorizontal } from "lucide-react";
import { api, type FeedFilters, type Tag } from "../lib/api";
import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
import { useDebounced } from "../lib/useDebounced";
@@ -283,30 +283,6 @@ export default function Sidebar({
setLayout={setLayout}
items={items}
actions={actions}
top={
/* Scope: your own subscriptions (Mine) vs the shared Library. */
<SegmentedToggle
ariaLabel={t("header.scope.label")}
fullWidth
size="md"
options={[
{
value: "my",
label: t("header.scope.my"),
icon: <User className="w-3.5 h-3.5" />,
title: t("header.scope.myTip"),
},
{
value: "all",
label: t("header.scope.all"),
icon: <Library className="w-3.5 h-3.5" />,
title: t("header.scope.allTip"),
},
]}
value={filters.scope}
onChange={(scope) => setFilters({ ...filters, scope })}
/>
}
>
{tagManagerOpen && (
<TagManager onClose={() => setTagManagerOpen(false)} onFocusChannel={onFocusChannel} />