2025-12-17 02:03:33 +07:00
|
|
|
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'
|
|
|
|
|
import { clsx } from 'clsx'
|
|
|
|
|
import { Loader2 } from 'lucide-react'
|
|
|
|
|
|
|
|
|
|
interface NeonButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
|
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
|
|
|
|
|
size?: 'sm' | 'md' | 'lg'
|
|
|
|
|
color?: 'neon' | 'purple' | 'pink'
|
|
|
|
|
isLoading?: boolean
|
|
|
|
|
icon?: ReactNode
|
|
|
|
|
iconPosition?: 'left' | 'right'
|
|
|
|
|
glow?: boolean
|
|
|
|
|
pulse?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const NeonButton = forwardRef<HTMLButtonElement, NeonButtonProps>(
|
|
|
|
|
(
|
|
|
|
|
{
|
|
|
|
|
className,
|
|
|
|
|
variant = 'primary',
|
|
|
|
|
size = 'md',
|
|
|
|
|
color = 'neon',
|
|
|
|
|
isLoading,
|
|
|
|
|
icon,
|
|
|
|
|
iconPosition = 'left',
|
|
|
|
|
glow = true,
|
|
|
|
|
pulse = false,
|
|
|
|
|
children,
|
|
|
|
|
disabled,
|
|
|
|
|
...props
|
|
|
|
|
},
|
|
|
|
|
ref
|
|
|
|
|
) => {
|
|
|
|
|
const colorMap = {
|
|
|
|
|
neon: {
|
|
|
|
|
primary: 'bg-neon-500 hover:bg-neon-400 text-dark-900',
|
|
|
|
|
secondary: 'bg-dark-600 hover:bg-dark-500 text-neon-400 border border-neon-500/30',
|
|
|
|
|
outline: 'bg-transparent border-2 border-neon-500 text-neon-500 hover:bg-neon-500 hover:text-dark-900',
|
|
|
|
|
ghost: 'bg-transparent hover:bg-neon-500/10 text-neon-400',
|
|
|
|
|
danger: 'bg-red-600 hover:bg-red-700 text-white',
|
2025-12-17 14:53:56 +07:00
|
|
|
glow: '0 0 12px rgba(34, 211, 238, 0.4)',
|
|
|
|
|
glowHover: '0 0 18px rgba(34, 211, 238, 0.55)',
|
2025-12-17 20:59:47 +07:00
|
|
|
glowDanger: '0 0 12px rgba(239, 68, 68, 0.4)',
|
|
|
|
|
glowDangerHover: '0 0 18px rgba(239, 68, 68, 0.55)',
|
2025-12-17 02:03:33 +07:00
|
|
|
},
|
|
|
|
|
purple: {
|
|
|
|
|
primary: 'bg-accent-500 hover:bg-accent-400 text-white',
|
|
|
|
|
secondary: 'bg-dark-600 hover:bg-dark-500 text-accent-400 border border-accent-500/30',
|
|
|
|
|
outline: 'bg-transparent border-2 border-accent-500 text-accent-500 hover:bg-accent-500 hover:text-white',
|
|
|
|
|
ghost: 'bg-transparent hover:bg-accent-500/10 text-accent-400',
|
|
|
|
|
danger: 'bg-red-600 hover:bg-red-700 text-white',
|
2025-12-17 14:53:56 +07:00
|
|
|
glow: '0 0 12px rgba(139, 92, 246, 0.4)',
|
|
|
|
|
glowHover: '0 0 18px rgba(139, 92, 246, 0.55)',
|
2025-12-17 20:59:47 +07:00
|
|
|
glowDanger: '0 0 12px rgba(239, 68, 68, 0.4)',
|
|
|
|
|
glowDangerHover: '0 0 18px rgba(239, 68, 68, 0.55)',
|
2025-12-17 02:03:33 +07:00
|
|
|
},
|
|
|
|
|
pink: {
|
|
|
|
|
primary: 'bg-pink-500 hover:bg-pink-400 text-white',
|
|
|
|
|
secondary: 'bg-dark-600 hover:bg-dark-500 text-pink-400 border border-pink-500/30',
|
|
|
|
|
outline: 'bg-transparent border-2 border-pink-500 text-pink-500 hover:bg-pink-500 hover:text-white',
|
|
|
|
|
ghost: 'bg-transparent hover:bg-pink-500/10 text-pink-400',
|
|
|
|
|
danger: 'bg-red-600 hover:bg-red-700 text-white',
|
2025-12-17 14:53:56 +07:00
|
|
|
glow: '0 0 12px rgba(244, 114, 182, 0.4)',
|
|
|
|
|
glowHover: '0 0 18px rgba(244, 114, 182, 0.55)',
|
2025-12-17 20:59:47 +07:00
|
|
|
glowDanger: '0 0 12px rgba(239, 68, 68, 0.4)',
|
|
|
|
|
glowDangerHover: '0 0 18px rgba(239, 68, 68, 0.55)',
|
2025-12-17 02:03:33 +07:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sizeClasses = {
|
|
|
|
|
sm: 'px-3 py-1.5 text-sm gap-1.5',
|
|
|
|
|
md: 'px-4 py-2.5 text-base gap-2',
|
|
|
|
|
lg: 'px-6 py-3 text-lg gap-2.5',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const iconSizes = {
|
|
|
|
|
sm: 'w-4 h-4',
|
|
|
|
|
md: 'w-5 h-5',
|
|
|
|
|
lg: 'w-6 h-6',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const colors = colorMap[color]
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
ref={ref}
|
|
|
|
|
disabled={disabled || isLoading}
|
|
|
|
|
className={clsx(
|
|
|
|
|
'inline-flex items-center justify-center font-semibold rounded-lg',
|
|
|
|
|
'transition-all duration-300 ease-out',
|
|
|
|
|
'disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none',
|
|
|
|
|
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-dark-900',
|
|
|
|
|
color === 'neon' && 'focus:ring-neon-500',
|
|
|
|
|
color === 'purple' && 'focus:ring-accent-500',
|
|
|
|
|
color === 'pink' && 'focus:ring-pink-500',
|
|
|
|
|
colors[variant],
|
|
|
|
|
sizeClasses[size],
|
|
|
|
|
pulse && 'neon-glow-pulse',
|
|
|
|
|
className
|
|
|
|
|
)}
|
|
|
|
|
style={{
|
2025-12-17 20:59:47 +07:00
|
|
|
boxShadow: glow && !disabled && variant !== 'ghost'
|
|
|
|
|
? (variant === 'danger' ? colors.glowDanger : colors.glow)
|
|
|
|
|
: undefined,
|
2025-12-17 02:03:33 +07:00
|
|
|
}}
|
|
|
|
|
onMouseEnter={(e) => {
|
|
|
|
|
if (glow && !disabled && variant !== 'ghost') {
|
2025-12-17 20:59:47 +07:00
|
|
|
e.currentTarget.style.boxShadow = variant === 'danger' ? colors.glowDangerHover : colors.glowHover
|
2025-12-17 02:03:33 +07:00
|
|
|
}
|
|
|
|
|
props.onMouseEnter?.(e)
|
|
|
|
|
}}
|
|
|
|
|
onMouseLeave={(e) => {
|
|
|
|
|
if (glow && !disabled && variant !== 'ghost') {
|
2025-12-17 20:59:47 +07:00
|
|
|
e.currentTarget.style.boxShadow = variant === 'danger' ? colors.glowDanger : colors.glow
|
2025-12-17 02:03:33 +07:00
|
|
|
}
|
|
|
|
|
props.onMouseLeave?.(e)
|
|
|
|
|
}}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{isLoading && <Loader2 className={clsx(iconSizes[size], 'animate-spin')} />}
|
|
|
|
|
{!isLoading && icon && iconPosition === 'left' && (
|
|
|
|
|
<span className={iconSizes[size]}>{icon}</span>
|
|
|
|
|
)}
|
|
|
|
|
{children}
|
|
|
|
|
{!isLoading && icon && iconPosition === 'right' && (
|
|
|
|
|
<span className={iconSizes[size]}>{icon}</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
NeonButton.displayName = 'NeonButton'
|
|
|
|
|
|
|
|
|
|
// Gradient button variant
|
|
|
|
|
interface GradientButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
|
|
size?: 'sm' | 'md' | 'lg'
|
|
|
|
|
isLoading?: boolean
|
|
|
|
|
icon?: ReactNode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const GradientButton = forwardRef<HTMLButtonElement, GradientButtonProps>(
|
|
|
|
|
({ className, size = 'md', isLoading, icon, children, disabled, ...props }, ref) => {
|
|
|
|
|
const sizeClasses = {
|
|
|
|
|
sm: 'px-3 py-1.5 text-sm gap-1.5',
|
|
|
|
|
md: 'px-4 py-2.5 text-base gap-2',
|
|
|
|
|
lg: 'px-6 py-3 text-lg gap-2.5',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
ref={ref}
|
|
|
|
|
disabled={disabled || isLoading}
|
|
|
|
|
className={clsx(
|
|
|
|
|
'relative inline-flex items-center justify-center font-semibold rounded-lg',
|
|
|
|
|
'bg-gradient-to-r from-neon-500 via-accent-500 to-pink-500',
|
|
|
|
|
'text-white transition-all duration-300',
|
2025-12-17 14:53:56 +07:00
|
|
|
'hover:shadow-[0_0_20px_rgba(139,92,246,0.35)]',
|
2025-12-17 02:03:33 +07:00
|
|
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
|
|
|
'focus:outline-none focus:ring-2 focus:ring-accent-500 focus:ring-offset-2 focus:ring-offset-dark-900',
|
|
|
|
|
sizeClasses[size],
|
|
|
|
|
className
|
|
|
|
|
)}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{isLoading && <Loader2 className="w-5 h-5 animate-spin" />}
|
|
|
|
|
{!isLoading && icon && <span className="w-5 h-5">{icon}</span>}
|
|
|
|
|
{children}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
GradientButton.displayName = 'GradientButton'
|