fix(feed): make the row's widths actually come from one table

The comment claimed the boxes were generated from ROW_COL. They weren't —
still w-32/w-16/w-48 by hand — so a comment promised a link that didn't
exist, which is worse than not claiming it. The boxes take the numbers
directly now (inline, since Tailwind can't read a runtime value), and the
thresholds add up in plain arithmetic rather than an IIFE whose result
depended on key order.
This commit is contained in:
2026-07-17 02:16:16 +02:00
parent ba23c0c996
commit 99c1bd9916
+47 -26
View File
@@ -322,33 +322,40 @@ function Thumb({
);
}
// The one-line row's column widths, in ONE place the Tailwind classes below are generated from
// them, so the thresholds can't drift out of step with the boxes they describe (they did when both
// were written by hand). Each is measured against the worst case in the library AND in both
// languages; see the row's own comment for where each number comes from.
// The one-line row's column widths, in ONE place: the boxes below take these directly (as inline
// widths — Tailwind can't accept a runtime value), and so do the drop-out thresholds. Two
// hand-written sets would drift, and the symptom would be silent: a column appearing just before
// it actually fits. Each is measured against the worst case in the LIBRARY and in BOTH languages —
// see the row's own comment for where each number comes from.
const ROW_COL = {
actions: 192, // w-48 — six buttons at 28 + five 4px gaps = 188
title: 120, // — not a column: the floor below which a title isn't worth showing
channel: 128, // w-32
meta: 104, // — RowMetaCells' own grid: 4rem + 1rem + 1rem + two 4px gaps
views: 64, // w-16
published: 192, // w-48
actions: 192, // six buttons at 28 + five 4px gaps = 188
channel: 128,
meta: 104, // RowMetaCells' own grid: 4rem + 1rem + 1rem + two 4px gaps
views: 64,
published: 192,
} as const;
const ROW_GAP = 12; // gap-3
const ROW_PAD = 26; // px-3 both sides, plus a rounding allowance
const ROW_TITLE_MIN = 120; // not a column: the floor below which a title isn't worth showing
// What the row must measure before each column earns its place — cumulative from the left, so each
// one only appears once everything ahead of it AND a readable title already fit.
const ROW_FITS = (() => {
let w = ROW_PAD + ROW_COL.actions + ROW_GAP + ROW_COL.title;
const at = (px: number) => (w += ROW_GAP + px);
return {
channel: at(ROW_COL.channel),
meta: at(ROW_COL.meta),
views: at(ROW_COL.views),
published: at(ROW_COL.published),
};
})();
// What the row must measure before each column earns its place — cumulative, so each appears only
// once everything to its left AND a readable title already fit.
const ROW_BASE = ROW_PAD + ROW_COL.actions + ROW_GAP + ROW_TITLE_MIN;
const ROW_FITS = {
channel: ROW_BASE + ROW_GAP + ROW_COL.channel,
meta: ROW_BASE + ROW_GAP + ROW_COL.channel + ROW_GAP + ROW_COL.meta,
views: ROW_BASE + ROW_GAP + ROW_COL.channel + ROW_GAP + ROW_COL.meta + ROW_GAP + ROW_COL.views,
published:
ROW_BASE +
ROW_GAP +
ROW_COL.channel +
ROW_GAP +
ROW_COL.meta +
ROW_GAP +
ROW_COL.views +
ROW_GAP +
ROW_COL.published,
};
function VideoCard({
video,
@@ -489,7 +496,9 @@ function VideoCard({
<div className={clsx(rowShell, "flex items-center gap-3 px-3 py-2", watched && "opacity-55")}>
{/* 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>
<div className="shrink-0" style={{ width: ROW_COL.actions }}>
{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 the row narrows, least important first: a title with no date beats a
@@ -497,18 +506,30 @@ function VideoCard({
filter panel are doing. Until the first measure lands (width 0) show everything: the
row is off-screen for that frame anyway, and guessing narrow would flash. */}
<div className="min-w-0 flex-1">{title}</div>
{fits(ROW_FITS.channel) && <div className="shrink-0 w-32">{channelButton}</div>}
{fits(ROW_FITS.channel) && (
<div className="shrink-0" style={{ width: ROW_COL.channel }}>
{channelButton}
</div>
)}
{fits(ROW_FITS.meta) && <RowMetaCells video={video} />}
{/* Right-aligned like the duration: it's a number, so the digits share an edge. 64px
holds the widest bare form ("101.7K" at 48) in either language — dropping the word
took this from 144 and handed the difference to the title. */}
{fits(ROW_FITS.views) && (
<div className="shrink-0 w-16 text-sm text-muted text-right tabular-nums truncate">
<div
className="shrink-0 text-sm text-muted text-right tabular-nums truncate"
style={{ width: ROW_COL.views }}
>
{viewsCell(false)}
</div>
)}
{fits(ROW_FITS.published) && (
<div className="shrink-0 w-48 text-sm text-muted truncate">{published}</div>
<div
className="shrink-0 text-sm text-muted truncate"
style={{ width: ROW_COL.published }}
>
{published}
</div>
)}
{/* Thumb draws this over the image; with no image it goes on the row itself. */}
{pct > 0 && (