fix(feed): size the badge column against the library's worst case

A 60-video sample topped out at 2:00:58, so the column was 8px short of
an 11-hour stream that is also saved — and the library holds one. That row
would wrap to two lines in the one layout that promises single ones.

The stacked layouts' textBlock now builds after the one-line row returns,
since only they use it.
This commit is contained in:
2026-07-16 23:54:21 +02:00
parent f5649fc0b9
commit 5433a60ac9
+26 -24
View File
@@ -329,13 +329,6 @@ function VideoCard({
{video.published_at && <>{" · "}{formatDate(video.published_at, i18n.language)}</>}
</>
);
const meta = (
<>
{views}
{views && " · "}
{published}
</>
);
// Show the full title in a native tooltip only when it's actually cut off — vertically where it
// clamps to 2 lines, horizontally where the one-line row truncates it.
const titleRef = useRef<HTMLAnchorElement>(null);
@@ -377,16 +370,6 @@ function VideoCard({
{video.channel_title}
</button>
);
// Title + channel + meta stacked — the card and the thumbnailed row share this; only their
// wrapper / Actions placement differs. Safe to reuse: exactly one branch below renders.
const textBlock = (
<>
{title}
<div className="mt-0.5">{channelButton}</div>
<div className="text-xs text-muted mt-0.5">{meta}</div>
</>
);
const actions = (
<Actions
video={video}
@@ -407,12 +390,13 @@ function VideoCard({
// A table without being one. Every column is a FIXED width, which is what makes the same datum
// land on the same vertical line in every row — a CSS grid can't do it for us, because
// VirtualFeed renders each row as its own subtree, so there is no shared grid to align to.
// Widths come from measuring the real data at text-sm (60-video sample), then taking the next
// Tailwind step up: channel names run to 342px at worst (they truncate — most are far shorter),
// views peak at 79, duration + a live chip + the saved pip at ~118, "11 months ago ·
// Feb 28, 2026" at 181. The actions are 188 (6 x 28 + 5 x 4). Re-measure before trimming any
// of them — badges first looked like 102 until the chips' own margins were counted, and the
// column silently wrapped to a second line.
// Widths come from measuring the real data at text-sm, then taking the next Tailwind step up:
// channel names run to 342px at worst (they truncate — most are far shorter), views peak at 79,
// the badges at 130 ("11:46:50" + a STREAM chip + the saved pip — the library really does hold
// 11-hour streams), "11 months ago · Feb 28, 2026" at 181, and the actions 188 (6 x 28 + 5 x 4).
// Size against the WORST case in the library, not a page of it: badges were sized 102 off a
// 60-video sample, which missed both the chips' own margins and any stream over two hours, and
// the column silently wrapped to a second line — in the one layout that promises single lines.
// The title takes what's left, which is constant per container width — so it aligns too.
if (noThumb) {
const pct = resumePct(video);
@@ -423,7 +407,7 @@ function VideoCard({
<div className="shrink-0 w-48">{actions}</div>
<div className="min-w-0 flex-1">{title}</div>
<div className="shrink-0 w-40">{channelButton}</div>
<div className="shrink-0 w-32 text-sm text-muted">
<div className="shrink-0 w-36 text-sm text-muted">
<InlineBadges video={video} />
</div>
<div className="shrink-0 w-24 text-sm text-muted truncate">{views}</div>
@@ -437,6 +421,24 @@ function VideoCard({
</div>
);
}
}
// Everything below is a STACKED layout — the card and the thumbnailed row. Only they run the
// data together as one sentence; the one-line row above gives each datum its own column and has
// already returned, so building this for it would be waste dressed up as shared code.
const textBlock = (
<>
{title}
<div className="mt-0.5">{channelButton}</div>
<div className="text-xs text-muted mt-0.5">
{views}
{views && " · "}
{published}
</div>
</>
);
if (spec.family === "row") {
return (
<div className={clsx(rowShell, "flex gap-3 p-2", watched && "opacity-55")}>
<Thumb