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
+33 -28
View File
@@ -16,7 +16,8 @@ import Tabs from "./ui/Tabs";
import { Section, SettingRow } from "./ui/form";
import { useMe } from "../lib/useMe";
import { notifyActionError } from "../lib/actionErrors";
import { FocusSafeButton } from "./ui/FocusSafeButton";
import { Button } from "./ui/Button";
import { Pressable } from "./ui/Pressable";
const RANGES = [7, 30, 90] as const;
type StatsTab = "overview" | "system";
@@ -175,26 +176,28 @@ function Overview({ me }: { me: Me }) {
{!me.is_demo && (
<Section title={t("stats.sync.actions")}>
<Tooltip hint={t("stats.sync.syncSubscriptionsHint")}>
<FocusSafeButton
onClick={() => syncSubs.mutate()}
disabled={syncSubs.isPending}
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
>
<RefreshCw className={`w-4 h-4 ${syncSubs.isPending ? "animate-spin" : ""}`} />
{t("stats.sync.syncSubscriptions")}
</FocusSafeButton>
</Tooltip>
<Tooltip hint={t("stats.sync.backfillEverythingHint")}>
<FocusSafeButton
onClick={() => deepAll.mutate()}
disabled={deepAll.isPending}
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition mt-2"
>
<History className={`w-4 h-4 ${deepAll.isPending ? "animate-pulse" : ""}`} />
{t("stats.sync.backfillEverything")}
</FocusSafeButton>
</Tooltip>
<div className="flex flex-wrap gap-2">
<Tooltip hint={t("stats.sync.syncSubscriptionsHint")}>
<Button
variant="glass"
icon={<RefreshCw />}
loading={syncSubs.isPending}
onClick={() => syncSubs.mutate()}
>
{t("stats.sync.syncSubscriptions")}
</Button>
</Tooltip>
<Tooltip hint={t("stats.sync.backfillEverythingHint")}>
<Button
variant="glass"
icon={<History />}
loading={deepAll.isPending}
onClick={() => deepAll.mutate()}
>
{t("stats.sync.backfillEverything")}
</Button>
</Tooltip>
</div>
</Section>
)}
</>
@@ -227,15 +230,16 @@ function SystemStats() {
{s && (
<Section title={t("stats.sync.admin")}>
<Tooltip hint={t("stats.sync.adminPauseHint")}>
<button
<Button
variant="glass"
icon={s.paused ? <Play /> : <Pause />}
loading={pauseResume.isPending}
onClick={() => pauseResume.mutate(s.paused)}
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm transition"
>
{s.paused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
{s.paused
? t("stats.sync.resumeBackgroundSync")
: t("stats.sync.pauseBackgroundSync")}
</button>
</Button>
</Tooltip>
</Section>
)}
@@ -321,9 +325,10 @@ function UserRow({ row, max }: { row: AdminQuotaRow; max: number }) {
const isSystem = row.user_id === null;
return (
<div className="glass-card rounded-xl p-3">
<button
<Pressable
onClick={() => setOpen((o) => !o)}
className="w-full flex items-center gap-3 text-left"
aria-expanded={open}
className="w-full flex items-center gap-3"
>
<span className={`text-sm font-medium truncate flex-1 ${isSystem ? "text-muted" : ""}`}>
{isSystem ? t("stats.system") : row.email}
@@ -331,7 +336,7 @@ function UserRow({ row, max }: { row: AdminQuotaRow; max: number }) {
<span className="text-sm font-semibold tabular-nums shrink-0">
{formatNumber(row.total)}
</span>
</button>
</Pressable>
{/* Proportion bar */}
<div className="mt-2 h-1.5 bg-border/40 rounded-full overflow-hidden">
<div