fix(feed): keep the small card's action row inside the card
Six buttons need 188px but the small card's text column bottoms out near 160 (its 180px column less padding), so the reset button — the only one that is conditional, hence the only one anyone noticed — hung outside the card by up to 15px. The buttons go tighter there (154px), and the row can wrap as a backstop rather than clip. AddToPlaylist and DownloadButton now take the button style as a prop so all six stay one size. Tile actions also pin to the tile's bottom edge, so they land in the same place whether the title above ran to one line or three.
This commit is contained in:
@@ -22,11 +22,14 @@ function Actions({
|
||||
onState,
|
||||
onResetState,
|
||||
onToggleSave,
|
||||
dense = false,
|
||||
}: {
|
||||
video: Video;
|
||||
onState: (id: string, status: string) => void;
|
||||
onResetState?: (id: string) => void;
|
||||
onToggleSave: (id: string, saved: boolean) => void;
|
||||
/** Tighter buttons for the small card, whose column bottoms out at 180px. */
|
||||
dense?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const act = (status: string) => (e: React.MouseEvent) => {
|
||||
@@ -42,15 +45,24 @@ function Actions({
|
||||
e.stopPropagation();
|
||||
onToggleSave(video.id, !video.saved);
|
||||
};
|
||||
// One button style for the row — AddToPlaylist and DownloadButton take it as a prop, so all six
|
||||
// stay the same size instead of two of them keeping their own default.
|
||||
// `dense` shrinks 28px buttons to 24 and halves the gap: six of them need 188px, but the small
|
||||
// card's text column bottoms out around 160 (its 180px column less the padding), so at its
|
||||
// narrowest the last button used to hang outside the card. 154 at dense.
|
||||
const btn = clsx(
|
||||
"rounded-md hover:bg-surface text-muted hover:text-fg",
|
||||
dense ? "p-1" : "p-1.5"
|
||||
);
|
||||
return (
|
||||
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition">
|
||||
// flex-wrap is the safety net, not the plan: the sizing above is what makes them fit. If a
|
||||
// future button or a narrower column breaks that arithmetic, the row wraps to a second line
|
||||
// rather than silently rendering a half-clipped control outside the card.
|
||||
<div className={clsx("flex flex-wrap opacity-0 group-hover:opacity-100 transition", dense ? "gap-0.5" : "gap-1")}>
|
||||
<button
|
||||
onClick={act("watched")}
|
||||
title={video.status === "watched" ? t("card.watchedUnmark") : t("card.markWatched")}
|
||||
className={clsx(
|
||||
"p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg",
|
||||
video.status === "watched" && "text-accent"
|
||||
)}
|
||||
className={clsx(btn, video.status === "watched" && "text-accent")}
|
||||
>
|
||||
{video.status === "watched" ? (
|
||||
<CheckCheck className="w-4 h-4" />
|
||||
@@ -61,22 +73,16 @@ function Actions({
|
||||
<button
|
||||
onClick={toggleSave}
|
||||
title={video.saved ? t("card.savedRemove") : t("card.saveForLater")}
|
||||
className={clsx(
|
||||
"p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg",
|
||||
video.saved && "fill-current text-accent"
|
||||
)}
|
||||
className={clsx(btn, video.saved && "fill-current text-accent")}
|
||||
>
|
||||
<Bookmark className="w-4 h-4" />
|
||||
</button>
|
||||
<AddToPlaylist videoId={video.id} />
|
||||
<DownloadButton videoId={video.id} title={video.title} />
|
||||
<AddToPlaylist videoId={video.id} className={btn} />
|
||||
<DownloadButton videoId={video.id} title={video.title} className={btn} />
|
||||
<button
|
||||
onClick={act("hidden")}
|
||||
title={video.status === "hidden" ? t("card.unhide") : t("card.hide")}
|
||||
className={clsx(
|
||||
"p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg",
|
||||
video.status === "hidden" && "text-accent"
|
||||
)}
|
||||
className={clsx(btn, video.status === "hidden" && "text-accent")}
|
||||
>
|
||||
{video.status === "hidden" ? (
|
||||
<Eye className="w-4 h-4" />
|
||||
@@ -92,7 +98,7 @@ function Actions({
|
||||
onResetState(video.id);
|
||||
}}
|
||||
title={t("card.resetState")}
|
||||
className="p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg"
|
||||
className={btn}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -379,7 +385,13 @@ function VideoCard({
|
||||
);
|
||||
|
||||
const actions = (
|
||||
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} />
|
||||
<Actions
|
||||
video={video}
|
||||
onState={onState}
|
||||
onResetState={onResetState}
|
||||
onToggleSave={onToggleSave}
|
||||
dense={spec.dense}
|
||||
/>
|
||||
);
|
||||
// A permanent container, not a hover-only fill. Rows used to be fully transparent at rest and
|
||||
// paint an opaque bg-card on hover — so running the pointer down the list kept covering and
|
||||
@@ -422,12 +434,14 @@ function VideoCard({
|
||||
onOpen={onOpen}
|
||||
small
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="min-w-0 flex-1 flex flex-col">
|
||||
{textBlock}
|
||||
{/* Tiles put the actions under the meta, in vertical space the thumbnail already
|
||||
reserves — which frees the ~168px the docked-right block used, so the block still
|
||||
works in a narrow column. */}
|
||||
{spec.actionsBelow && <div className="mt-1">{actions}</div>}
|
||||
works in a narrow column. mt-auto pins them to the tile's bottom edge (the flex row
|
||||
already stretches this column to full height), so they land in the same place
|
||||
whether the title above them ran to one line or three. */}
|
||||
{spec.actionsBelow && <div className="mt-auto pt-1">{actions}</div>}
|
||||
</div>
|
||||
{!spec.actionsBelow && actions}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user