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
+6 -7
View File
@@ -42,6 +42,7 @@ import { PlexPosterCard } from "./plexCards";
import { useNavigationActions } from "./navigationContext";
import { usePlexActions } from "./plexContext";
import { navItemAttrs } from "../lib/listNav";
import { Pressable } from "./ui/Pressable";
// The Home surface (E24) — "Mit nézzek most?".
//
@@ -244,8 +245,7 @@ function ShelfTile({
? t("home.shelf.unwatchedCount", { count: count ?? 0 })
: t("home.shelf.itemCount", { count: count ?? 0 });
return (
<button
type="button"
<Pressable
onClick={() => card.next && onPlay(card)}
{...menuProps?.(card)}
{...(navIndex !== undefined && navItemAttrs(navIndex))}
@@ -257,7 +257,7 @@ function ShelfTile({
// Shift+F10, the only keyboard route to the menu. Inert to the click (the handler guards),
// alive to all three input paths.
aria-disabled={!card.next}
className="block w-full text-left rounded-xl overflow-hidden border border-border bg-surface transition-colors aria-disabled:opacity-60 [&:not([aria-disabled='true'])]:hover:border-accent"
className="block w-full rounded-xl overflow-hidden border border-border bg-surface transition-colors aria-disabled:opacity-60 [&:not([aria-disabled='true'])]:hover:border-accent"
>
<span className="block aspect-video bg-card overflow-hidden">
{card.thumb ? (
@@ -268,7 +268,7 @@ function ShelfTile({
<span className="block text-xs font-medium truncate">{card.title}</span>
<span className="block text-2xs text-muted">{countLabel}</span>
</span>
</button>
</Pressable>
);
}
@@ -307,8 +307,7 @@ function Hero({
{...pinMenu.triggerProps(card)}
className="mb-6 rounded-xl border border-border bg-surface p-4 grid gap-4 sm:grid-cols-[minmax(0,18rem)_1fr]"
>
<button
type="button"
<Pressable
onClick={() => onPlay(card)}
className={`relative block w-full overflow-hidden rounded-lg border border-border bg-card ${
card.aspect === "poster" ? "aspect-[2/3] max-w-[12rem]" : "aspect-video"
@@ -331,7 +330,7 @@ function Hero({
/>
</span>
) : null}
</button>
</Pressable>
<div className="flex min-w-0 flex-col">
<p className="text-2xs font-semibold uppercase tracking-wider text-accent">{kicker}</p>