152 lines
4.1 KiB
TypeScript
152 lines
4.1 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import React from 'react'
|
|
|
|
export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'ghost'
|
|
export type ButtonSize = 'small' | 'medium' | 'large'
|
|
|
|
interface ButtonProps {
|
|
children: ReactNode
|
|
variant?: ButtonVariant
|
|
size?: ButtonSize
|
|
disabled?: boolean
|
|
loading?: boolean
|
|
onClick?: () => void
|
|
type?: 'button' | 'submit' | 'reset'
|
|
className?: string
|
|
'aria-label'?: string
|
|
'aria-expanded'?: boolean
|
|
'aria-haspopup'?: boolean | 'true' | 'false' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
|
|
'aria-selected'?: boolean
|
|
'aria-controls'?: string
|
|
role?: string
|
|
id?: string
|
|
}
|
|
|
|
function getVariantClasses(variant: ButtonVariant): string {
|
|
switch (variant) {
|
|
case 'primary':
|
|
return 'bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border-neon-cyan/50 hover:shadow-glow-cyan'
|
|
case 'secondary':
|
|
return 'bg-cyber-light hover:bg-cyber-dark text-cyber-accent border-neon-cyan/30 hover:border-neon-cyan/50'
|
|
case 'success':
|
|
return 'bg-neon-green/20 hover:bg-neon-green/30 text-neon-green border-neon-green/50 hover:shadow-glow-green'
|
|
case 'danger':
|
|
return 'bg-red-500/20 hover:bg-red-500/30 text-red-400 border-red-500/50'
|
|
case 'ghost':
|
|
return 'bg-transparent hover:bg-cyber-light text-cyber-accent border-transparent hover:border-neon-cyan/30'
|
|
default:
|
|
return 'bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border-neon-cyan/50 hover:shadow-glow-cyan'
|
|
}
|
|
}
|
|
|
|
function getSizeClasses(size: ButtonSize): string {
|
|
switch (size) {
|
|
case 'small':
|
|
return 'px-3 py-1.5 text-sm'
|
|
case 'medium':
|
|
return 'px-4 py-2 text-base'
|
|
case 'large':
|
|
return 'px-6 py-3 text-lg'
|
|
default:
|
|
return 'px-4 py-2 text-base'
|
|
}
|
|
}
|
|
|
|
function LoadingSpinner(): React.ReactElement {
|
|
return (
|
|
<svg
|
|
className="animate-spin h-4 w-4"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
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"
|
|
/>
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function ButtonContent({
|
|
children,
|
|
loading,
|
|
}: {
|
|
children: ReactNode
|
|
loading: boolean
|
|
}): React.ReactElement {
|
|
return (
|
|
<>
|
|
{loading && (
|
|
<span className="mr-2">
|
|
<LoadingSpinner />
|
|
</span>
|
|
)}
|
|
{children}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function getCombinedClasses(
|
|
variant: ButtonVariant,
|
|
size: ButtonSize,
|
|
className: string
|
|
): string {
|
|
const variantClasses = getVariantClasses(variant)
|
|
const sizeClasses = getSizeClasses(size)
|
|
const baseClasses = 'inline-flex items-center justify-center rounded-lg font-medium transition-all border focus:outline-none focus:ring-2 focus:ring-neon-cyan disabled:opacity-50 disabled:cursor-not-allowed'
|
|
return `${baseClasses} ${variantClasses} ${sizeClasses} ${className}`.trim()
|
|
}
|
|
|
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref): React.ReactElement => {
|
|
const {
|
|
children,
|
|
variant = 'primary',
|
|
size = 'medium',
|
|
disabled = false,
|
|
loading = false,
|
|
onClick,
|
|
type = 'button',
|
|
className = '',
|
|
'aria-label': ariaLabel,
|
|
'aria-expanded': ariaExpanded,
|
|
'aria-haspopup': ariaHaspopup,
|
|
'aria-selected': ariaSelected,
|
|
'aria-controls': ariaControls,
|
|
role: roleProp,
|
|
id,
|
|
} = props
|
|
const combinedClasses = getCombinedClasses(variant, size, className)
|
|
|
|
return (
|
|
<button
|
|
ref={ref}
|
|
id={id}
|
|
type={type}
|
|
onClick={onClick}
|
|
disabled={disabled || loading}
|
|
className={combinedClasses}
|
|
role={roleProp}
|
|
aria-label={ariaLabel}
|
|
aria-expanded={ariaExpanded}
|
|
aria-haspopup={ariaHaspopup}
|
|
aria-selected={ariaSelected}
|
|
aria-controls={ariaControls}
|
|
aria-busy={loading}
|
|
>
|
|
<ButtonContent loading={loading}>{children}</ButtonContent>
|
|
</button>
|
|
)
|
|
})
|
|
|
|
Button.displayName = 'Button'
|