Feat/continue watching card affordance (#120)

* feat(web): improve Continue Watching card affordances

Add hover/active feedback to the play button (scale, brightness,
shadow) and make the episode meta and time-left lines link to the
item page like the heading. The blank time-left placeholder stays a
plain div so card heights align without an invisible link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(web): cover audiobook-only continue sections using poster cards

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Quick
2026-06-09 20:02:18 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent 46540dfec3
commit 79c749b9a7
2 changed files with 64 additions and 3 deletions
+20 -3
View File
@@ -252,7 +252,7 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) {
to={card.watchHref}
onClick={handleWatchClick}
aria-label={`Play ${heading}`}
className="bg-primary text-primary-foreground absolute top-1/2 left-1/2 flex h-11 w-11 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full opacity-100 shadow-lg transition-all duration-200 pointer-fine:pointer-events-none pointer-fine:opacity-0 pointer-fine:group-hover/media:pointer-events-auto pointer-fine:group-hover/media:opacity-100 pointer-fine:focus-visible:pointer-events-auto pointer-fine:focus-visible:opacity-100"
className="bg-primary text-primary-foreground absolute top-1/2 left-1/2 flex h-11 w-11 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full opacity-100 shadow-lg transition-all duration-200 hover:scale-110 hover:shadow-xl hover:brightness-110 active:scale-95 pointer-fine:pointer-events-none pointer-fine:opacity-0 pointer-fine:group-hover/media:pointer-events-auto pointer-fine:group-hover/media:opacity-100 pointer-fine:focus-visible:pointer-events-auto pointer-fine:focus-visible:opacity-100"
>
<Play className="ml-0.5 h-5 w-5" fill="currentColor" />
</ViewTransitionLink>
@@ -283,7 +283,14 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) {
>
{heading}
</ViewTransitionLink>
{episodeMeta && <div className="text-muted-foreground truncate text-xs">{episodeMeta}</div>}
{episodeMeta && (
<ViewTransitionLink
to={card.itemHref}
className="text-muted-foreground block truncate text-xs hover:underline"
>
{episodeMeta}
</ViewTransitionLink>
)}
{premiereBadge && (
<div className="mt-1">
<span
@@ -295,7 +302,17 @@ export default function ContinueWatchingCard(props: ContinueWatchingCardProps) {
</span>
</div>
)}
{timeLeftLabel && <div className="text-muted-foreground text-xs">{timeLeftLabel}</div>}
{timeLeftLabel &&
(timeLeftLabel === "\u00A0" ? (
<div className="text-muted-foreground text-xs">{timeLeftLabel}</div>
) : (
<ViewTransitionLink
to={card.itemHref}
className="text-muted-foreground block w-fit text-xs hover:underline"
>
{timeLeftLabel}
</ViewTransitionLink>
))}
</div>
</div>
);
+44
View File
@@ -121,6 +121,50 @@ describe("SectionRow", () => {
expect(markup).toContain("More actions");
});
it("renders all-audiobook continue sections as square poster cards", () => {
const queryClient = new QueryClient();
const section: ResolvedSection = {
id: "continue-listening",
section_type: "continue_watching",
title: "Continue Listening",
featured: false,
item_limit: 10,
total_count: 1,
is_custom: false,
customized: false,
items: [
{
content_id: "book-001",
type: "audiobook",
title: "Project Hail Mary",
position_seconds: 600,
duration_seconds: 58000,
year: 2021,
genres: [],
status: "matched",
rating_imdb: null,
overview: "",
poster_url: "/book-cover.jpg",
poster_thumbhash: "",
backdrop_url: "",
backdrop_thumbhash: "",
logo_url: "",
},
],
};
const markup = renderToStaticMarkup(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<SectionRow section={section} />
</MemoryRouter>
</QueryClientProvider>,
);
expect(markup).toContain("aspect-square");
expect(markup).not.toContain("aspect-video");
});
it("routes supported section view-all actions to browse destinations", () => {
const queryClient = new QueryClient();
const section: ResolvedSection = {