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:
2026-09-24 23:03:23 +02:00
parent 86f467bd65
commit 81936e9f56
94 changed files with 1327 additions and 1093 deletions
+13 -19
View File
@@ -12,7 +12,8 @@ import { LoadingState, StateMessage } from "./ui/QueryState";
import PosterTile from "./PosterTile";
import PlexLibraryPicker from "./PlexLibraryPicker";
import NavGrid from "./ui/NavGrid";
import { FocusSafeButton } from "./ui/FocusSafeButton";
import { Button } from "./ui/Button";
import { IconButton } from "./ui/IconButton";
// A collection tile's sub-line: item count · total runtime (each dropped when unknown, e.g. show-only
// collections have no movie runtime).
@@ -109,13 +110,9 @@ function NewCollectionPanel({
<div className="glass-card mb-6 rounded-2xl p-4">
<div className="mb-2 flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold">{t("plex.collection.newCollection")}</h3>
<button
onClick={onClose}
aria-label={t("common.close")}
className="glass-hover grid h-8 w-8 place-items-center rounded-lg"
>
<X className="h-4 w-4" />
</button>
<IconButton size="md" onClick={onClose} label={t("common.close")}>
<X />
</IconButton>
</div>
<input
autoFocus
@@ -146,14 +143,15 @@ function NewCollectionPanel({
term={term}
enabled={nameOk}
renderAction={(c) => (
<FocusSafeButton
<Button
variant="glass"
size="xs"
icon={<Check />}
onClick={() => create(c)}
disabled={busy}
className="glass-card glass-hover inline-flex items-center gap-1 rounded-lg px-2.5 py-1 text-xs disabled:opacity-50"
loading={busy}
>
<Check className="h-3.5 w-3.5" />
{t("plex.collection.createWith")}
</FocusSafeButton>
</Button>
)}
/>
</div>
@@ -190,13 +188,9 @@ export function PlexCollectionsBrowse({ onPick }: { onPick: (id: string, title:
className="glass-card w-full max-w-sm rounded-lg px-3 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
{isAdmin && !creating && (
<button
onClick={() => setCreating(true)}
className="glass-card glass-hover inline-flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm"
>
<Plus className="h-4 w-4" />
<Button variant="glass" size="sm" icon={<Plus />} onClick={() => setCreating(true)}>
{t("plex.collection.newCollection")}
</button>
</Button>
)}
</div>
{creating && (