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
+21 -21
View File
@@ -21,7 +21,8 @@ import PlexLibraryPicker from "./PlexLibraryPicker";
import { toggleWatchState } from "../lib/plexWatch";
import { LoadingState, StateMessage } from "./ui/QueryState";
import NavGrid from "./ui/NavGrid";
import { FocusSafeButton } from "./ui/FocusSafeButton";
import { Button } from "./ui/Button";
import { IconButton } from "./ui/IconButton";
// The Collection info page (ask B): a hero (art/title/cert/year-range/count/runtime/summary) + the
// ordered member grid, a Play-all / Shuffle over the playable leaves, and — for a NON-external,
@@ -312,12 +313,14 @@ export default function PlexCollectionView({
>
{d.summary}
</p>
<button
<Button
variant="link"
aria-expanded={expanded}
onClick={() => setExpanded((v) => !v)}
className="mt-1 text-xs font-medium text-accent hover:underline"
className="mt-1 text-xs"
>
{expanded ? t("plex.collection.less") : t("plex.collection.more")}
</button>
</Button>
</div>
)}
@@ -412,15 +415,15 @@ export default function PlexCollectionView({
menuProps={pinMenu.triggerProps}
/>
{canEdit && (
<FocusSafeButton
<IconButton
variant="media"
onClick={() => doRemove(c)}
disabled={busy}
title={t("plex.collection.removeItem")}
aria-label={t("plex.collection.removeItem")}
className="glass-card glass-hover absolute right-1.5 top-1.5 z-base grid h-6 w-6 place-items-center rounded-full text-fg disabled:opacity-50"
label={t("plex.collection.removeItem")}
className="absolute right-1.5 top-1.5 z-base"
>
<X className="h-3.5 w-3.5" />
</FocusSafeButton>
<X />
</IconButton>
)}
</div>
))}
@@ -475,13 +478,9 @@ function AddItemPanel({
aria-label={t("plex.collection.addSearch")}
className="glass-card flex-1 rounded-lg px-3 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<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>
<PlexLibraryPicker
scope={scope}
@@ -490,14 +489,15 @@ function AddItemPanel({
renderAction={(c) => {
const inColl = existing.has(c.id) || added.has(c.id);
return (
<FocusSafeButton
<Button
variant="glass"
size="xs"
icon={inColl ? <Check /> : <Plus />}
onClick={() => add(c)}
disabled={inColl}
className="glass-card glass-hover inline-flex items-center gap-1 rounded-lg px-2.5 py-1 text-xs disabled:opacity-50"
>
{inColl ? <Check className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
{inColl ? t("plex.collection.added") : t("plex.collection.add")}
</FocusSafeButton>
</Button>
);
}}
/>