fix(ui): review round 2 — reveal floor, failed next page, count states, row Play

- DataTable: the reveal floor holds only in the view it was made for (Channels
  never clears its follow, so every later sort mounted rows up to it)
- Feed + Plex browse: a failed next page stops the read-ahead and shows a row
  that retries, instead of re-requesting it once a round-trip
- the YouTube Load more click uses nextPageOnClick; a contract pins NEXT_PAGE
  to the read-aheads and the click sites to nextPageOnClick
- ItemCount: a failed count says so; a stale one says updating to a screen reader
- the narrow-picture Play lift applies only where a hover row is there to clear
- PlayerModal: one description ladder for the popover and the panel, both tested
- plexImage contract reads every img src, any attribute order
- Tabs comment names who persists the sub-tab
This commit is contained in:
2026-10-03 02:25:15 +02:00
parent c4c442bc5b
commit b40440b825
17 changed files with 360 additions and 63 deletions
+12 -3
View File
@@ -267,19 +267,28 @@ export default function DataTable<T>({
// Adjusted DURING render against the last seq seen (the house's key-change reset), so the frame
// that re-sorted the row already shows its new page. Seeded with the mount's seq: arriving with a
// stale reveal must not move a page nobody asked to move.
const viewOf = (p: number) =>
`${p}:${size}:${JSON.stringify(sort)}:${JSON.stringify(filters)}:${narrowedBy}`;
const [revealSeen, setRevealSeen] = useState(reveal?.seq);
// The view the reveal was made for: the floor below holds only there. A caller keeps its reveal
// after the focus (Channels never clears `follow`), and a floor that outlived its view mounted every
// row up to that channel on each later sort or search (E31 review 2).
const [revealView, setRevealView] = useState<string | null>(null);
if (reveal && reveal.seq !== revealSeen) {
setRevealSeen(reveal.seq);
const at = sorted.findIndex((row) => rowKey(row) === reveal.key);
if (at >= 0 && !allRows) setPage(Math.floor(at / size));
const target = at >= 0 && !allRows ? Math.floor(at / size) : safePage;
if (target !== safePage) setPage(target);
setRevealView(viewOf(target));
}
const view = viewOf(safePage);
const paged = allRows ? sorted : sorted.slice(safePage * size, safePage * size + size);
// A long page ("All", 100) mounts its first rows at once and the rest in the background — but
// always through the revealed row, which the caller focuses as soon as this frame commits.
const shown = useProgressiveRows(
paged,
`${safePage}:${size}:${JSON.stringify(sort)}:${JSON.stringify(filters)}:${narrowedBy}`,
reveal ? paged.findIndex((row) => rowKey(row) === reveal.key) : -1,
view,
reveal && revealView === view ? paged.findIndex((row) => rowKey(row) === reveal.key) : -1,
);
function toggleSort(key: string) {