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 // The one-line row's column widths at the DEFAULT text size, mirroring the Tailwind classes the
// widths — Tailwind can't accept a runtime value), and so do the drop-out thresholds. Two // boxes actually use. They have to be mirrored by hand — Tailwind can't take a runtime value, and
// hand-written sets would drift, and the symptom would be silent: a column appearing just before // the classes have to stay, because they're rem: Settings' text-size slider drives the root font
// it actually fits. Each is measured against the worst case in the LIBRARY and in BOTH languages — // (`html { font-size: calc(16px * var(--font-scale)) }`, 0.91.3), so the boxes must grow with
// see the row's own comment for where each number comes from. // 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 = { const ROW_COL = {
actions: 192, // six buttons at 28 + five 4px gaps = 188 actions: 192, // w-48 — six buttons at 28 + five 4px gaps = 188
channel: 128, channel: 128, // w-32
meta: 104, // RowMetaCells' own grid: 4rem + 1rem + 1rem + two 4px gaps meta: 104, // RowMetaCells' own grid: 4rem + 1rem + 1rem + two 4px gaps
views: 64, views: 64, // w-16
published: 192, published: 192, // w-48
} as const; } as const;
const ROW_GAP = 12; // gap-3 const ROW_GAP = 12; // gap-3
const ROW_PAD = 26; // px-3 both sides, plus a rounding allowance 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 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 // 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_BASE = ROW_PAD + ROW_COL.actions + ROW_GAP + ROW_TITLE_MIN;
const ROW_FITS = { const ROW_FITS = {
channel: ROW_BASE + ROW_GAP + ROW_COL.channel, 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")}> <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 {/* 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. */} they're the shortest pointer trip from it. */}
<div className="shrink-0" style={{ width: ROW_COL.actions }}> <div className="shrink-0 w-48">{actions}</div>
{actions}
</div>
{/* Everything else is shrink-0, so the title is the only thing that can give — which means {/* 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 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 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 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. */} row is off-screen for that frame anyway, and guessing narrow would flash. */}
<div className="min-w-0 flex-1">{title}</div> <div className="min-w-0 flex-1">{title}</div>
{fits(ROW_FITS.channel) && ( {fits(ROW_FITS.channel) && <div className="shrink-0 w-32">{channelButton}</div>}
<div className="shrink-0" style={{ width: ROW_COL.channel }}>
{channelButton}
</div>
)}
{fits(ROW_FITS.meta) && <RowMetaCells video={video} />} {fits(ROW_FITS.meta) && <RowMetaCells video={video} />}
{/* Right-aligned like the duration: it's a number, so the digits share an edge. 64px {/* 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 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. */} took this from 144 and handed the difference to the title. */}
{fits(ROW_FITS.views) && ( {fits(ROW_FITS.views) && (
<div <div className="shrink-0 w-16 text-sm text-muted text-right tabular-nums truncate">
className="shrink-0 text-sm text-muted text-right tabular-nums truncate"
style={{ width: ROW_COL.views }}
>
{viewsCell(false)} {viewsCell(false)}
</div> </div>
)} )}
{fits(ROW_FITS.published) && ( {fits(ROW_FITS.published) && (
<div <div className="shrink-0 w-48 text-sm text-muted truncate">{published}</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. */} {/* Thumb draws this over the image; with no image it goes on the row itself. */}
{pct > 0 && ( {pct > 0 && (