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:
@@ -15,7 +15,9 @@ import PlexCollectionEditor from "./PlexCollectionEditor";
|
||||
import PlexPlaylistAdd from "./PlexPlaylistAdd";
|
||||
import { PLAYABLE_TINT, playableClass } from "./plexPlayable";
|
||||
import { navItemAttrs } from "../lib/listNav";
|
||||
import { FocusSafeButton } from "./ui/FocusSafeButton";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
|
||||
// The rich "media info" surface for a Plex item — poster/art, IMDb score + link, content rating,
|
||||
// genres, director, cast (with photos), summary. Reused in TWO places: a full-page view opened from
|
||||
@@ -59,14 +61,14 @@ function Score({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return filterable && onFilterRating ? (
|
||||
<button
|
||||
<Pressable
|
||||
onClick={() => onFilterRating(Math.floor(score))}
|
||||
className="inline-flex items-center gap-1 hover:opacity-80"
|
||||
className="inline-flex items-center gap-1 hover:underline"
|
||||
title={t("plex.info.filterRating", { n: Math.floor(score) })}
|
||||
>
|
||||
<Star className="h-3.5 w-3.5 fill-current" />
|
||||
{score.toFixed(1)}
|
||||
</button>
|
||||
</Pressable>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Star className="h-3.5 w-3.5 fill-current" />
|
||||
@@ -257,13 +259,13 @@ function CrewPanel({
|
||||
{crew.map((c, i) => (
|
||||
<div key={`${c.name}:${c.job}:${i}`} className="min-w-0">
|
||||
{onOpenPerson ? (
|
||||
<button
|
||||
<Pressable
|
||||
onClick={() => onOpenPerson(c.name)}
|
||||
title={t("plex.info.viewPerson", { name: c.name })}
|
||||
className="truncate text-left text-sm font-medium hover:text-accent"
|
||||
className="max-w-full truncate text-sm font-medium hover:text-accent"
|
||||
>
|
||||
{c.name}
|
||||
</button>
|
||||
</Pressable>
|
||||
) : (
|
||||
<div className="truncate text-sm font-medium">{c.name}</div>
|
||||
)}
|
||||
@@ -471,13 +473,14 @@ export default function PlexInfo({
|
||||
}
|
||||
/>
|
||||
{overlay && onClose && (
|
||||
<button
|
||||
<IconButton
|
||||
variant="overlay"
|
||||
size="md"
|
||||
onClick={onClose}
|
||||
className="shrink-0 p-1.5 rounded-lg text-white/80 hover:bg-white/15"
|
||||
aria-label={t("plex.player.close")}
|
||||
label={t("plex.player.close")}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<X />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -592,57 +595,41 @@ export default function PlexInfo({
|
||||
{!overlay && (
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
{onPlay && (
|
||||
<button
|
||||
onClick={onPlay}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-accent px-4 py-2 font-medium text-accent-fg hover:opacity-90"
|
||||
>
|
||||
<Play className="w-5 h-5 fill-current" />
|
||||
<Button onClick={onPlay} icon={<Play className="fill-current" />}>
|
||||
{inProgress ? t("plex.resume") : t("plex.play")}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{detail.status === "watched" ? (
|
||||
<button
|
||||
onClick={() => setState("new")}
|
||||
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<Button variant="glass" onClick={() => setState("new")} icon={<RotateCcw />}>
|
||||
{t("plex.info.markUnwatched")}
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setState("watched")}
|
||||
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
<Button variant="glass" onClick={() => setState("watched")} icon={<Check />}>
|
||||
{t("plex.info.markWatched")}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{inProgress && (
|
||||
<button
|
||||
onClick={() => setState("new")}
|
||||
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<Button variant="glass" onClick={() => setState("new")} icon={<RotateCcw />}>
|
||||
{t("plex.info.clearResume")}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{detail.kind === "movie" && (
|
||||
<button
|
||||
<Button
|
||||
variant="glass"
|
||||
onClick={() => setPlaylistOpen(true)}
|
||||
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
||||
icon={<ListPlus />}
|
||||
>
|
||||
<ListPlus className="w-4 h-4" />
|
||||
{t("plex.playlistAdd.manage")}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && detail.kind === "movie" && library && (
|
||||
<button
|
||||
<Button
|
||||
variant="glass"
|
||||
onClick={() => setEditorOpen(true)}
|
||||
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
||||
icon={<FolderPlus />}
|
||||
>
|
||||
<FolderPlus className="w-4 h-4" />
|
||||
{t("plex.collEditor.manage")}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -709,8 +696,9 @@ export default function PlexInfo({
|
||||
</>
|
||||
);
|
||||
return clickable ? (
|
||||
<button
|
||||
<Pressable
|
||||
key={i}
|
||||
align="center"
|
||||
{...navItemAttrs(i)}
|
||||
onClick={() =>
|
||||
onOpenPerson ? onOpenPerson(c.name) : onFilter?.({ actors: [c.name] })
|
||||
@@ -720,10 +708,10 @@ export default function PlexInfo({
|
||||
? t("plex.info.viewPerson", { name: c.name })
|
||||
: t("plex.info.filterActor", { name: c.name })
|
||||
}
|
||||
className="group/cast w-20 shrink-0 text-center"
|
||||
className="group/cast w-20 shrink-0"
|
||||
>
|
||||
{inner}
|
||||
</button>
|
||||
</Pressable>
|
||||
) : (
|
||||
<div key={i} className="w-20 shrink-0 text-center">
|
||||
{inner}
|
||||
@@ -747,27 +735,28 @@ export default function PlexInfo({
|
||||
{col.title}
|
||||
</h2>
|
||||
{onFilter && (
|
||||
<button
|
||||
<Button
|
||||
variant="link-muted"
|
||||
className="text-xs"
|
||||
onClick={() =>
|
||||
onFilter({
|
||||
collections: [col.id],
|
||||
collectionTitles: { [col.id]: col.title },
|
||||
})
|
||||
}
|
||||
className="shrink-0 text-xs text-muted hover:text-accent"
|
||||
>
|
||||
{t("plex.info.browseCollection")}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Carousel ariaLabel={col.title} gap="gap-3" keyboardNav={!!onPlayItem}>
|
||||
{col.items.map((m, i) => (
|
||||
<FocusSafeButton
|
||||
<Pressable
|
||||
key={m.id}
|
||||
{...navItemAttrs(i)}
|
||||
onClick={() => onPlayItem?.(m.id)}
|
||||
disabled={!onPlayItem}
|
||||
className="group/col w-28 shrink-0 text-left"
|
||||
className="group/col w-28 shrink-0"
|
||||
>
|
||||
<div className="relative aspect-2/3 overflow-hidden rounded-lg border border-border bg-surface">
|
||||
<img
|
||||
@@ -804,7 +793,7 @@ export default function PlexInfo({
|
||||
{m.title}
|
||||
</div>
|
||||
{m.year ? <div className="text-2xs text-muted">{m.year}</div> : null}
|
||||
</FocusSafeButton>
|
||||
</Pressable>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user