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:
@@ -9,7 +9,9 @@ import {
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useDismiss } from "../lib/useDismiss";
|
||||
import { FocusSafeButton } from "./ui/FocusSafeButton";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
|
||||
// Shared "detail page" UI reused by the movie info page (PlexInfo) and the series show/season pages,
|
||||
// so they look consistent (the standing glassy/HTPC brief): the faint fixed art backdrop, the two
|
||||
@@ -32,13 +34,13 @@ export function Filterable({
|
||||
}) {
|
||||
if (!onClick) return <span className={className}>{children}</span>;
|
||||
return (
|
||||
<button
|
||||
<Pressable
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
className={`${className ?? ""} cursor-pointer hover:text-accent transition`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,14 +86,16 @@ export function DetailCustomizeMenu({
|
||||
// any of them needs the platform to portal INTO a caller-named (scaled) container.
|
||||
return (
|
||||
<div className="relative shrink-0">
|
||||
<button
|
||||
<IconButton
|
||||
ref={btnRef}
|
||||
variant={overlay ? "overlay" : "ghost"}
|
||||
size="md"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
title={t("plex.info.customize")}
|
||||
className={`p-1.5 rounded-lg ${overlay ? "text-white/80 hover:bg-white/15" : "text-muted hover:bg-surface hover:text-fg"}`}
|
||||
aria-expanded={open}
|
||||
label={t("plex.info.customize")}
|
||||
>
|
||||
<SlidersHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
<SlidersHorizontal />
|
||||
</IconButton>
|
||||
{open && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
@@ -135,9 +139,12 @@ export function PrefToggle({
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
<Pressable
|
||||
role="switch"
|
||||
aria-checked={on}
|
||||
layer
|
||||
onClick={onClick}
|
||||
className="flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-left hover:bg-surface"
|
||||
className="flex w-full items-center justify-between rounded-lg px-2 py-1.5"
|
||||
>
|
||||
<span>{label}</span>
|
||||
<span
|
||||
@@ -147,7 +154,7 @@ export function PrefToggle({
|
||||
className={`absolute top-0.5 h-3 w-3 rounded-full transition-all ${on ? "left-3.5 bg-accent-fg" : "left-0.5 bg-white"}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,13 +163,9 @@ export function PrefToggle({
|
||||
|
||||
export function BackBtn({ onBack, label }: { onBack: () => void; label: string }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="glass-card glass-hover inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<Button variant="glass" size="sm" icon={<ArrowLeft />} onClick={onBack} className="mb-4">
|
||||
{label}
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,19 +185,16 @@ export function ActionBtn({
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<FocusSafeButton
|
||||
<Button
|
||||
variant={primary ? "primary" : "glass"}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`inline-flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium transition disabled:opacity-50 ${
|
||||
primary ? "btn-primary shadow-xs" : "glass-card glass-hover"
|
||||
}`}
|
||||
icon={<Icon />}
|
||||
>
|
||||
<Icon className="w-4 h-4 shrink-0" />
|
||||
<span className="flex flex-col items-start leading-tight">
|
||||
<span>{label}</span>
|
||||
{sub && <span className="text-2xs font-normal opacity-70">{sub}</span>}
|
||||
</span>
|
||||
</FocusSafeButton>
|
||||
{label}
|
||||
{/* The resume point rides on the same line: the md rung is one line tall (B1). */}
|
||||
{sub && <span className="opacity-75">· {sub}</span>}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ export function DetailHero({
|
||||
return (
|
||||
<div className="glass relative rounded-2xl p-4 sm:p-5 mb-8 flex gap-4 sm:gap-5">
|
||||
<div className="absolute right-3 top-3 z-base">{customizeMenu}</div>
|
||||
<FocusSafeButton
|
||||
<Pressable
|
||||
onClick={onPosterClick}
|
||||
disabled={posterDisabled}
|
||||
className={`group/poster relative ${posterSizeClass} shrink-0 self-start`}
|
||||
@@ -240,7 +240,7 @@ export function DetailHero({
|
||||
<Play className="w-10 h-10 text-white" fill="currentColor" />
|
||||
</div>
|
||||
)}
|
||||
</FocusSafeButton>
|
||||
</Pressable>
|
||||
<div className="min-w-0 flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user