feat(design): glass surfaces by role, the static Badge, LinkButton, user pickers on the Combobox (E28 S3d1)
This commit is contained in:
@@ -3,6 +3,7 @@ import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { FocusSafeButton } from "./FocusSafeButton";
|
||||
import { iconButtonClass, type IconButtonSize, type IconButtonVariant } from "./buttonStyles";
|
||||
|
||||
// A button that is only a glyph (docs/DESIGN.md A2): two sizes, r8, and a name it cannot ship without
|
||||
// (`label` is required: it is the accessible name and the tooltip). On a picture it is round and dark
|
||||
@@ -10,55 +11,6 @@ import { FocusSafeButton } from "./FocusSafeButton";
|
||||
// pill field it sits inside — `round`), and nothing else. The glyph
|
||||
// is sized here, so a row of them never mixes 14 and 16px icons.
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
| "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).
|
||||
media: `bg-black/60 text-white ${ACT}hover:bg-black/75`,
|
||||
overlay: `text-white/80 ${ACT}hover:text-white ${ACT}hover:bg-white/15`,
|
||||
};
|
||||
|
||||
// An icon toggle that is on shows it in the glyph (watched, saved, a selected track), not as a solid
|
||||
// fill: a solid accent square in a row of quiet glyphs outshouts the row's primary action.
|
||||
const PRESSED = "aria-pressed:text-accent";
|
||||
|
||||
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"> {
|
||||
/** The accessible name and the tooltip. Required: the button renders no text. */
|
||||
label: string;
|
||||
@@ -100,14 +52,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
||||
aria-label={label}
|
||||
aria-pressed={pressed}
|
||||
title={noTitle ? undefined : label}
|
||||
className={clsx(
|
||||
"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],
|
||||
round || variant === "media" ? "rounded-full" : "rounded-lg",
|
||||
pressed !== undefined && PRESSED,
|
||||
SIZES[size],
|
||||
className,
|
||||
)}
|
||||
className={clsx(iconButtonClass({ variant, size, round, pressed }), className)}
|
||||
{...rest}
|
||||
>
|
||||
{loading ? <Loader2 className="animate-spin" /> : children}
|
||||
|
||||
Reference in New Issue
Block a user