fix(ui): focus, paging and state that a key or a finger can reach

- A key landing on a side panel's tab steps into the card it peeks open, not onto the hidden tab.
- The Channels cards keep their status / pager row at the list's end while the band is folded.
- A menu row's note shows on an enabled row too: the download state under 'Download…', not a title.
- A removal chip hands the focus to its neighbour before it goes, for every caller (the feed's and
  Plex's active filters); the hidden-filter cue's own copy is gone.
- Back to a spot within a page's end spacer lands at the page's end, not its top.
- A running push / revert is tracked per list: another list's push is no longer refused unseen.
This commit is contained in:
2026-09-27 19:43:04 +02:00
parent e31a588f19
commit 740602b9bf
15 changed files with 231 additions and 48 deletions
+19 -2
View File
@@ -28,6 +28,12 @@ function rememberScroll(key: string, top: number): void {
// Put the scroller back to `top`. The feed is virtualized, so on a remount the list needs a frame
// or two to render enough rows to reach its full height; an immediate `scrollTop` would otherwise
// clamp short. Re-apply over a few frames until it sticks (or the content is simply shorter now).
// A spot the page can no longer reach — the last stretch of a page scrolled to its very end, whose
// Back-to-top spacer only shows once scrolled — goes as far as the page does: it used to be left at
// the top (round 2, F8). Only a spacer's worth short, and only once the height has held still, is
// that answer early; a list still loading is further short than that, and gets the full wait.
const SETTLED_FRAMES = 10;
const SPACER_REM = 6;
function restoreScroll(el: HTMLElement, top: number): () => void {
if (top <= 0) {
el.scrollTop = 0;
@@ -35,11 +41,22 @@ function restoreScroll(el: HTMLElement, top: number): () => void {
}
let raf = 0;
let frames = 0;
let still = 0;
let lastMax = -1;
let cancelled = false;
const tick = () => {
if (cancelled) return;
if (el.scrollHeight - el.clientHeight >= top) el.scrollTop = top;
if (el.scrollTop >= top - 1 || frames++ > 90) return; // reached, or gave up after ~1.5s
const max = el.scrollHeight - el.clientHeight;
if (max >= top) el.scrollTop = top;
if (el.scrollTop >= top - 1) return; // reached
still = max === lastMax ? still + 1 : 0;
lastMax = max;
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
const nearEnd = max > 0 && top - max <= SPACER_REM * rem;
if ((nearEnd && still >= SETTLED_FRAMES) || frames++ > 90) {
el.scrollTop = max; // as near as the page reaches now
return;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);