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:
@@ -0,0 +1,84 @@
|
||||
import { type ReactNode, type CSSProperties } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, Eye, EyeOff, GripVertical } from "lucide-react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
// One collapsible "island" card inside a SidePanel: a titled glass-card with per-group collapse,
|
||||
// and — in the panel's edit mode — a drag handle (reorder) and a hide toggle. Generalized from
|
||||
// the feed sidebar's WidgetCard so all three panels share the same island look and behavior.
|
||||
export default function PanelGroup({
|
||||
id,
|
||||
title,
|
||||
editing,
|
||||
collapsed,
|
||||
hidden,
|
||||
onToggleCollapse,
|
||||
onToggleHidden,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
editing: boolean;
|
||||
collapsed: boolean;
|
||||
hidden: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
onToggleHidden: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id,
|
||||
disabled: !editing,
|
||||
});
|
||||
const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition };
|
||||
const showBody = !editing && !collapsed;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`glass-card rounded-xl ${
|
||||
isDragging ? "relative z-10 shadow-2xl ring-1 ring-accent" : ""
|
||||
} ${hidden && editing ? "opacity-50" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 px-3 py-2">
|
||||
{editing && (
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
title={t("sidebar.dragToReorder")}
|
||||
className="-ml-1 cursor-grab active:cursor-grabbing text-muted hover:text-fg touch-none"
|
||||
>
|
||||
<GripVertical className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={editing ? undefined : onToggleCollapse}
|
||||
disabled={editing}
|
||||
className="flex-1 min-w-0 flex items-center text-left select-none text-xs uppercase tracking-wide text-muted enabled:hover:text-fg transition"
|
||||
>
|
||||
<span className="truncate">{title}</span>
|
||||
</button>
|
||||
{editing ? (
|
||||
<button
|
||||
onClick={onToggleHidden}
|
||||
title={hidden ? t("sidebar.showWidget") : t("sidebar.hideWidget")}
|
||||
className="text-muted hover:text-fg"
|
||||
>
|
||||
{hidden ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
title={collapsed ? t("sidebar.expand") : t("sidebar.collapse")}
|
||||
className="text-muted hover:text-fg"
|
||||
>
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${collapsed ? "-rotate-90" : ""}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showBody && <div className="px-3 pb-3">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user