feat(layout): full-height collapsible filter sidebar; sync status + scope relocated
Restructure the app shell into three top-level columns: - The per-user sync status (video counts + live sync state) moves from the top bar to a compact block at the top of the left nav rail (icon-only with a tooltip when collapsed). - The feed's Mine/Library scope toggle moves to the top of the filter sidebar. - The filter sidebar becomes a full-height sibling column with its own collapse control (a thin rail carrying the active-filter count), mirroring the nav rail. The top bar is now just the feed search / page title. - Both panels' collapsed state is persisted to the user's preferences (server-side, so it follows the account across devices), seeded from a localStorage cache to avoid a flash. Default: both panels open.
This commit is contained in:
parent
c6fe94450b
commit
072b3296a3
9 changed files with 255 additions and 78 deletions
|
|
@ -4,13 +4,17 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
EyeOff,
|
||||
GripVertical,
|
||||
Library,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Share2,
|
||||
SlidersHorizontal,
|
||||
User,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
|
|
@ -120,6 +124,8 @@ export default function Sidebar({
|
|||
setLayout,
|
||||
onFocusChannel,
|
||||
isDemo = false,
|
||||
collapsed,
|
||||
onToggleCollapse,
|
||||
}: {
|
||||
filters: FeedFilters;
|
||||
setFilters: (f: FeedFilters) => void;
|
||||
|
|
@ -127,6 +133,10 @@ export default function Sidebar({
|
|||
setLayout: (l: SidebarLayout) => void;
|
||||
onFocusChannel: (name: string) => void;
|
||||
isDemo?: boolean;
|
||||
// Full-height collapse (state owned by App, persisted to the user's preferences), mirroring
|
||||
// the left nav rail.
|
||||
collapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tagsQuery = useQuery({ queryKey: ["tags"], queryFn: api.tags });
|
||||
|
|
@ -464,16 +474,81 @@ export default function Sidebar({
|
|||
? orderedAvailable
|
||||
: orderedAvailable.filter((id) => !layout.hidden[id]);
|
||||
|
||||
return (
|
||||
<aside className="w-64 shrink-0 border-r border-border bg-surface/40 overflow-y-auto p-4 space-y-3 hidden md:block">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-semibold">
|
||||
{t("sidebar.filters")}
|
||||
// Collapsed: a thin rail (mirroring the nav) — an expand control plus a filter icon that
|
||||
// carries the active-filter count so you can tell filters are on without opening it.
|
||||
if (collapsed) {
|
||||
return (
|
||||
<aside className="hidden md:flex w-[46px] shrink-0 flex-col items-center gap-2 border-r border-border bg-surface/40 py-3">
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
title={t("sidebar.expandPanel")}
|
||||
aria-label={t("sidebar.expandPanel")}
|
||||
className="p-1 rounded-md text-muted hover:text-fg hover:bg-card transition"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
title={t("sidebar.filters")}
|
||||
className="relative p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition"
|
||||
>
|
||||
<SlidersHorizontal className="w-5 h-5" />
|
||||
{activeCount > 0 && (
|
||||
<span className="ml-1.5 text-xs font-medium text-accent">
|
||||
{t("sidebar.activeCount", { count: activeCount })}
|
||||
<span className="absolute -top-1 -right-1 min-w-[16px] h-4 px-1 rounded-full bg-accent text-accent-fg text-[10px] font-bold leading-none grid place-items-center ring-2 ring-bg">
|
||||
{activeCount > 9 ? "9+" : activeCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="hidden md:block w-64 shrink-0 border-r border-border bg-surface/40 overflow-y-auto p-4 space-y-3">
|
||||
{/* Scope: your own subscriptions (Mine) vs the shared Library — moved out of the top bar. */}
|
||||
<div
|
||||
className="flex items-center rounded-full border border-border bg-card p-0.5 text-xs"
|
||||
role="group"
|
||||
aria-label={t("header.scope.label")}
|
||||
>
|
||||
{(["my", "all"] as const).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setFilters({ ...filters, scope: s })}
|
||||
title={t("header.scope." + s + "Tip")}
|
||||
aria-pressed={filters.scope === s}
|
||||
className={`flex-1 inline-flex items-center justify-center gap-1 px-2.5 py-1 rounded-full transition ${
|
||||
filters.scope === s ? "bg-accent text-accent-fg" : "text-muted hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
{s === "my" ? (
|
||||
<User className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Library className="w-3.5 h-3.5" />
|
||||
)}
|
||||
<span>{t("header.scope." + s)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
title={t("sidebar.collapsePanel")}
|
||||
aria-label={t("sidebar.collapsePanel")}
|
||||
className="shrink-0 -ml-1 p-1 rounded-md text-muted hover:text-fg hover:bg-card transition"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="text-sm font-semibold truncate">
|
||||
{t("sidebar.filters")}
|
||||
{activeCount > 0 && (
|
||||
<span className="ml-1.5 text-xs font-medium text-accent">
|
||||
{t("sidebar.activeCount", { count: activeCount })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
{!editing && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue