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
+22 -21
View File
@@ -1,8 +1,11 @@
import { type ReactNode, type CSSProperties } from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown, Eye, EyeOff, GripVertical, Pencil } from "lucide-react";
import { ChevronDown, Eye, EyeOff, Pencil } from "lucide-react";
import { CSS } from "@dnd-kit/utilities";
import { useSortableItem } from "./sortableA11y";
import { DragHandle } from "./DragHandle";
import { IconButton } from "./IconButton";
import { Pressable } from "./Pressable";
// One "island" card inside a SidePanel: a titled glass-card, and — in the panel's edit mode — a drag
// handle (reorder) and a hide toggle. Generalized from the feed sidebar's WidgetCard so all three
@@ -46,7 +49,7 @@ export default function PanelGroup({
const titleIsTrigger = !editing && !collapsible && !!onTitleClick;
const titleCls =
"flex-1 min-w-0 flex items-center gap-1 text-left select-none text-sm uppercase tracking-wide";
"flex-1 min-w-0 flex items-center gap-1 select-none text-sm uppercase tracking-wide";
return (
<div
@@ -58,55 +61,53 @@ export default function PanelGroup({
>
<div className="flex items-center gap-1.5 px-3 py-2">
{editing && (
<button
<DragHandle
{...attributes}
{...listeners}
title={t("sidebar.dragToReorder")}
className="-ml-1 cursor-grab active:cursor-grabbing text-muted hover:text-fg touch-none"
>
<GripVertical className="w-4 h-4" />
</button>
label={t("sidebar.dragToReorder")}
className="-ml-1"
/>
)}
{collapsible && !editing ? (
<button
<Pressable
onClick={onToggleCollapse}
aria-expanded={!collapsed}
className={`${titleCls} text-muted hover:text-fg transition`}
>
<span className="truncate">{title}</span>
</button>
</Pressable>
) : titleIsTrigger ? (
<button
<Pressable
onClick={onTitleClick}
title={titleActionLabel}
className={`${titleCls} group text-muted hover:text-accent transition`}
>
<span className="truncate">{title}</span>
<Pencil className="w-3 h-3 shrink-0 text-muted/60 group-hover:text-accent" />
</button>
</Pressable>
) : (
<span className={`${titleCls} text-muted`}>
<span className="truncate">{title}</span>
</span>
)}
{editing ? (
<button
<IconButton
onClick={onToggleHidden}
title={hidden ? t("sidebar.showWidget") : t("sidebar.hideWidget")}
className="text-muted hover:text-fg"
label={hidden ? t("sidebar.showWidget") : t("sidebar.hideWidget")}
>
{hidden ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
{hidden ? <EyeOff /> : <Eye />}
</IconButton>
) : (
collapsible && (
<button
<IconButton
onClick={onToggleCollapse}
title={collapsed ? t("sidebar.expand") : t("sidebar.collapse")}
className="text-muted hover:text-fg"
aria-expanded={!collapsed}
label={collapsed ? t("sidebar.expand") : t("sidebar.collapse")}
>
<ChevronDown
className={`w-4 h-4 transition-transform ${collapsed ? "-rotate-90" : ""}`}
/>
</button>
</IconButton>
)
)}
</div>