/ src / components / Button / Button.tsx
Button.tsx
  1  import * as React from 'react';
  2  import { Slot } from '@radix-ui/react-slot';
  3  import { cva, type VariantProps } from 'class-variance-authority';
  4  import { cn } from '../../lib/utils';
  5  
  6  /**
  7   * Button variants using ACDC design system tokens
  8   *
  9   * Uses CSS variables from design-tokens.css for theming:
 10   * - Primary button colors adapt to chain context via --btn-primary-bg
 11   * - Alpha/Delta variants use fixed chain colors
 12   */
 13  const buttonVariants = cva(
 14    // Base styles
 15    [
 16      'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium',
 17      'transition-all duration-150 ease-out',
 18      'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
 19      'disabled:pointer-events-none disabled:opacity-50',
 20    ],
 21    {
 22      variants: {
 23        variant: {
 24          // Primary - uses chain context (alpha by default, delta when [data-chain="delta"])
 25          primary: [
 26            'bg-[var(--btn-primary-bg)] text-white',
 27            'hover:bg-[var(--btn-primary-bg-hover)]',
 28            'active:bg-[var(--btn-primary-bg-active)]',
 29            'focus-visible:ring-[var(--btn-primary-bg)]',
 30          ],
 31          // Secondary - outlined
 32          secondary: [
 33            'bg-transparent border border-[var(--border-default)]',
 34            'text-[var(--text-primary)]',
 35            'hover:border-[var(--btn-primary-bg)] hover:text-[var(--btn-primary-bg)]',
 36            'active:bg-[var(--bg-tertiary)]',
 37            'focus-visible:ring-[var(--btn-primary-bg)]',
 38          ],
 39          // Ghost - minimal
 40          ghost: [
 41            'bg-transparent text-[var(--text-secondary)]',
 42            'hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)]',
 43            'active:bg-[var(--bg-tertiary)]',
 44            'focus-visible:ring-[var(--alpha-500)]',
 45          ],
 46          // Destructive - for dangerous actions
 47          destructive: [
 48            'bg-[var(--error)] text-white',
 49            'hover:bg-[var(--error-hover)]',
 50            'active:bg-[var(--error-active)]',
 51            'focus-visible:ring-[var(--error)]',
 52          ],
 53          // Success - for confirmations
 54          success: [
 55            'bg-[var(--success)] text-white',
 56            'hover:bg-[var(--success-hover)]',
 57            'active:bg-[var(--success-active)]',
 58            'focus-visible:ring-[var(--success)]',
 59          ],
 60          // Link style
 61          link: [
 62            'bg-transparent text-[var(--alpha-500)] underline-offset-4',
 63            'hover:underline',
 64            'focus-visible:ring-[var(--alpha-500)]',
 65          ],
 66          // Alpha - explicitly alpha chain
 67          alpha: [
 68            'bg-[var(--alpha-500)] text-white',
 69            'hover:bg-[var(--alpha-600)]',
 70            'active:bg-[var(--alpha-700)]',
 71            'focus-visible:ring-[var(--alpha-500)]',
 72          ],
 73          // Delta - explicitly delta chain
 74          delta: [
 75            'bg-[var(--delta-500)] text-white',
 76            'hover:bg-[var(--delta-600)]',
 77            'active:bg-[var(--delta-700)]',
 78            'focus-visible:ring-[var(--delta-500)]',
 79          ],
 80        },
 81        size: {
 82          sm: 'h-8 px-3 text-xs rounded-[var(--radius-sm)]',
 83          md: 'h-10 px-4 text-sm rounded-[var(--radius-md)]',
 84          lg: 'h-12 px-6 text-base rounded-[var(--radius-lg)]',
 85          icon: 'h-10 w-10 rounded-[var(--radius-md)]',
 86        },
 87      },
 88      defaultVariants: {
 89        variant: 'primary',
 90        size: 'md',
 91      },
 92    }
 93  );
 94  
 95  export interface ButtonProps
 96    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 97      VariantProps<typeof buttonVariants> {
 98    /** Render as child element (for composition with Link, etc.) */
 99    asChild?: boolean;
100    /** Show loading spinner */
101    loading?: boolean;
102  }
103  
104  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
105    ({ className, variant, size, asChild = false, loading = false, children, disabled, ...props }, ref) => {
106      const Comp = asChild ? Slot : 'button';
107      return (
108        <Comp
109          className={cn(buttonVariants({ variant, size, className }))}
110          ref={ref}
111          disabled={disabled || loading}
112          {...props}
113        >
114          {loading ? (
115            <>
116              <svg
117                className="h-4 w-4 animate-spin"
118                xmlns="http://www.w3.org/2000/svg"
119                fill="none"
120                viewBox="0 0 24 24"
121                aria-hidden="true"
122              >
123                <circle
124                  className="opacity-25"
125                  cx="12"
126                  cy="12"
127                  r="10"
128                  stroke="currentColor"
129                  strokeWidth="4"
130                />
131                <path
132                  className="opacity-75"
133                  fill="currentColor"
134                  d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
135                />
136              </svg>
137              <span>Loading...</span>
138            </>
139          ) : (
140            children
141          )}
142        </Comp>
143      );
144    }
145  );
146  Button.displayName = 'Button';
147  
148  export { Button, buttonVariants };