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
+31 -32
View File
@@ -6,6 +6,7 @@ import {
useLayoutEffect,
useRef,
useState,
type ReactNode,
} from "react";
import { useTranslation } from "react-i18next";
import { createPortal } from "react-dom";
@@ -606,6 +607,30 @@ export default function PlayerModal({
qc,
});
// The description, said the same way in its two places (the popover under 1280 px, the panel
// beside the video above it): loading, failed with a retry, the text, or none. One ladder, so the
// two cannot drift (E31 review 2: the failed state was tested in the panel only). `wrap` is each
// place's own box for the text.
const description = (wrap: (body: ReactNode) => ReactNode) =>
detail.isLoading ? (
<div className="text-sm text-muted">{t("player.loading")}</div>
) : detail.isError && !detail.data ? (
<StateMessage tone="error" compact onRetry={() => void detail.refetch()}>
{t("player.descriptionFailed")}
</StateMessage>
) : detail.data?.description ? (
wrap(
renderDescription(detail.data.description, {
currentId: currentVideoId,
onSeek: seekTo,
onLoadVideo: loadVideo,
t,
}),
)
) : (
<div className="text-sm text-muted">{t("player.noDescription")}</div>
);
// Portaled to <body>, like every other full-screen overlay here. A fixed, full-viewport modal
// must not render inside the page scroller: once that scroller is a stacking context (its
// edge-fade mask makes it one) this z-overlay is scoped inside it, and the nav rail (z-rail) and the
@@ -887,28 +912,15 @@ export default function PlayerModal({
<SectionLabel level={3} className="mb-2">
{t("player.description")}
</SectionLabel>
{detail.isLoading ? (
<div className="text-sm text-muted">{t("player.loading")}</div>
) : detail.isError && !detail.data ? (
<StateMessage tone="error" compact onRetry={() => void detail.refetch()}>
{t("player.descriptionFailed")}
</StateMessage>
) : detail.data?.description ? (
{description((body) => (
<div
ref={descFade.ref}
style={descFade.style}
className="text-sm whitespace-pre-wrap wrap-break-word max-h-64 overflow-y-auto no-scrollbar leading-relaxed"
>
{renderDescription(detail.data.description, {
currentId: currentVideoId,
onSeek: seekTo,
onLoadVideo: loadVideo,
t,
})}
{body}
</div>
) : (
<div className="text-sm text-muted">{t("player.noDescription")}</div>
)}
))}
</PopoverSurface>
<Button
variant="ghost"
@@ -1089,24 +1101,11 @@ export default function PlayerModal({
<SectionLabel level={3} className="mb-2">
{t("player.description")}
</SectionLabel>
{detail.isLoading ? (
<div className="text-sm text-muted">{t("player.loading")}</div>
) : detail.isError && !detail.data ? (
<StateMessage tone="error" compact onRetry={() => void detail.refetch()}>
{t("player.descriptionFailed")}
</StateMessage>
) : detail.data?.description ? (
{description((body) => (
<div className="text-sm whitespace-pre-wrap wrap-break-word leading-relaxed">
{renderDescription(detail.data.description, {
currentId: currentVideoId,
onSeek: seekTo,
onLoadVideo: loadVideo,
t,
})}
{body}
</div>
) : (
<div className="text-sm text-muted">{t("player.noDescription")}</div>
)}
))}
</section>
)}
</div>