feat(e10): re-aim the Button variants at the paint the app actually uses, and migrate the first 15 sites

Counting the un-migrated call sites showed the primitive and the app had
drifted into two different looks: the majority of hand-rolled buttons
hover to accent (outline) and fill with card (ghost), while Button
hovered to fg and filled with surface. That split is the inconsistency
this epic exists to remove, so the primitive follows the majority rather
than the other way round -- 16 existing call sites shift slightly, ~90
hand-rolled ones stop being special.

Adds the variants the call sites prove are missing (outline-accent,
outline-danger, outline-warn, link), an icon-sm size for the p-1.5 icon
family, and a size=none so a link owns no button padding; renames
danger-outline to danger-soft so it cannot be confused with
outline-danger (red at rest vs red on hover). Two new tests lock the
link-size default and the rule that a variant never adds a second
font-weight utility -- without tailwind-merge, two font-* utilities are
decided by stylesheet order, not class order, so that is a real hazard.

Migrates AdminUsers, AuditLog, Channels, TagManager, Playlists,
PlaylistsRail and SettingsPanel (Button call sites 37 -> 52). Left alone
deliberately: conditional-paint sites, video-overlay white buttons, drag
handles, chip removers and card-click targets -- the families Button's
own docstring excludes.

3x tsc, eslint, knip, prettier and 689 vitest green; visual UAT on Users,
Access requests and Playlists at 1280x900, no console errors.
This commit is contained in:
2026-09-05 18:31:00 +02:00
parent 5bfcf5d83a
commit 29bb914a95
9 changed files with 154 additions and 86 deletions
+42 -34
View File
@@ -26,6 +26,7 @@ import {
import { notify } from "../lib/notifications";
import { invalidateUserAccess, isEmailOff } from "../lib/adminUsers";
import { ADMIN_USERS_TAB_KEY } from "../lib/adminUsersTab";
import { Button } from "./ui/Button";
import Tooltip from "./ui/Tooltip";
import InlineNotice from "./ui/InlineNotice";
import { Section } from "./ui/form";
@@ -244,14 +245,16 @@ function UsersRoles({ me }: { me: Me }) {
: t("users.roles.makeAdmin")
}
>
<button
<Button
variant="glass"
size="xs"
className="shrink-0"
onClick={() => onToggle(u)}
disabled={u.is_demo || self || pendingIds.has(u.id)}
className="shrink-0 glass-card glass-hover inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs disabled:opacity-40 transition"
icon={<Shield className="w-3.5 h-3.5" />}
>
<Shield className="w-3.5 h-3.5" />
{u.role === "admin" ? t("users.roles.makeUser") : t("users.roles.makeAdmin")}
</button>
</Button>
</Tooltip>
<Tooltip
hint={
@@ -264,36 +267,36 @@ function UsersRoles({ me }: { me: Me }) {
: t("users.suspend.action")
}
>
<button
<Button
variant={u.is_suspended ? "outline-warn" : "outline"}
size="icon-sm"
className="shrink-0"
onClick={() => onSuspend(u)}
disabled={u.is_demo || self || pendingIds.has(u.id)}
aria-label={
u.is_suspended ? t("users.suspend.undoAction") : t("users.suspend.action")
}
className={`shrink-0 p-1.5 rounded-lg border disabled:opacity-40 transition ${
u.is_suspended
? "border-warn/40 text-warn hover:bg-warn/10"
: "border-border text-muted hover:text-warn"
}`}
>
{u.is_suspended ? (
<RotateCcw className="w-4 h-4" />
) : (
<Ban className="w-4 h-4" />
)}
</button>
icon={
u.is_suspended ? (
<RotateCcw className="w-4 h-4" />
) : (
<Ban className="w-4 h-4" />
)
}
/>
</Tooltip>
<Tooltip
hint={u.is_demo || self ? t("users.delete.locked") : t("users.delete.action")}
>
<button
<Button
variant="outline-danger"
size="icon-sm"
className="shrink-0"
onClick={() => onDelete(u)}
disabled={u.is_demo || self || pendingIds.has(u.id)}
aria-label={t("users.delete.action")}
className="shrink-0 p-1.5 rounded-lg border border-border text-muted hover:text-danger disabled:opacity-40 transition"
>
<Trash2 className="w-4 h-4" />
</button>
icon={<Trash2 className="w-4 h-4" />}
/>
</Tooltip>
</div>
);
@@ -435,25 +438,28 @@ function PendingAccessRowView({
</div>
{!row.email_verified && (
<Tooltip hint={t("users.pendingAccess.resendHint")}>
<button
<Button
variant="outline"
size="icon-sm"
className="shrink-0"
onClick={onResend}
disabled={busy}
className="shrink-0 p-1.5 rounded-lg border border-border text-muted hover:text-fg disabled:opacity-50 transition"
aria-label={t("users.pendingAccess.resend")}
>
<Send className="w-4 h-4" />
</button>
icon={<Send className="w-4 h-4" />}
/>
</Tooltip>
)}
{row.can_activate && (
<Tooltip hint={t("users.pendingAccess.activateHint")}>
<button
<Button
variant="outline-accent"
size="xs"
className="shrink-0"
onClick={onActivate}
disabled={busy}
className="shrink-0 px-2.5 py-1.5 rounded-lg border border-accent/40 text-accent text-xs font-medium hover:bg-accent/10 disabled:opacity-50 transition"
>
{t("users.pendingAccess.activate")}
</button>
</Button>
</Tooltip>
)}
</div>
@@ -673,12 +679,14 @@ function AdminInvites() {
placeholder={t("settings.invites.addPlaceholder")}
className="flex-1 min-w-0 bg-surface border border-border rounded-xl px-3 py-2 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
<Button
type="submit"
className="shrink-0 glass-card glass-hover flex items-center gap-1.5 px-3 py-2 rounded-xl text-sm transition"
variant="glass"
className="shrink-0"
icon={<UserPlus className="w-4 h-4" />}
>
<UserPlus className="w-4 h-4" /> {t("settings.invites.add")}
</button>
{t("settings.invites.add")}
</Button>
</form>
{decided.length > 0 && (