fix(feed): stop the compact row eating its own title on a smaller window

Every column but the title is shrink-0, so once the fixed widths outgrow
the row the title gives all of it: measured clientWidth 0 on every row at
a 1000px window — a table of channel, duration, views and date with no
titles. Columns now drop from the right as space runs out, least
important first, in the order the user named: title, then channel, then
the rest. Measured at 1000px: title 0 -> 351.

I'd written this off as "E15's problem, bites below ~640px" — wrong by
600px, because I only ever measured at 1440.
This commit is contained in:
2026-07-17 00:55:46 +02:00
parent 638aa5750e
commit ed08aa87f0
+20 -7
View File
@@ -149,7 +149,7 @@ function resumePct(video: Video): number {
// reading as a column. The flex row this replaces let a "36:36" and a "3:31:09" push the marker
// beside them to different places, and pulled it left again whenever the duration was absent
// (live/upcoming) — visible as a ragged edge down the page.
function RowMetaCells({ video }: { video: Video }) {
function RowMetaCells({ video, className }: { video: Video; className?: string }) {
const { t } = useTranslation();
// Icons, not the labelled chips the thumbnail uses: at a glance this is the least important thing
// in the row, so it gets about a letter's worth of width. Icons rather than a coloured dot
@@ -168,7 +168,12 @@ function RowMetaCells({ video }: { video: Video }) {
// widen a slot and the title column silently pays for it. rem, not px, so the whole cell tracks
// the text-size setting the same way its contents do.
return (
<div className="shrink-0 grid grid-cols-[4rem_1rem_1rem] gap-1 items-center text-sm text-muted">
<div
className={clsx(
"shrink-0 grid grid-cols-[4rem_1rem_1rem] gap-1 items-center text-sm text-muted",
className
)}
>
{/* Right-aligned: a duration is a number, so the digits line up on their own edge (30px for
"1:52" up to 59px for an 11-hour stream — 4rem holds the longest). */}
<span className="text-right tabular-nums">
@@ -379,7 +384,9 @@ function VideoCard({
const ro = new ResizeObserver(check);
els.forEach(([el]) => el && ro.observe(el));
return () => ro.disconnect();
}, [video.title, video.channel_title]);
// `view` matters as much as the text: switching views returns a different branch, so React can
// remount these nodes — without it the observer would be left watching detached ones.
}, [video.title, video.channel_title, view]);
const title = (
<a
ref={titleRef}
@@ -451,11 +458,17 @@ function VideoCard({
{/* Actions lead: this is where the eye already is (the title is the thing you read), so
they're the shortest pointer trip from it. */}
<div className="shrink-0 w-48">{actions}</div>
{/* Everything else is shrink-0, so the title is the only thing that can give — which means
once the fixed columns outgrow the row, it gives ALL of it and vanishes. Columns drop
from the right as space runs out, least important first: a title with no date beats a
date with no title. Breakpoints are the viewport, not this row, so they're rough — the
rail and filter panel move the row's width too — but they're pitched to keep the title
alive at the narrow end rather than to hold every column to the last pixel. */}
<div className="min-w-0 flex-1">{title}</div>
<div className="shrink-0 w-32">{channelButton}</div>
<RowMetaCells video={video} />
<div className="shrink-0 w-36 text-sm text-muted truncate">{views}</div>
<div className="shrink-0 w-48 text-sm text-muted truncate">{published}</div>
<div className="shrink-0 w-32 hidden sm:block">{channelButton}</div>
<RowMetaCells video={video} className="hidden md:grid" />
<div className="shrink-0 w-36 text-sm text-muted truncate hidden lg:block">{views}</div>
<div className="shrink-0 w-48 text-sm text-muted truncate hidden xl:block">{published}</div>
{/* Thumb draws this over the image; with no image it goes on the row itself. */}
{pct > 0 && (
<div className="absolute bottom-0 left-2 right-2 h-1 rounded-full bg-black/40">