Files
igny8/frontend/src/components/ui/button/Button.tsx
IGNY8 VPS (Salman) 4f7ab9c606 stlyes fixes
2025-12-29 19:52:51 +00:00

190 lines
6.5 KiB
TypeScript

/**
* Button Component
*
* 🔒 STYLE LOCKED - See DESIGN_SYSTEM.md for available variants and sizes.
* Do not modify variants or add new ones without updating DESIGN_SYSTEM.md first.
*/
import { ReactNode, forwardRef } from "react";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "gradient";
type ButtonTone = "brand" | "success" | "warning" | "danger" | "neutral";
type ButtonShape = "rounded" | "pill";
type ButtonElement = HTMLButtonElement;
interface ButtonProps {
children: ReactNode; // Button text or content
size?: ButtonSize; // Button size
variant?: ButtonVariant; // Visual variant
tone?: ButtonTone; // Color tone
shape?: ButtonShape; // Border radius style
startIcon?: ReactNode; // Icon before the text
endIcon?: ReactNode; // Icon after the text
onClick?: () => void; // Click handler
disabled?: boolean; // Disabled state
fullWidth?: boolean; // Stretch to parent width
className?: string; // Additional classes
type?: "button" | "submit" | "reset"; // Button type
}
const toneMap: Record<
ButtonTone,
{
primary: string;
secondary: string;
outline: string;
ghost: string;
ring: string;
}
> = {
brand: {
primary: "bg-brand-500 text-white hover:bg-brand-600",
secondary:
"bg-gray-500 text-white hover:bg-gray-600 dark:bg-gray-500 dark:hover:bg-gray-600",
outline:
"text-brand-600 ring-1 ring-brand-200 hover:bg-brand-25 dark:ring-brand-500/40 dark:text-brand-300 dark:hover:bg-brand-500/[0.08]",
ghost:
"text-brand-600 hover:bg-brand-50 dark:text-brand-300 dark:hover:bg-brand-500/[0.08]",
ring: "focus-visible:ring-brand-500",
},
success: {
primary: "bg-success-500 text-white hover:bg-success-600",
secondary:
"bg-gray-500 text-white hover:bg-gray-600 dark:bg-gray-500 dark:hover:bg-gray-600",
outline:
"text-success-600 ring-1 ring-success-200 hover:bg-success-25 dark:ring-success-500/40 dark:text-success-300",
ghost:
"text-success-600 hover:bg-success-50 dark:text-success-300 dark:hover:bg-success-500/[0.08]",
ring: "focus-visible:ring-success-500",
},
warning: {
primary: "bg-warning-500 text-white hover:bg-warning-600",
secondary:
"bg-gray-500 text-white hover:bg-gray-600 dark:bg-gray-500 dark:hover:bg-gray-600",
outline:
"text-warning-600 ring-1 ring-warning-200 hover:bg-warning-25 dark:ring-warning-500/40 dark:text-warning-300",
ghost:
"text-warning-600 hover:bg-warning-50 dark:text-warning-300 dark:hover:bg-warning-500/[0.08]",
ring: "focus-visible:ring-warning-500",
},
danger: {
primary: "bg-error-500 text-white hover:bg-error-600",
secondary:
"bg-gray-500 text-white hover:bg-gray-600 dark:bg-gray-500 dark:hover:bg-gray-600",
outline:
"text-error-600 ring-1 ring-error-200 hover:bg-error-25 dark:ring-error-500/40 dark:text-error-300",
ghost:
"text-error-600 hover:bg-error-50 dark:text-error-300 dark:hover:bg-error-500/[0.08]",
ring: "focus-visible:ring-error-500",
},
neutral: {
primary:
"bg-gray-900 text-white hover:bg-gray-800 dark:bg-white/10 dark:hover:bg-white/20",
secondary:
"bg-gray-500 text-white hover:bg-gray-600 dark:bg-gray-500 dark:hover:bg-gray-600",
outline:
"text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50 dark:text-gray-200 dark:ring-white/[0.08] dark:hover:bg-white/[0.04]",
ghost:
"text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/[0.04]",
ring: "focus-visible:ring-gray-400",
},
};
const gradientTone: Record<ButtonTone, string> = {
brand:
"text-white shadow-[0_20px_45px_-30px_rgba(6,147,227,0.9)] bg-[linear-gradient(135deg,var(--color-primary)_0%,var(--color-primary-dark)_100%)]",
success:
"text-white shadow-[0_20px_45px_-30px_rgba(11,191,135,0.9)] bg-[linear-gradient(135deg,var(--color-success)_0%,var(--color-success-dark)_100%)]",
warning:
"text-white shadow-[0_20px_45px_-30px_rgba(255,122,0,0.9)] bg-[linear-gradient(135deg,var(--color-warning)_0%,var(--color-warning-dark)_100%)]",
danger:
"text-white shadow-[0_20px_45px_-30px_rgba(239,68,68,0.9)] bg-[linear-gradient(135deg,var(--color-danger)_0%,var(--color-danger-dark)_100%)]",
neutral:
"text-white shadow-theme-lg bg-[linear-gradient(135deg,var(--color-gray-900),var(--color-gray-800))]",
};
const sizeClasses: Record<ButtonSize, string> = {
xs: "h-7 px-2.5 text-xs",
sm: "h-9 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-5 text-base",
xl: "h-14 px-6 text-lg",
"2xl": "h-auto px-8 py-4 text-lg font-semibold",
};
const Button = forwardRef<ButtonElement, ButtonProps>(
(
{
children,
size = "md",
variant = "primary",
tone = "brand",
shape = "rounded",
startIcon,
endIcon,
onClick,
className = "",
disabled = false,
fullWidth = false,
type = "button",
},
ref,
) => {
const toneStyles = toneMap[tone];
const variantClasses: Record<ButtonVariant, string> = {
primary: toneStyles.primary,
secondary: toneStyles.secondary,
outline: clsx(
"bg-transparent transition-colors",
toneStyles.outline,
"dark:bg-transparent",
),
ghost: toneStyles.ghost,
gradient: gradientTone[tone],
};
const baseClasses =
"inline-flex items-center justify-center gap-2 font-medium transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
const shapeClasses =
shape === "pill" ? "rounded-full" : "rounded-lg";
const ringClass = variant === "gradient" ? "focus-visible:ring-white/70" : toneStyles.ring;
const computedClass = twMerge(
clsx(
baseClasses,
sizeClasses[size],
shapeClasses,
variantClasses[variant],
ringClass,
{
"w-full": fullWidth,
},
className,
),
);
return (
<button
ref={ref}
className={computedClass}
onClick={onClick}
type={type}
disabled={disabled}
>
{startIcon && <span className="flex items-center">{startIcon}</span>}
<span className="whitespace-nowrap">{children}</span>
{endIcon && <span className="flex items-center">{endIcon}</span>}
</button>
);
},
);
Button.displayName = "Button";
export default Button;