feat(feed): add the small-card, compact-row and tile views

Tiles answer the row view's wasted space: the actions move under the meta
into vertical slack the thumbnail already reserves, which frees ~168px of
width, so the block survives in a narrow column and the list can go
multi-column. Rows stay single-column on purpose — reading down one edge
is why that view exists.

The column count now comes from a per-view minimum width, so every
multi-column view reflows on the mechanism the card grid already used.
This commit is contained in:
2026-07-16 03:17:30 +02:00
parent 1a94411a03
commit 78f21eb46b
6 changed files with 127 additions and 54 deletions
+36 -13
View File
@@ -14,7 +14,7 @@ import AddToPlaylist from "./AddToPlaylist";
import DownloadButton from "./DownloadButton";
import clsx from "clsx";
import type { Video } from "../lib/api";
import type { FeedView } from "../lib/feedView";
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
import { formatDate, formatDuration, formatViews, relativeTime } from "../lib/format";
function Actions({
@@ -304,7 +304,12 @@ function VideoCard({
</>
);
if (view === "rows") {
const spec = FEED_VIEW_SPEC[view];
const actions = (
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} />
);
if (spec.family === "row") {
return (
<div
className={clsx(
@@ -312,9 +317,22 @@ function VideoCard({
watched && "opacity-55"
)}
>
<Thumb video={video} className="w-44 aspect-video shrink-0" onOpen={onOpen} />
<div className="min-w-0 flex-1">{textBlock}</div>
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} />
{/* `compact` = the thumbnail-less row: everything else (meta, actions) stays put. */}
{!spec.compact && (
<Thumb
video={video}
className={clsx("aspect-video shrink-0", spec.actionsBelow ? "w-40" : "w-44")}
onOpen={onOpen}
/>
)}
<div className="min-w-0 flex-1">
{textBlock}
{/* Tiles put the actions under the meta, in vertical space the thumbnail already
reserves — which frees the ~168px the docked-right block used, so the block still
works in a narrow column. */}
{spec.actionsBelow && <div className="mt-1">{actions}</div>}
</div>
{!spec.actionsBelow && actions}
</div>
);
}
@@ -322,20 +340,25 @@ function VideoCard({
return (
<div
className={clsx(
"cv-card group glass-card glass-hover rounded-2xl p-2.5 transition-all duration-150 hover:-translate-y-1",
"cv-card group glass-card glass-hover rounded-2xl transition-all duration-150 hover:-translate-y-1",
spec.compact ? "p-2" : "p-2.5",
watched && "opacity-55"
)}
>
<Thumb video={video} className="aspect-video" onOpen={onOpen} />
<div className="flex gap-3 mt-2.5 px-0.5 pb-0.5">
<Avatar
src={video.channel_thumbnail}
fallback={video.channel_title ?? ""}
className="w-9 h-9 rounded-full shrink-0 mt-0.5"
/>
<div className={clsx("flex gap-3 px-0.5 pb-0.5", spec.compact ? "mt-2" : "mt-2.5")}>
{/* The small card drops the channel avatar: at its ~180px column the 36px avatar plus the
gap would leave the title barely half the width. The channel name is still below it. */}
{!spec.compact && (
<Avatar
src={video.channel_thumbnail}
fallback={video.channel_title ?? ""}
className="w-9 h-9 rounded-full shrink-0 mt-0.5"
/>
)}
<div className="min-w-0 flex-1">
{textBlock}
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} />
{actions}
</div>
</div>
</div>