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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user