improvement(feed): rework the row views from UAT feedback

The bare row becomes a single line — title truncating into the space
left, then channel, badges, meta and the actions each holding a fixed
column — and it drops the 896px reading cap, since with the actions
right-aligned there is no gap for the extra width to stretch. The
actions get a reserved slot: the reset button only exists for videos
with watch state, so without one the meta sat 32px further right on
some rows than others.

Rows and tiles get a permanent container instead of a hover-only fill.
They were transparent at rest and painted an opaque bg on hover, so
running the pointer down the list kept covering and re-revealing the
ambient backdrop — which reads as the background flickering.

Card actions now sit on the bottom edge rather than wherever the title
ends, and the small thumbnails carry icon-only controls: the labelled
Continue / Restart are sized for a card's image and were clipped at
176px.
This commit is contained in:
2026-07-16 04:14:27 +02:00
parent de1b32ba6f
commit f108c93c02
3 changed files with 107 additions and 52 deletions
+12 -5
View File
@@ -19,6 +19,9 @@ export interface FeedViewSpec {
/** Narrowest grid column in px, or null for ONE full-width column. VirtualFeed derives the
* column count from this and the container width, so every multi-column view reflows for free. */
minCol: number | null;
/** Single-column views only: the reading-width cap in px (null = fill). A long line is hard to
* scan, so the stacked row keeps a cap; the one-line row doesn't need as tight a one. */
maxCol: number | null;
/** Show the video thumbnail. False only for the bare row — whose badges (duration, live state,
* saved, resume bar) then move into the meta line, since Thumb is what draws them. */
thumb: boolean;
@@ -31,12 +34,16 @@ export interface FeedViewSpec {
// The whole matrix in one place. Adding a view here makes TS point at every switch that needs it.
export const FEED_VIEW_SPEC: Record<FeedView, FeedViewSpec> = {
cards: { family: "card", minCol: 260, thumb: true, dense: false, actionsBelow: false },
cardsSmall: { family: "card", minCol: 180, thumb: true, dense: true, actionsBelow: false },
cards: { family: "card", minCol: 260, maxCol: null, thumb: true, dense: false, actionsBelow: false },
cardsSmall: { family: "card", minCol: 180, maxCol: null, thumb: true, dense: true, actionsBelow: false },
// Single column on purpose: the point of the list is that your eye runs down one edge with the
// titles stacked. Density must not cost that — `tiles` is the multi-column option instead.
rows: { family: "row", minCol: null, thumb: true, dense: false, actionsBelow: false },
rowsCompact: { family: "row", minCol: null, thumb: false, dense: false, actionsBelow: false },
// Keeps the 896px cap: its actions dock at the right edge, so a wider row would only stretch the
// dead space between them and the meta.
rows: { family: "row", minCol: null, maxCol: 896, thumb: true, dense: false, actionsBelow: false },
// Wider (1280): everything sits on one line with the actions right-aligned, so there's no gap to
// grow — the extra width just buys more title before it truncates.
rowsCompact: { family: "row", minCol: null, maxCol: 1280, thumb: false, dense: false, actionsBelow: false },
// 380 = thumbnail (160) + gap (12) + ~192 for the text + the block's own padding (16). The text
// floor is the action row's measured 156px plus enough for a title to be worth reading.
// Tuned by measuring the real layouts, not guessed: 3 columns need 3n+32 px, so 380 turns the
@@ -44,7 +51,7 @@ export const FEED_VIEW_SPEC: Record<FeedView, FeedViewSpec> = {
// (~955px) still gets 2. 470 was tried first and needed 956px for two — the common layout fell
// back to ONE column and the mode looked broken; 400 then left 596px tiles whose titles ended
// halfway, which is the very waste this mode exists to kill.
tiles: { family: "row", minCol: 380, thumb: true, dense: false, actionsBelow: true },
tiles: { family: "row", minCol: 380, maxCol: null, thumb: true, dense: false, actionsBelow: true },
};
function isFeedView(v: unknown): v is FeedView {