fix(feed): keep the row's columns growing with the text-size setting

My last commit made ROW_COL the literal source of the widths — and froze
them, because inline pixels can't follow the root font that Settings'
text slider drives (0.9-1.3). Measured at 130%: the channel box stuck at
128px while its text wanted 152 and the meta cell, being a rem grid, grew
to 156 — one row in two unit systems.

The classes come back and ROW_COL documents them, saying plainly that the
two are matched by hand and why they have to be. Chasing a comment's
literal truth cost a property I'd already verified.
This commit is contained in:
2026-07-17 02:21:04 +02:00
parent 99c1bd9916
commit 0b56eb9d53
+19 -29
View File
@@ -322,24 +322,28 @@ function Thumb({
);
}
// 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.
// The one-line row's column widths at the DEFAULT text size, mirroring the Tailwind classes the
// boxes actually use. They have to be mirrored by hand — Tailwind can't take a runtime value, and
// the classes have to stay, because they're rem: Settings' text-size slider drives the root font
// (`html { font-size: calc(16px * var(--font-scale)) }`, 0.91.3), so the boxes must grow with
// their contents. Inline pixels froze them and a channel name at 130% wanted 152px in a 128px box.
// Keep this table and the classes in step; each number is measured against the worst case in the
// LIBRARY and in BOTH languages — see the row's own comment for where each comes from.
const ROW_COL = {
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,
actions: 192, // w-48 — six buttons at 28 + five 4px gaps = 188
channel: 128, // w-32
meta: 104, // RowMetaCells' own grid: 4rem + 1rem + 1rem + two 4px gaps
views: 64, // w-16
published: 192, // w-48
} 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, so each appears only
// once everything to its left AND a readable title already fit.
// once everything to its left AND a readable title already fit. Calibrated at the default text
// size; at 130% the columns grow ~30% while these don't, so they read slightly eager — the title
// gets tighter there, but it can't vanish, since the last column standing down frees ~200px.
const ROW_BASE = ROW_PAD + ROW_COL.actions + ROW_GAP + ROW_TITLE_MIN;
const ROW_FITS = {
channel: ROW_BASE + ROW_GAP + ROW_COL.channel,
@@ -496,9 +500,7 @@ 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" style={{ width: ROW_COL.actions }}>
{actions}
</div>
<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 the row narrows, least important first: a title with no date beats a
@@ -506,30 +508,18 @@ 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" style={{ width: ROW_COL.channel }}>
{channelButton}
</div>
)}
{fits(ROW_FITS.channel) && <div className="shrink-0 w-32">{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 text-sm text-muted text-right tabular-nums truncate"
style={{ width: ROW_COL.views }}
>
<div className="shrink-0 w-16 text-sm text-muted text-right tabular-nums truncate">
{viewsCell(false)}
</div>
)}
{fits(ROW_FITS.published) && (
<div
className="shrink-0 text-sm text-muted truncate"
style={{ width: ROW_COL.published }}
>
{published}
</div>
<div className="shrink-0 w-48 text-sm text-muted truncate">{published}</div>
)}
{/* Thumb draws this over the image; with no image it goes on the row itself. */}
{pct > 0 && (