Skip to content
File

Blob: src/client/components/ui/button.tsx

typescript57 lines
1import { type ButtonHTMLAttributes, type Ref } from "react";
2 
3type Variant = "primary" | "secondary" | "danger" | "ghost";
4type Size = "sm" | "md";
5 
6export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
7 variant?: Variant;
8 size?: Size;
9 loading?: boolean;
10 ref?: Ref<HTMLButtonElement>;
11}
12 
13// Primary uses dark text on the gold accents so contrast clears WCAG AA
14// in both base and hover states. accent-500 + zinc-950 is ~7.9:1,
15// accent-400 (hover) + zinc-950 is ~9.5:1; white text on the same
16// backgrounds is ~2.6:1 and would fail.
17const VARIANT_CLASSES: Record<Variant, string> = {
18 primary: "bg-accent-500 text-zinc-950 hover:bg-accent-400 active:scale-[0.98] transition-transform",
19 secondary:
20 "border border-zinc-700/60 bg-zinc-800/60 text-zinc-200 hover:bg-zinc-700/60 active:scale-[0.98] transition-transform",
21 danger:
22 "border border-red-500/20 bg-red-500/10 text-red-400 hover:bg-red-500/20 active:scale-[0.98] transition-transform",
23 ghost: "text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-100 active:scale-[0.97] transition-transform",
24};
25 
26const SIZE_CLASSES: Record<Size, string> = {
27 sm: "h-9 rounded-lg px-3 text-sm",
28 md: "h-11 rounded-xl px-4 text-sm font-medium",
29};
30 
31export const Button = ({
32 ref,
33 className = "",
34 variant = "primary",
35 size = "md",
36 loading = false,
37 disabled,
38 children,
39 ...rest
40}: ButtonProps) => (
41 <button
42 ref={ref}
43 disabled={disabled || loading}
44 className={`inline-flex items-center justify-center gap-2 disabled:cursor-not-allowed disabled:opacity-60 ${SIZE_CLASSES[size]} ${VARIANT_CLASSES[variant]} ${className}`}
45 aria-busy={loading || undefined}
46 {...rest}
47 >
48 {loading ? (
49 <span
50 className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
51 aria-hidden="true"
52 />
53 ) : null}
54 {children}
55 </button>
56);