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
+16 -3
View File
@@ -36,6 +36,7 @@ import { api, type PlexMarker } from "../lib/api";
import { formatDuration } from "../lib/format";
import { LS, useAccountPersistedObject } from "../lib/storage";
import { useDismiss } from "../lib/useDismiss";
import { useScrollFade } from "../lib/useScrollFade";
import { useBackToClose } from "../lib/history";
// The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page.
@@ -245,13 +246,24 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
const menuOpenRef = useRef(false);
const menuRef = useRef<HTMLDivElement>(null);
const menuTriggerRef = useRef<HTMLButtonElement>(null);
const tracksRef = useRef<HTMLDivElement>(null);
const tracksRef = useRef<HTMLDivElement | null>(null);
const tracksTriggerRef = useRef<HTMLButtonElement>(null);
audioRef.current = audioOrd;
menuOpenRef.current = menuOpen || tracksOpen; // keep the control bar up while either menu is open
// Each menu stays open until a click/Escape OUTSIDE it (so sliders can be dragged freely).
useDismiss(menuOpen, () => setMenuOpen(false), [menuRef, menuTriggerRef]);
useDismiss(tracksOpen, () => setTracksOpen(false), [tracksRef, tracksTriggerRef]);
// The tracks menu is both dismiss-tracked and edge-faded, so its two refs are merged here.
// Must be stable: an inline arrow would detach (null) and re-attach the node on every render,
// and the fade's node-state ref would re-render in response — a loop. `fade.ref` is a setState.
const tracksFade = useScrollFade();
const setTracksNode = useCallback(
(node: HTMLElement | null) => {
tracksRef.current = node as HTMLDivElement | null;
tracksFade.ref(node);
},
[tracksFade.ref]
);
// Keyboard-shortcut cheat sheet (toggled with "h") + a live wall clock for the top bar.
const [helpOpen, setHelpOpen] = useState(false);
const helpOpenRef = useRef(false);
@@ -1222,10 +1234,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
</button>
{tracksOpen && (
<div
ref={tracksRef}
ref={setTracksNode}
style={tracksFade.style}
onClick={(e) => e.stopPropagation()}
onWheel={(e) => e.stopPropagation()}
className="glass-menu absolute bottom-full right-0 mb-2 w-56 max-h-[60vh] overflow-auto rounded-xl p-2 text-sm text-white shadow-2xl"
className="glass-menu absolute bottom-full right-0 mb-2 w-56 max-h-[60vh] overflow-auto no-scrollbar rounded-xl p-2 text-sm text-white shadow-2xl"
>
{detail.audio_streams.length > 1 && (
<>