feat(ui): unified floating side panels for filters and playlists

Replace the full-height filter rails with one shared floating glass SidePanel
used by Feed, Plex and Playlists. Each collapses to a slim tab beside the nav;
its sections are reorderable "islands" (drag / hide / per-group collapse) with a
per-panel saved layout; the body hides its scrollbar with a soft top/bottom edge
fade. The nav rail becomes a matching floating rounded card.

- New shared SidePanel / PanelGroup / PanelGroups + useScrollFade hook.
- Generalise the feed-only sidebarLayout into a per-panel panelLayout (feed keeps
  its storage key; plex/playlists get their own; persisted per account).
- Lift the Playlists rail to App level (selected playlist is now App state) so it
  floats and collapses like the filter rails; remove the old CollapsedFilterRail.
- The floating top header's fixed offset accounts for the panels' margins.
This commit is contained in:
2026-07-13 05:13:40 +02:00
parent c2d914bacb
commit facb3ac9cb
18 changed files with 1111 additions and 872 deletions
+11 -2
View File
@@ -29,6 +29,7 @@ import { useLiveQuery } from "../lib/useLiveQuery";
import { getUnreadCount, subscribe } from "../lib/notifications";
import type { Page } from "../lib/urlState";
import { moduleLabelKey, moduleOrder, SYSTEM_PAGES } from "../lib/modules";
import { useScrollFade } from "../lib/useScrollFade";
import { type LangCode } from "../i18n";
import AvatarImg from "./Avatar";
import LanguageSwitcher from "./LanguageSwitcher";
@@ -168,6 +169,10 @@ export default function NavSidebar({
});
const dlActive = Object.values(dlIndexQuery.data ?? {}).filter((s) => s !== "done").length;
// Scroll-affordance for the module list: hidden scrollbar + top/bottom edge fade (shared hook,
// also used by the side panels) so a high-zoom overflow scrolls without a scrollbar.
const listFade = useScrollFade();
type NavItem = { page: Page; icon: typeof Home; label: string; badge?: number };
// Per-page presentation (icon/label/badge). WHICH pages appear and in what order comes from the
// shared moduleOrder(me) — the same source the header's ◀/▶ stepper uses — so the two never drift
@@ -268,7 +273,7 @@ export default function NavSidebar({
return (
<nav
className={`glass relative shrink-0 border-r border-border flex flex-col py-3 transition-[width] ${
className={`glass relative shrink-0 rounded-2xl flex flex-col py-3 m-3 transition-[width] ${
collapsed ? "w-[56px] px-2" : "w-52 px-3"
}`}
aria-label={t("nav.primary")}
@@ -307,7 +312,11 @@ export default function NavSidebar({
/>
</div>
<div className="flex-1 min-h-0 overflow-y-auto flex flex-col gap-1">
<div
ref={listFade.ref}
className="flex-1 min-h-0 overflow-y-auto no-scrollbar flex flex-col gap-1"
style={listFade.style}
>
{userItems.map(renderItem)}
{systemItems.length > 0 &&
(collapsed ? (