import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react"; import { cx } from "../lib/cx"; export type ButtonVariant = "primary" | "secondary" | "fab"; export interface ButtonProps extends ButtonHTMLAttributes { variant?: ButtonVariant; loading?: boolean; /** Required for `variant="fab"` — icon-only, no visible label. */ icon?: ReactNode; } function Spinner({ dark = false }: { dark?: boolean }) { return ( ); } /** * Shared press behavior: 1px downward shift + swap from elevated to an * inset shadow + slight darkening. This is what actually reads as * "physically pushed in" — scale alone doesn't sell it, the inset shadow * does the work. */ const pressable = cx( "elevation-transition active:translate-y-px active:brightness-95", "shadow-[var(--shadow-elevated)] active:shadow-[var(--shadow-pressed)]" ); const base = cx( "relative inline-flex items-center justify-center gap-2", "font-body font-bold no-select [-webkit-tap-highlight-color:transparent]", "disabled:opacity-40 disabled:pointer-events-none disabled:active:translate-y-0 disabled:active:shadow-[var(--shadow-elevated)]" ); /** * Touch-optimized button set. All variants meet the 48px minimum touch target. * * Text color rule: our brand action gradients (lime→teal, coral→amber) are * light/high-luminance by design — see theme.css. Buttons use --ink text on * them, not white, to stay at WCAG AA without a text-shadow crutch. The * `secondary` variant uses --ink for the same reason (low-transparency * surface fill, not a dark background). */ export const Button = forwardRef( ({ variant = "primary", loading = false, disabled, icon, className, children, ...props }, ref) => { const isDisabled = disabled || loading; if (variant === "fab") { return ( ); } if (variant === "secondary") { return ( ); } // primary return ( ); } ); Button.displayName = "Button";