feat(design): every button on its family, the chip primitive, the lint rule (E28 S3b2)
The second half of the button sweep: the remaining hand-rolled buttons in
feature components, the design lab and the components/ui composites now
stand on Button, IconButton, Pressable, Chip or DragHandle.
- Chip + DismissibleChip (filter / token / tag / suggest / ghost, 24px
floor, 44 on touch) replace five chip geometries; FilterChipGroup
renders Chips; overflow lanes measure a mirror painted by chipClass.
- The Plex control row is IconButton's player rung (36, 20px glyph,
round, overlay; 44 on touch); the YT modal bar is on the sm rung with
pressed cycle toggles and names that survive a folded label.
- New variants: IconButton ghost-accent, danger-soft; Button warn on a
new state-warn utility. The .btn-{tone} recipes and btnCls are gone.
- Fixed on the way: membership rows (collection editor, Plex playlist
add) announce checked/mixed; a one-letter tag chip was 21px wide;
descriptionLinks moved out of lib/ (it renders components).
- local/button-primitive: a raw <button>/<FocusSafeButton> outside the
control-primitive files is a lint error (contract-tested, seen red).
This commit is contained in:
@@ -11,12 +11,14 @@ import { FocusSafeButton } from "./FocusSafeButton";
|
||||
|
||||
type IconButtonVariant =
|
||||
| "ghost" // the default: quiet glyph, a state layer on hover
|
||||
| "ghost-accent" // an accent glyph: the action it stands for is already in effect (queued, requested)
|
||||
| "primary" // solid accent — the one affirmative action of a compact form (Send, Save view)
|
||||
| "outline" // bordered — an icon action that stands on its own in a form or a header
|
||||
| "outline-accent" // bordered + accent — an affirmative row action (Approve)
|
||||
| "outline-danger" // bordered, danger on hover — a row's destructive action beside bordered siblings
|
||||
| "outline-warn" // bordered + warn — a reversible restriction that is currently ON (Suspended)
|
||||
| "danger" // quiet at rest, danger on hover — a row's delete/remove
|
||||
| "danger-soft" // bordered AND danger at rest — a destructive restriction that is ON (Blocked)
|
||||
| "glass" // glass card — beside the glass Buttons on a Plex page
|
||||
| "media" // round, dark translucent — on a thumbnail or a poster
|
||||
| "overlay"; // white glyph on the always-dark player chrome, whatever the theme
|
||||
@@ -24,17 +26,20 @@ type IconButtonVariant =
|
||||
type IconButtonSize =
|
||||
| "sm" // 28 — rows and dense toolbars
|
||||
| "md" // 32 — on its own
|
||||
| "lg"; // 48 — only the Play in the middle of a picture (C5: a not-started card)
|
||||
| "lg" // 48 — only the Play in the middle of a picture (C5: a not-started card)
|
||||
| "player"; // 36 with a 20px glyph, 44 on touch (A3) — the Plex player's control row (C5)
|
||||
|
||||
const ACT = "enabled:not-aria-disabled:";
|
||||
const VARIANTS: Record<IconButtonVariant, string> = {
|
||||
ghost: `text-muted state-layer ${ACT}hover:text-fg`,
|
||||
"ghost-accent": "text-accent state-layer",
|
||||
primary: "state-filled",
|
||||
outline: `border border-border text-muted state-layer ${ACT}hover:text-fg`,
|
||||
"outline-accent": "border border-accent/40 text-accent state-layer",
|
||||
"outline-danger": `border border-border text-muted state-layer ${ACT}hover:text-danger ${ACT}hover:border-danger/40`,
|
||||
"outline-warn": "border border-warn/40 text-warn state-layer",
|
||||
danger: `text-muted state-layer ${ACT}hover:text-danger`,
|
||||
"danger-soft": `border border-danger/60 text-danger state-layer ${ACT}hover:border-danger`,
|
||||
glass: `glass-card glass-hover ${ACT}hover:text-accent`,
|
||||
// The player chrome and a thumbnail are dark in the light theme too, so these two paint with
|
||||
// white and black, not with the theme's fg (whose state layer would be a dark wash on a dark video).
|
||||
@@ -50,6 +55,7 @@ const SIZES: Record<IconButtonSize, string> = {
|
||||
sm: "size-7 [&>svg]:size-4",
|
||||
md: "size-8 [&>svg]:size-4",
|
||||
lg: "size-12 [&>svg]:size-6",
|
||||
player: "size-9 [&>svg]:size-5",
|
||||
};
|
||||
|
||||
export interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
||||
@@ -94,7 +100,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
||||
aria-pressed={pressed}
|
||||
title={noTitle ? undefined : label}
|
||||
className={clsx(
|
||||
"inline-grid shrink-0 place-items-center transition touch-target disabled:opacity-50 disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:cursor-not-allowed",
|
||||
"inline-grid shrink-0 place-items-center transition touch-target aria-expanded:text-accent disabled:opacity-50 disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:cursor-not-allowed",
|
||||
VARIANTS[variant],
|
||||
onMedia || variant === "media" ? "rounded-full" : "rounded-lg",
|
||||
pressed !== undefined && PRESSED,
|
||||
|
||||
Reference in New Issue
Block a user