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:
@@ -12,7 +12,9 @@ import {
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Contrast, Reference, Sample } from "./labKit";
|
||||
import { DANGER, FILLED, H, LAYER, STRONG } from "./labTokens";
|
||||
import { H, LAYER, STRONG } from "./labTokens";
|
||||
import { Button } from "../components/ui/Button";
|
||||
import { IconButton } from "../components/ui/IconButton";
|
||||
|
||||
// E28 S2 · Batch A — controls, DECIDED (owner, 2026-09-24: all six as recommended), rendered with the
|
||||
// batch B decisions that refine them: B1's compact ladder and B3's state layers. Only the chosen
|
||||
@@ -20,14 +22,7 @@ import { DANGER, FILLED, H, LAYER, STRONG } from "./labTokens";
|
||||
|
||||
type Tone = "primary" | "ghost" | "outline" | "glass" | "danger";
|
||||
|
||||
const TONE: Record<Tone, string> = {
|
||||
primary: FILLED,
|
||||
ghost: clsx("text-muted hover:text-fg", LAYER),
|
||||
outline: clsx("border border-border text-muted hover:text-fg", LAYER),
|
||||
glass: clsx("glass-card glass-hover hover:text-accent"),
|
||||
danger: DANGER,
|
||||
};
|
||||
|
||||
// The lab renders the shipped primitives (E28 S3b), so a reference can never drift from the app.
|
||||
export function Btn({
|
||||
tone,
|
||||
size,
|
||||
@@ -40,48 +35,24 @@ export function Btn({
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
"inline-flex items-center justify-center rounded-lg font-medium transition",
|
||||
TONE[tone],
|
||||
H[size],
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<Button variant={tone} size={size} icon={icon}>
|
||||
{children}
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const ico = "w-4 h-4";
|
||||
|
||||
function IconBtn({ box, children }: { box: string; children: ReactNode }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
"grid place-items-center rounded-lg text-muted hover:text-fg transition",
|
||||
LAYER,
|
||||
box,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function OnMedia() {
|
||||
const b = "grid h-8 w-8 place-items-center rounded-full bg-black/55 text-white hover:bg-black/75";
|
||||
return (
|
||||
<div className="relative aspect-video w-56 overflow-hidden rounded-lg bg-linear-to-br from-accent/50 to-card">
|
||||
<span className="absolute bottom-1.5 right-1.5 flex gap-1.5">
|
||||
<button type="button" className={b}>
|
||||
<Play className={ico} />
|
||||
</button>
|
||||
<button type="button" className={b}>
|
||||
<ListPlus className={ico} />
|
||||
</button>
|
||||
<IconButton label="Play" variant="media" size="md">
|
||||
<Play />
|
||||
</IconButton>
|
||||
<IconButton label="Add to playlist" variant="media" size="md">
|
||||
<ListPlus />
|
||||
</IconButton>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -178,9 +149,9 @@ export function BatchA() {
|
||||
<Btn tone="glass" size="sm" icon={<RefreshCw className={ico} />}>
|
||||
Sync now
|
||||
</Btn>
|
||||
<IconBtn box={H.iconSm}>
|
||||
<MoreHorizontal className={ico} />
|
||||
</IconBtn>
|
||||
<IconButton label="More" size="sm">
|
||||
<MoreHorizontal />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Sample>
|
||||
<Sample label="Dialog footer · md">
|
||||
@@ -215,26 +186,26 @@ export function BatchA() {
|
||||
<div className="flex items-center justify-between rounded-lg bg-card/60 px-3 py-0.5 text-sm">
|
||||
<span className="truncate">Techno Tim</span>
|
||||
<span className="flex gap-1">
|
||||
<IconBtn box={H.iconSm}>
|
||||
<Pencil className={ico} />
|
||||
</IconBtn>
|
||||
<IconBtn box={H.iconSm}>
|
||||
<RefreshCw className={ico} />
|
||||
</IconBtn>
|
||||
<IconBtn box={H.iconSm}>
|
||||
<Trash2 className={ico} />
|
||||
</IconBtn>
|
||||
<IconButton label="Rename" size="sm">
|
||||
<Pencil />
|
||||
</IconButton>
|
||||
<IconButton label="Refresh" size="sm">
|
||||
<RefreshCw />
|
||||
</IconButton>
|
||||
<IconButton label="Delete" size="sm">
|
||||
<Trash2 />
|
||||
</IconButton>
|
||||
</span>
|
||||
</div>
|
||||
</Sample>
|
||||
<Sample label="On its own · 32">
|
||||
<div className="flex gap-2">
|
||||
<IconBtn box={H.icon}>
|
||||
<Download className={ico} />
|
||||
</IconBtn>
|
||||
<IconBtn box={H.icon}>
|
||||
<MoreHorizontal className={ico} />
|
||||
</IconBtn>
|
||||
<IconButton label="Download" size="md">
|
||||
<Download />
|
||||
</IconButton>
|
||||
<IconButton label="More" size="md">
|
||||
<MoreHorizontal />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Sample>
|
||||
<Sample label="On a picture">
|
||||
|
||||
@@ -19,7 +19,6 @@ export const STRONG = "var(--edge-strong)";
|
||||
/** B3 state layers (index.css utilities). */
|
||||
export const LAYER = "state-layer";
|
||||
export const FILLED = "state-filled";
|
||||
export const DANGER = "state-danger";
|
||||
|
||||
/** B9: the header + fade edge (index.css utilities; the host is positioned). */
|
||||
export const FADE_BELOW = "fade-below";
|
||||
|
||||
Reference in New Issue
Block a user