import type { ButtonHTMLAttributes, AnchorHTMLAttributes, ReactNode } from "react"; type ButtonTone = "primary" | "secondary" | "ghost"; type BaseProps = { children: ReactNode; className?: string; tone?: ButtonTone; /** Accessible label for screen readers (overrides children text) */ "aria-label"?: string; /** Indicates if this button is part of a larger grouping */ "aria-haspopup"?: boolean | "menu" | "listbox" | "tree" | "grid" | "dialog"; /** Identifies the element controlled by this button */ "aria-controls"?: string; /** Identifies the expanded state (for toggle buttons) */ "aria-expanded"?: boolean; /** Identifies the pressed state (for toggle buttons) */ "aria-pressed"?: boolean; /** Keyboard navigation: explicit tab index */ tabIndex?: number; }; type NativeButtonProps = BaseProps & ButtonHTMLAttributes & { href?: never; to?: never; }; type AnchorButtonProps = BaseProps & AnchorHTMLAttributes & { href: string; to?: never; }; /** * Button component with accessibility support and multiple visual tones. * * Can be rendered as: * - ` * * @example * // External link * */ export type ButtonProps = NativeButtonProps | AnchorButtonProps; const tones: Record = { primary: "border-accent bg-accent text-bg hover:bg-accent-hover focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg", secondary: "border-border bg-surface-raised text-fg hover:border-border-light hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg", ghost: "border-transparent bg-transparent text-cyan hover:border-border-light hover:bg-surface focus-visible:ring-2 focus-visible:ring-cyan focus-visible:ring-offset-2 focus-visible:ring-offset-bg", }; /** * Shared button styles and accessibility support. * Used by both Button and ButtonAsLink components. */ export function useButtonProps(tone: ButtonTone = "secondary", className: string = "") { return [ "inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-55", "focus-visible:outline-none", tones[tone], className, ] .filter(Boolean) .join(" "); } export function Button({ children, className = "", tone = "secondary", ...props }: ButtonProps) { const classes = useButtonProps(tone, className); // Extract accessibility props to spread on the rendered element const { "aria-label": ariaLabel, "aria-haspopup": ariaHaspopup, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-pressed": ariaPressed, tabIndex, ...restProps } = props; const ariaProps = { "aria-label": ariaLabel, "aria-haspopup": ariaHaspopup, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-pressed": ariaPressed, tabIndex }; // Handle native anchor (href prop) if ("href" in restProps && restProps.href) { const anchorProps = restProps as Omit; return ( {children} ); } // Handle native button const buttonProps = restProps as Omit; return ( ); }