File
Blob: src/client/components/ui/button.tsx
| 1 | import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react"; |
| 2 | import { Loader2 } from "lucide-react"; |
| 3 | |
| 4 | const variantClasses = { |
| 5 | primary: "bg-accent-600 text-white hover:bg-accent-500 active:scale-[0.98]", |
| 6 | secondary: "border border-zinc-700/60 bg-zinc-800/60 text-zinc-300 hover:bg-zinc-700/60 active:scale-[0.98]", |
| 7 | danger: "border border-red-500/20 bg-red-500/10 text-red-400 hover:bg-red-500/20 active:scale-[0.98]", |
| 8 | ghost: "text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-100 active:scale-[0.97]", |
| 9 | } as const; |
| 10 | |
| 11 | const sizeClasses = { |
| 12 | sm: "gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium", |
| 13 | md: "gap-2 rounded-lg px-5 py-2.5 text-sm font-semibold", |
| 14 | } as const; |
| 15 | |
| 16 | export type ButtonVariant = keyof typeof variantClasses; |
| 17 | export type ButtonSize = keyof typeof sizeClasses; |
| 18 | |
| 19 | const baseClasses = |
| 20 | "inline-flex items-center justify-center transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas disabled:pointer-events-none disabled:opacity-50"; |
| 21 | |
| 22 | export function buttonClasses({ |
| 23 | variant = "secondary", |
| 24 | size = "sm", |
| 25 | className, |
| 26 | }: { |
| 27 | variant?: ButtonVariant; |
| 28 | size?: ButtonSize; |
| 29 | className?: string; |
| 30 | } = {}) { |
| 31 | return [baseClasses, variantClasses[variant], sizeClasses[size], className].filter(Boolean).join(" "); |
| 32 | } |
| 33 | |
| 34 | interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { |
| 35 | variant?: ButtonVariant; |
| 36 | size?: ButtonSize; |
| 37 | loading?: boolean; |
| 38 | icon?: ReactNode; |
| 39 | } |
| 40 | |
| 41 | export const Button = forwardRef<HTMLButtonElement, ButtonProps>( |
| 42 | ({ variant = "secondary", size = "sm", loading, icon, children, className, disabled, ...rest }, ref) => { |
| 43 | return ( |
| 44 | <button |
| 45 | ref={ref} |
| 46 | className={buttonClasses({ variant, size, className })} |
| 47 | disabled={disabled || loading} |
| 48 | {...rest} |
| 49 | > |
| 50 | {loading ? <Loader2 className={size === "sm" ? "h-3.5 w-3.5 animate-spin" : "h-4 w-4 animate-spin"} /> : icon} |
| 51 | {children} |
| 52 | </button> |
| 53 | ); |
| 54 | }, |
| 55 | ); |
| 56 | |
| 57 | Button.displayName = "Button"; |