feat(design): the button families on the decided ladder, first half of the sweep (E28 S3b1)
Button is rebuilt to A1/B1/B3: explicit heights (xs 24 / sm 28 / md 32, lg 40 for a hero CTA), r8 (a pill on media), state layers instead of opacity and bg-card hovers, a `pressed` toggle, `overlay`, `media` and `link-muted` variants. The icon-only sizes move to the new IconButton (28 / 32, 48 only for the Play on a picture; a required label). Pressable (a card, row or tile that is the control) and DragHandle join them. The state-* utilities now stand down on a disabled, busy or pressed control, held by theme.contract.test.ts. Swept onto them: the Button icon-size callers and 16 files (Plex playlist, Plex detail, the YT player modal chrome, Welcome, Plex and video cards with AddToPlaylist and DownloadButton, the video editor, Feed, personal views, Scheduler, Users, the nav rail, Plex detail parts, Settings): 268 raw buttons are 153. Fixed on the way: FocusSafeButton overwrote a caller's aria-disabled (the "unavailable, here is why" state; adminInvites caught it), a season toggle and a Plex settings switch had no name/state, and the video card's overlay follows the C5 card (Resume pill, From the start pill, a round Play).
This commit is contained in:
@@ -26,6 +26,8 @@ import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
||||
import { formatDate, formatDuration, formatNumber, formatViews, relativeTime } from "../lib/format";
|
||||
import { isPlainLeftClick } from "../lib/nav";
|
||||
import ChannelNameLink from "./ChannelNameLink";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Button } from "./ui/Button";
|
||||
|
||||
function Actions({
|
||||
video,
|
||||
@@ -54,56 +56,49 @@ 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");
|
||||
// Every button of the row is the IconButton `sm` rung (28), AddToPlaylist and DownloadButton
|
||||
// included. `dense` halves the gap for the small card, whose text column bottoms out around 160px;
|
||||
// six 28px buttons wrap there (the flex-wrap below) until the C1 action list (E28 S3e) moves the
|
||||
// rare ones into the card's "…".
|
||||
return (
|
||||
// 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 reveal transition", dense ? "gap-0.5" : "gap-1")}>
|
||||
<button
|
||||
<IconButton
|
||||
onClick={act("watched")}
|
||||
title={video.status === "watched" ? t("card.watchedUnmark") : t("card.markWatched")}
|
||||
className={clsx(btn, video.status === "watched" && "text-accent")}
|
||||
pressed={video.status === "watched"}
|
||||
label={video.status === "watched" ? t("card.watchedUnmark") : t("card.markWatched")}
|
||||
>
|
||||
{video.status === "watched" ? (
|
||||
<CheckCheck className="w-4 h-4" />
|
||||
) : (
|
||||
<Check className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
{video.status === "watched" ? <CheckCheck /> : <Check />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={toggleSave}
|
||||
title={video.saved ? t("card.savedRemove") : t("card.saveForLater")}
|
||||
className={clsx(btn, video.saved && "fill-current text-accent")}
|
||||
pressed={!!video.saved}
|
||||
label={video.saved ? t("card.savedRemove") : t("card.saveForLater")}
|
||||
>
|
||||
<Bookmark className="w-4 h-4" />
|
||||
</button>
|
||||
<AddToPlaylist videoId={video.id} className={btn} />
|
||||
<DownloadButton videoId={video.id} title={video.title} className={btn} />
|
||||
<button
|
||||
<Bookmark className={clsx(video.saved && "fill-current")} />
|
||||
</IconButton>
|
||||
<AddToPlaylist videoId={video.id} />
|
||||
<DownloadButton videoId={video.id} title={video.title} />
|
||||
<IconButton
|
||||
onClick={act("hidden")}
|
||||
title={video.status === "hidden" ? t("card.unhide") : t("card.hide")}
|
||||
className={clsx(btn, video.status === "hidden" && "text-accent")}
|
||||
pressed={video.status === "hidden"}
|
||||
label={video.status === "hidden" ? t("card.unhide") : t("card.hide")}
|
||||
>
|
||||
{video.status === "hidden" ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
||||
</button>
|
||||
{video.status === "hidden" ? <Eye /> : <EyeOff />}
|
||||
</IconButton>
|
||||
{onResetState && resettable && (
|
||||
<button
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onResetState(video.id);
|
||||
}}
|
||||
title={t("card.resetState")}
|
||||
className={btn}
|
||||
label={t("card.resetState")}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
<RotateCcw />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -285,45 +280,59 @@ function Thumb({
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-2 bg-black/45 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity reveal-hover">
|
||||
{inProgress ? (
|
||||
<>
|
||||
<button
|
||||
onClick={open(null)}
|
||||
title={t("card.continueTitle")}
|
||||
aria-label={t("card.continue")}
|
||||
className={clsx(
|
||||
"inline-flex items-center justify-center gap-1.5 font-semibold btn-primary shadow-lg transition",
|
||||
small ? "w-9 h-9 rounded-full" : "px-3 py-1.5 rounded-lg text-sm",
|
||||
)}
|
||||
>
|
||||
<Play className="w-4 h-4 fill-current" />
|
||||
{!small && t("card.continue")}
|
||||
</button>
|
||||
<button
|
||||
onClick={open(0)}
|
||||
title={t("card.restartTitle")}
|
||||
aria-label={t("card.restart")}
|
||||
className={clsx(
|
||||
"inline-flex items-center justify-center gap-1.5 font-medium bg-white/15 text-white backdrop-blur-xs hover:bg-white/25 transition",
|
||||
small ? "w-9 h-9 rounded-full" : "px-3 py-1.5 rounded-lg text-sm",
|
||||
)}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
{!small && t("card.restart")}
|
||||
</button>
|
||||
{small ? (
|
||||
<IconButton
|
||||
variant="primary"
|
||||
size="md"
|
||||
onMedia
|
||||
onClick={open(null)}
|
||||
label={t("card.continue")}
|
||||
title={t("card.continueTitle")}
|
||||
>
|
||||
<Play className="fill-current" />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onMedia
|
||||
onClick={open(null)}
|
||||
title={t("card.continueTitle")}
|
||||
icon={<Play className="fill-current" />}
|
||||
>
|
||||
{t("card.continue")}
|
||||
</Button>
|
||||
)}
|
||||
{small ? (
|
||||
<IconButton
|
||||
variant="media"
|
||||
size="md"
|
||||
onClick={open(0)}
|
||||
label={t("card.restart")}
|
||||
title={t("card.restartTitle")}
|
||||
>
|
||||
<RotateCcw />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button
|
||||
variant="media"
|
||||
size="sm"
|
||||
onClick={open(0)}
|
||||
title={t("card.restartTitle")}
|
||||
icon={<RotateCcw />}
|
||||
>
|
||||
{t("card.restart")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
<IconButton
|
||||
variant="media"
|
||||
size={small ? "md" : "lg"}
|
||||
onClick={open(null)}
|
||||
title={t("card.play")}
|
||||
aria-label={t("card.play")}
|
||||
className={clsx(
|
||||
"inline-flex items-center justify-center rounded-full bg-accent text-accent-fg shadow-lg hover:scale-105 transition",
|
||||
small ? "w-9 h-9" : "w-12 h-12",
|
||||
)}
|
||||
label={t("card.play")}
|
||||
>
|
||||
<Play
|
||||
className={clsx("fill-current translate-x-px", small ? "w-4 h-4" : "w-5 h-5")}
|
||||
/>
|
||||
</button>
|
||||
<Play className="translate-x-px fill-current" />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user