feat(design): the three 375 rows (E28 S3f3)
- Download row: answers to its own width (container query); below 40rem
it stacks: picture beside a two-line title, the meta line across the
row (one line, never wraps: zoomed on the iPad its pieces broke apart),
then the source link and the actions on one wrapping line. The paste
field takes a line of its own when the buttons would squeeze it.
- YT playlist row: below 42rem the length rides on the picture and the
title runs two lines over the channel.
- Channels tag row: the suggestions take a line of their own below
40rem (the chip lane was 0 px and the row's menu off the screen).
- Pager: on a phone the arrows drop their words (names kept), and the
size label stays one line.
- Lint: a class split where the interpolation is the variant itself
(${NARROW}:w-24) is refused too; it had shipped with no CSS.
This commit is contained in:
@@ -69,6 +69,11 @@ export function playlistColumns({
|
||||
{v.thumbnail_url && (
|
||||
<img src={v.thumbnail_url} alt="" className="w-full h-full object-cover" />
|
||||
)}
|
||||
{/* Narrow, the Length column is gone and its value rides on the picture, as on the
|
||||
media card (E28 S3f3, the owner's pick A). */}
|
||||
<span className="pointer-events-none absolute right-0.5 bottom-0.5 @2xl:hidden">
|
||||
{lengthCell(v, t, true)}
|
||||
</span>
|
||||
<span className="absolute inset-0 grid place-items-center bg-black/40 opacity-0 group-hover/thumb:opacity-100 group-focus-visible/thumb:opacity-100 transition">
|
||||
<Play className="w-4 h-4 text-white fill-current" />
|
||||
</span>
|
||||
@@ -80,7 +85,16 @@ export function playlistColumns({
|
||||
data-nav-open=""
|
||||
className="min-h-6 min-w-0 flex-1 touch-target"
|
||||
>
|
||||
<span className="block text-[0.8125rem] text-fg truncate">{v.title}</span>
|
||||
{/* Two lines and the channel under them while the Channel column is hidden (the owner,
|
||||
2026-09-26: at 375 the one-line title kept ~40px); one line beside the columns. */}
|
||||
<span className="break-words text-[0.8125rem] leading-snug text-fg line-clamp-2 @2xl:line-clamp-1">
|
||||
{v.title}
|
||||
</span>
|
||||
{v.channel_title && (
|
||||
<span className="block truncate text-2xs text-muted @2xl:hidden">
|
||||
{v.channel_title}
|
||||
</span>
|
||||
)}
|
||||
</Pressable>
|
||||
</div>
|
||||
),
|
||||
@@ -103,28 +117,15 @@ export function playlistColumns({
|
||||
header: t("playlists.colDuration"),
|
||||
align: "right",
|
||||
width: "5rem",
|
||||
// Narrow, the length rides on the picture instead (the title cell's badge).
|
||||
narrowHidden: true,
|
||||
nowrap: true,
|
||||
sortable: true,
|
||||
// Nulls (live/upcoming, or not yet enriched) sort last in BOTH directions in the old
|
||||
// comparator; sortRows is a plain numeric compare, so park them at the end of an ascending
|
||||
// sort with a large sentinel rather than letting them read as zero-length.
|
||||
sortValue: (v) => v.duration_seconds ?? Number.MAX_SAFE_INTEGER,
|
||||
render: (v) =>
|
||||
v.duration_seconds != null ? (
|
||||
<span className="text-2xs text-muted tabular-nums">
|
||||
{formatDuration(v.duration_seconds)}
|
||||
</span>
|
||||
) : v.live_status === "live" || v.live_status === "upcoming" ? (
|
||||
<span
|
||||
className={`text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
|
||||
v.live_status === "live"
|
||||
? "bg-live text-live-fg" /* the broadcast red, not --danger */
|
||||
: "bg-card border border-border text-muted"
|
||||
}`}
|
||||
>
|
||||
{t(`card.${v.live_status}`)}
|
||||
</span>
|
||||
) : null,
|
||||
render: (v) => lengthCell(v, t),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -142,3 +143,30 @@ export function playlistColumns({
|
||||
|
||||
return cols;
|
||||
}
|
||||
|
||||
// A video's length, or its live / upcoming state when it has none. `onPicture`: the dark badge on the
|
||||
// thumbnail (the narrow row), readable on any frame, like the media card's length badge.
|
||||
function lengthCell(v: Video, t: TFunction, onPicture = false): ReactNode {
|
||||
if (v.duration_seconds != null)
|
||||
return onPicture ? (
|
||||
<span className="block rounded-sm bg-black/80 px-1 text-3xs leading-4 font-medium text-white tabular-nums">
|
||||
{formatDuration(v.duration_seconds)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-2xs text-muted tabular-nums">{formatDuration(v.duration_seconds)}</span>
|
||||
);
|
||||
if (v.live_status !== "live" && v.live_status !== "upcoming") return null;
|
||||
return (
|
||||
<span
|
||||
className={`text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
|
||||
v.live_status === "live"
|
||||
? "bg-live text-live-fg" /* the broadcast red, not --danger */
|
||||
: onPicture
|
||||
? "bg-black/80 text-white"
|
||||
: "bg-card border border-border text-muted"
|
||||
}`}
|
||||
>
|
||||
{t(`card.${v.live_status}`)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user