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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
// 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 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
|
// 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
|
// (title/icon), the active-filter count + Clear-all action, its `items`, and any content below the
|
||||||
// groups (`top`, e.g. the feed's scope toggle) or below (`children`, e.g. the feed's tag manager).
|
// groups (`children`, e.g. the feed's tag manager). `kind` is the PanelId (also the layout key), so
|
||||||
// `kind` is the PanelId (also the layout key), so `top`/`children` default to nothing for the rails
|
// `children` defaults to nothing for the rails that need none.
|
||||||
// that need neither.
|
|
||||||
export default function FilterRail({
|
export default function FilterRail({
|
||||||
kind,
|
kind,
|
||||||
title,
|
title,
|
||||||
@@ -23,7 +22,6 @@ export default function FilterRail({
|
|||||||
layout,
|
layout,
|
||||||
setLayout,
|
setLayout,
|
||||||
items,
|
items,
|
||||||
top,
|
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
kind: PanelId;
|
kind: PanelId;
|
||||||
@@ -36,7 +34,6 @@ export default function FilterRail({
|
|||||||
layout: PanelLayout;
|
layout: PanelLayout;
|
||||||
setLayout: (l: PanelLayout) => void;
|
setLayout: (l: PanelLayout) => void;
|
||||||
items: PanelItem[];
|
items: PanelItem[];
|
||||||
top?: ReactNode;
|
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -54,7 +51,6 @@ export default function FilterRail({
|
|||||||
editHint={t("sidebar.editHint")}
|
editHint={t("sidebar.editHint")}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
>
|
>
|
||||||
{top}
|
|
||||||
<PanelGroups items={items} layout={layout} setLayout={setLayout} editing={editing} />
|
<PanelGroups items={items} layout={layout} setLayout={setLayout} editing={editing} />
|
||||||
{children}
|
{children}
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Me } from "../lib/api";
|
|||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import PlexSearchBox from "./PlexSearchBox";
|
import PlexSearchBox from "./PlexSearchBox";
|
||||||
import PlexScopeToggle from "./PlexScopeToggle";
|
import PlexScopeToggle from "./PlexScopeToggle";
|
||||||
|
import FeedScopeToggle from "./FeedScopeToggle";
|
||||||
import SyncStatus from "./SyncStatus";
|
import SyncStatus from "./SyncStatus";
|
||||||
import { useNavigation } from "./NavigationProvider";
|
import { useNavigation } from "./NavigationProvider";
|
||||||
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
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"
|
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}
|
{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
|
{/* 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. */}
|
the filter sidebar (S3) so the primary "what am I browsing" control is always in reach. */}
|
||||||
{page === "plex" && me.plex_enabled && <PlexScopeToggle />}
|
{page === "plex" && me.plex_enabled && <PlexScopeToggle />}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type ReactNode, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||||
import { qk } from "../lib/queryKeys";
|
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 { api, type FeedFilters, type Tag } from "../lib/api";
|
||||||
import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
|
import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
|
||||||
import { useDebounced } from "../lib/useDebounced";
|
import { useDebounced } from "../lib/useDebounced";
|
||||||
@@ -283,30 +283,6 @@ export default function Sidebar({
|
|||||||
setLayout={setLayout}
|
setLayout={setLayout}
|
||||||
items={items}
|
items={items}
|
||||||
actions={actions}
|
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 && (
|
{tagManagerOpen && (
|
||||||
<TagManager onClose={() => setTagManagerOpen(false)} onFocusChannel={onFocusChannel} />
|
<TagManager onClose={() => setTagManagerOpen(false)} onFocusChannel={onFocusChannel} />
|
||||||
|
|||||||
Reference in New Issue
Block a user