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:
@@ -13,7 +13,7 @@ import { copyText } from "../lib/clipboard";
|
||||
import { formatDateTime } from "../lib/format";
|
||||
import { invalidateUserAccess } from "../lib/adminUsers";
|
||||
import type { AdminUserAccess, AdminUserRow } from "../lib/api/admin";
|
||||
import { FocusSafeButton } from "./ui/FocusSafeButton";
|
||||
import { Button } from "./ui/Button";
|
||||
|
||||
// The admin's answer to "why can this person not get in?", and the place every recovery action for
|
||||
// one account lives (E12 · S5, design option C).
|
||||
@@ -196,24 +196,28 @@ export default function AccountPanel({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{a.can_activate && (
|
||||
<FocusSafeButton
|
||||
<Button
|
||||
variant="glass"
|
||||
fullWidth
|
||||
icon={<Check />}
|
||||
loading={activate.isPending}
|
||||
onClick={() => activate.mutate()}
|
||||
disabled={activate.isPending}
|
||||
className="w-full glass-card glass-hover flex items-center gap-2 px-3 py-2.5 rounded-lg text-xs border border-accent/40 text-accent disabled:opacity-50 transition"
|
||||
className="border border-accent/40 text-accent"
|
||||
>
|
||||
<Check className="w-4 h-4 shrink-0" />
|
||||
<span className="flex-1 text-left">{t("users.account.activate")}</span>
|
||||
</FocusSafeButton>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<FocusSafeButton
|
||||
<Button
|
||||
variant="glass"
|
||||
fullWidth
|
||||
icon={<Send />}
|
||||
loading={resend.isPending}
|
||||
onClick={() => resend.mutate()}
|
||||
// The demo account's address is never confirmed, and the server refuses to mail it — as
|
||||
// it refuses it a set-password link below (deep-review E11 round 3).
|
||||
disabled={user.is_demo || !a.email_configured || a.email_verified || resend.isPending}
|
||||
className="w-full glass-card glass-hover flex items-center gap-2 px-3 py-2.5 rounded-lg text-xs disabled:opacity-45 transition"
|
||||
disabled={user.is_demo || !a.email_configured || a.email_verified}
|
||||
>
|
||||
<Send className="w-4 h-4 shrink-0" />
|
||||
<span className="flex-1 text-left">{t("users.account.resend")}</span>
|
||||
{/* Says WHY it is unavailable instead of just greying out — the no-mail case is the
|
||||
whole reason the link below exists, so naming it here is the explanation. */}
|
||||
@@ -223,18 +227,21 @@ export default function AccountPanel({
|
||||
{a.email_configured && a.email_verified && (
|
||||
<span className="text-muted">{t("users.account.alreadyConfirmed")}</span>
|
||||
)}
|
||||
</FocusSafeButton>
|
||||
</Button>
|
||||
|
||||
<FocusSafeButton
|
||||
<Button
|
||||
variant="glass"
|
||||
fullWidth
|
||||
icon={<KeyRound />}
|
||||
loading={issue.isPending}
|
||||
disabled={user.is_demo}
|
||||
onClick={() => issue.mutate()}
|
||||
disabled={issue.isPending || user.is_demo}
|
||||
className="w-full glass-card glass-hover flex items-center gap-2 px-3 py-2.5 rounded-lg text-xs border border-accent/40 text-accent disabled:opacity-50 transition"
|
||||
className="border border-accent/40 text-accent"
|
||||
>
|
||||
<KeyRound className="w-4 h-4 shrink-0" />
|
||||
<span className="flex-1 text-left">
|
||||
{link ? t("users.account.setPasswordAgain") : t("users.account.setPassword")}
|
||||
</span>
|
||||
</FocusSafeButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{link && (
|
||||
@@ -243,13 +250,14 @@ export default function AccountPanel({
|
||||
<div className="flex-1 min-w-0 bg-surface border border-border rounded-lg px-3 py-2 text-2xs font-mono truncate">
|
||||
{link.url}
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="glass"
|
||||
icon={copied ? <Check /> : <Copy />}
|
||||
onClick={copy}
|
||||
className="shrink-0 glass-card glass-hover inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs border border-accent/40 text-accent transition"
|
||||
className="border border-accent/40 text-accent"
|
||||
>
|
||||
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
{copied ? t("users.account.copied") : t("users.account.copy")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-2xs text-muted leading-relaxed">{t("users.account.linkNote")}</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user