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 };