"use client"; import type { ButtonHTMLAttributes, ReactNode } from "react"; // The primary-button class chain was written out by hand at a dozen call // sites and the ghost/secondary ones at many more, each drifting slightly in // padding and hover colour. Collecting them here also gives every button a // visible keyboard focus ring, which none of them had. const BASE = "inline-flex items-center justify-center gap-1.5 rounded font-semibold transition-colors " + "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 " + "disabled:cursor-not-allowed disabled:opacity-50"; const VARIANTS = { primary: "bg-brand-500 text-white hover:bg-brand-600", secondary: "border border-border bg-white text-ink-body hover:bg-surface", ghost: "text-ink-body hover:bg-surface", danger: "bg-danger-solid text-white hover:brightness-110", // Square icon-only button; pair with an aria-label. icon: "text-ink-muted hover:bg-surface hover:text-ink", } as const; const SIZES = { sm: "px-3 py-1.5 text-xs", md: "px-4 py-2 text-sm", // Meets the 44px touch target without looking oversized on desktop. icon: "p-2", } as const; /** * For anchors that read as buttons (download links, cross-page actions). A * real keeps middle-click and "open in new tab" working, which a button * with an onClick would throw away — so those stay anchors and borrow the * styling instead. */ export const LINK_BUTTON_CLASS = `${BASE} ${VARIANTS.secondary} ${SIZES.sm}`; type ButtonProps = Omit, "className"> & { variant?: keyof typeof VARIANTS; size?: keyof typeof SIZES; /** Disables and shows the busy label; use for in-flight server actions. */ pending?: boolean; pendingLabel?: string; fullWidth?: boolean; className?: string; children?: ReactNode; }; export function Button({ variant = "primary", size, pending = false, pendingLabel, fullWidth, disabled, className = "", children, type = "button", ...rest }: ButtonProps) { const resolvedSize = size ?? (variant === "icon" ? "icon" : "md"); return ( ); }