improvement(scroll): fade every capped list's clipped edges

The sweep's own three (the thread, the Plex collections filter, the tag manager)
plus the seven the spec missed — the playlist picker, the row tag menu, the Plex
playlist/collection dialogs, the player's track menu, the video description and
the table's multi-select filter. Same idiom throughout: a height-capped list that
silently clipped its content now says so, and hides its scrollbar like the rail
and the panels already do.

The table's option list is its own module-level component because it owns a hook:
FilterPopover is redeclared per DataTable render, so React remounts it.
This commit is contained in:
2026-07-17 03:14:48 +02:00
parent a032324513
commit 0ed58e3cb1
10 changed files with 91 additions and 16 deletions
+19 -2
View File
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, ListFilter, X } from "lucide-react";
import { useDismiss } from "../lib/useDismiss";
import { useScrollFade } from "../lib/useScrollFade";
// A reusable, client-side data table: per-column sort + filter (in the header) + pagination,
// with its sort/filter/page state optionally persisted to localStorage so a reload (F5) keeps
@@ -9,6 +10,22 @@ import { useDismiss } from "../lib/useDismiss";
// other modules (e.g. the playlist manager) can reuse it. Below `md` it falls back to a card
// list built from the same column definitions.
// The multi-select filter's option list, capped and scrollable. Its own component (module level,
// not nested in DataTable) because it owns a hook: FilterPopover is redeclared on every DataTable
// render, so React remounts it — state parked in there would churn.
function FilterOptionList({ children }: { children: ReactNode }) {
const fade = useScrollFade();
return (
<div
ref={fade.ref}
style={fade.style}
className="flex flex-col gap-0.5 max-h-60 overflow-y-auto no-scrollbar"
>
{children}
</div>
);
}
type ColumnFilter<T> =
| { kind: "text"; get: (row: T) => string }
| { kind: "select"; options: { value: string; label: string }[]; test: (row: T, value: string) => boolean }
@@ -218,7 +235,7 @@ export default function DataTable<T>({
</div>
)}
{f.kind === "multi" && (
<div className="flex flex-col gap-0.5 max-h-60 overflow-y-auto">
<FilterOptionList>
{f.options.length === 0 && (
<span className="text-xs text-muted px-2 py-1">{t("datatable.noOptions")}</span>
)}
@@ -249,7 +266,7 @@ export default function DataTable<T>({
</button>
);
})}
</div>
</FilterOptionList>
)}
{isActive(val) && (
<button