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
+24 -24
View File
@@ -11,6 +11,7 @@ import Toaster from "./ui/Toaster";
import { useOverlayOrder } from "./ui/overlayOrder";
import { useLayer } from "../lib/layerStack";
import { trapTab, useDialogChrome } from "./ui/dialogFocus";
import { IconButton } from "./ui/IconButton";
// A first-login wizard that walks the user through granting YouTube access one scope at a
// time, each with a plain explanation and an up-front heads-up about Google's "unverified
@@ -172,13 +173,14 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
aria-labelledby={titleId}
className="glass relative w-[min(94vw,460px)] rounded-2xl p-7 text-center outline-none animate-[panelIn_0.26s_cubic-bezier(0.22,1,0.36,1)]"
>
<button
<IconButton
size="md"
onClick={close}
className="absolute top-3 right-3 p-2 rounded-lg text-muted hover:text-fg hover:bg-card/60 transition"
aria-label={t("onboarding.close")}
className="absolute top-3 right-3"
label={t("onboarding.close")}
>
<X className="w-4 h-4" />
</button>
<X />
</IconButton>
{step === "read" && (
<>
@@ -196,12 +198,9 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
</p>
<ConsentHeadsUp />
<div className="mt-5 flex flex-col gap-2">
<button
onClick={() => beginGrant("read")}
className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl font-semibold btn-primary transition"
>
<Youtube className="w-4 h-4" /> {t("onboarding.read.connect")}
</button>
<Button size="lg" icon={<Youtube />} onClick={() => beginGrant("read")}>
{t("onboarding.read.connect")}
</Button>
<Button variant="ghost" size="md" onClick={close}>
{t("onboarding.read.skip")}
</Button>
@@ -254,21 +253,21 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
)}
</p>
{importSubs.isError && (
<button
<Button
variant="outline"
icon={<RefreshCw />}
loading={importSubs.isPending}
onClick={() => importSubs.mutate()}
className="mb-3 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-sm bg-card border border-border hover:border-accent transition"
className="mb-3"
>
<RefreshCw className="w-4 h-4" /> {t("onboarding.write.retryImport")}
</button>
{t("onboarding.write.retryImport")}
</Button>
)}
<ConsentHeadsUp />
<div className="mt-5 flex flex-col gap-2">
<button
onClick={() => beginGrant("write")}
className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl font-semibold btn-primary transition"
>
<ShieldCheck className="w-4 h-4" /> {t("onboarding.write.enableUnsubscribe")}
</button>
<Button size="lg" icon={<ShieldCheck />} onClick={() => beginGrant("write")}>
{t("onboarding.write.enableUnsubscribe")}
</Button>
<Button
variant="ghost"
size="md"
@@ -294,15 +293,16 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
<p className="text-sm text-muted leading-relaxed mt-2 mb-5">
{t("onboarding.done.body")}
</p>
<button
<Button
size="lg"
fullWidth
onClick={() => {
endOnboarding(false);
onClose();
}}
className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl font-semibold btn-primary transition w-full"
>
{t("onboarding.done.done")}
</button>
</Button>
</>
)}