phase 1-3 css refactor

This commit is contained in:
Desktop
2025-11-14 15:04:47 +05:00
parent 9eee5168bb
commit 27465457d5
26 changed files with 818 additions and 429 deletions

View File

@@ -1,70 +1,209 @@
/**
* 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";
import { Link } from "react-router-dom";
type ButtonSize = "xs" | "sm" | "md" | "lg";
type ButtonVariant = "solid" | "soft" | "outline" | "ghost" | "gradient";
type ButtonTone = "brand" | "success" | "warning" | "danger" | "neutral";
type ButtonShape = "rounded" | "pill";
type ButtonElement = HTMLButtonElement | HTMLAnchorElement;
interface ButtonProps {
children: ReactNode; // Button text or content
size?: "sm" | "md"; // Button size
variant?: "primary" | "outline" | "secondary" | "success"; // Button variant
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
as?: "button" | "a" | typeof Link;
href?: string;
to?: string; // For React Router Link
target?: string;
rel?: string;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
children,
size = "md",
variant = "primary",
startIcon,
endIcon,
onClick,
className = "",
disabled = false,
type = "button",
}, ref) => {
// Size Classes
const sizeClasses = {
sm: "px-3 py-1.5 text-xs h-8",
md: "px-3 py-2 text-sm h-9",
};
// Variant Classes
const variantClasses = {
primary:
"bg-brand-500 text-white shadow-theme-xs hover:bg-brand-600 disabled:bg-brand-300",
const toneMap: Record<
ButtonTone,
{
solid: string;
soft: string;
outline: string;
ghost: string;
ring: string;
}
> = {
brand: {
solid: "bg-brand-500 text-white hover:bg-brand-600",
soft: "bg-brand-50 text-brand-600 hover:bg-brand-100",
outline:
"bg-white text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-700 dark:hover:bg-white/[0.03] dark:hover:text-gray-300",
secondary:
"bg-gray-500 text-white border border-gray-500 hover:bg-gray-600 hover:border-gray-600 dark:bg-gray-500 dark:border-gray-500 dark:hover:bg-gray-600 dark:hover:border-gray-600",
success:
"bg-success-500 text-white shadow-theme-xs hover:bg-success-600 disabled:bg-success-300",
};
"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: {
solid: "bg-success-500 text-white hover:bg-success-600",
soft: "bg-success-50 text-success-600 hover:bg-success-100",
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: {
solid: "bg-warning-500 text-white hover:bg-warning-600",
soft: "bg-warning-50 text-warning-600 hover:bg-warning-100",
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: {
solid: "bg-error-500 text-white hover:bg-error-600",
soft: "bg-error-50 text-error-600 hover:bg-error-100",
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: {
solid:
"bg-gray-900 text-white hover:bg-gray-800 dark:bg-white/10 dark:hover:bg-white/20",
soft:
"bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-white/[0.08] dark:text-white dark:hover:bg-white/[0.12]",
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",
},
};
return (
<button
ref={ref}
type={type}
className={`inline-flex items-center justify-center gap-2 rounded-lg transition ${className} ${
sizeClasses[size]
} ${variantClasses[variant]} ${
disabled ? "cursor-not-allowed opacity-50" : ""
}`}
onClick={onClick}
disabled={disabled}
>
{startIcon && <span className="flex items-center">{startIcon}</span>}
{children}
{endIcon && <span className="flex items-center">{endIcon}</span>}
</button>
);
});
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,#0f172a,#1e293b)]",
};
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",
};
const Button = forwardRef<ButtonElement, ButtonProps>(
(
{
children,
size = "md",
variant = "solid",
tone = "brand",
shape = "rounded",
startIcon,
endIcon,
onClick,
className = "",
disabled = false,
fullWidth = false,
type = "button",
as = "button",
href,
to,
target,
rel,
},
ref,
) => {
const toneStyles = toneMap[tone];
const variantClasses: Record<ButtonVariant, string> = {
solid: toneStyles.solid,
soft: toneStyles.soft,
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,
),
);
const Component = as === "a" ? "a" : as === Link ? Link : "button";
return (
<Component
ref={ref}
className={computedClass}
onClick={onClick}
{...(as === "button"
? {
type,
disabled,
}
: as === Link
? {
to,
"aria-disabled": disabled,
}
: {
href,
target,
rel,
"aria-disabled": disabled,
})}
>
{startIcon && <span className="flex items-center">{startIcon}</span>}
<span className="whitespace-nowrap">{children}</span>
{endIcon && <span className="flex items-center">{endIcon}</span>}
</Component>
);
},
);
Button.displayName = "Button";