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,83 +1,100 @@
/**
* Badge Component
*
*
* 🔒 STYLE LOCKED - See DESIGN_SYSTEM.md for available colors and variants.
* Do not modify colors or add new ones without updating DESIGN_SYSTEM.md first.
*/
type BadgeVariant = "light" | "solid";
type BadgeSize = "sm" | "md";
type BadgeColor =
| "primary"
| "success"
| "error"
| "warning"
| "info"
| "light"
| "dark";
import clsx from "clsx";
type BadgeVariant = "solid" | "soft" | "outline";
type BadgeSize = "xs" | "sm" | "md";
type BadgeTone = "brand" | "success" | "warning" | "danger" | "info" | "neutral";
interface BadgeProps {
variant?: BadgeVariant; // Light or solid variant
variant?: BadgeVariant; // Visual treatment
size?: BadgeSize; // Badge size
color?: BadgeColor; // Badge color
tone?: BadgeTone; // Badge color tone
startIcon?: React.ReactNode; // Icon at the start
endIcon?: React.ReactNode; // Icon at the end
children: React.ReactNode; // Badge content
className?: string; // Additional classes
}
const toneStyles: Record<
BadgeTone,
{
solid: string;
soft: string;
outline: string;
}
> = {
brand: {
solid: "bg-brand-500 text-white",
soft: "bg-brand-50 text-brand-600 dark:bg-brand-500/15 dark:text-brand-300",
outline:
"text-brand-600 ring-1 ring-brand-200 dark:ring-brand-500/30 dark:text-brand-300",
},
success: {
solid: "bg-success-500 text-white",
soft: "bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-300",
outline:
"text-success-600 ring-1 ring-success-200 dark:ring-success-500/30 dark:text-success-300",
},
warning: {
solid: "bg-warning-500 text-white",
soft: "bg-warning-50 text-warning-600 dark:bg-warning-500/15 dark:text-warning-300",
outline:
"text-warning-600 ring-1 ring-warning-200 dark:ring-warning-500/30 dark:text-warning-300",
},
danger: {
solid: "bg-error-500 text-white",
soft: "bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-300",
outline:
"text-error-600 ring-1 ring-error-200 dark:ring-error-500/30 dark:text-error-300",
},
info: {
solid: "bg-blue-light-500 text-white",
soft: "bg-blue-light-50 text-blue-light-600 dark:bg-blue-light-500/15 dark:text-blue-light-300",
outline:
"text-blue-light-600 ring-1 ring-blue-light-200 dark:ring-blue-light-500/30 dark:text-blue-light-300",
},
neutral: {
solid: "bg-gray-800 text-white",
soft: "bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80",
outline:
"text-gray-700 ring-1 ring-gray-300 dark:ring-white/[0.08] dark:text-white/80",
},
};
const sizeClasses: Record<BadgeSize, string> = {
xs: "h-5 px-2 text-[11px]",
sm: "h-6 px-2.5 text-xs",
md: "h-7 px-3 text-sm",
};
const Badge: React.FC<BadgeProps> = ({
variant = "light",
color = "primary",
size = "md",
variant = "soft",
tone = "brand",
size = "sm",
startIcon,
endIcon,
children,
className = "",
}) => {
const baseStyles =
"inline-flex items-center px-2.5 py-0.5 justify-center gap-1 rounded-full font-medium";
// Define size styles
const sizeStyles = {
sm: "text-theme-xs", // Smaller padding and font size
md: "text-sm", // Default padding and font size
};
// Define color styles for variants
const variants = {
light: {
primary:
"bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400",
success:
"bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-500",
error:
"bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500",
warning:
"bg-warning-50 text-warning-600 dark:bg-warning-500/15 dark:text-orange-400",
info: "bg-blue-light-50 text-blue-light-500 dark:bg-blue-light-500/15 dark:text-blue-light-500",
light: "bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80",
dark: "bg-gray-500 text-white dark:bg-white/5 dark:text-white",
},
solid: {
primary: "bg-brand-500 text-white dark:text-white",
success: "bg-success-500 text-white dark:text-white",
error: "bg-error-500 text-white dark:text-white",
warning: "bg-warning-500 text-white dark:text-white",
info: "bg-blue-light-500 text-white dark:text-white",
light: "bg-gray-400 dark:bg-white/5 text-white dark:text-white/80",
dark: "bg-gray-700 text-white dark:text-white",
},
};
// Get styles based on size and color variant
const sizeClass = sizeStyles[size];
const colorStyles = variants[variant][color];
const toneClass = toneStyles[tone][variant];
return (
<span className={`${baseStyles} ${sizeClass} ${colorStyles} ${className}`}>
{startIcon && <span className="mr-1">{startIcon}</span>}
<span
className={clsx(
"inline-flex items-center justify-center gap-1 rounded-full font-medium uppercase tracking-wide",
sizeClasses[size],
toneClass,
className,
)}
>
{startIcon && <span className="flex items-center">{startIcon}</span>}
{children}
{endIcon && <span className="ml-1">{endIcon}</span>}
{endIcon && <span className="flex items-center">{endIcon}</span>}
</span>
);
};

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";

View File

@@ -1,19 +1,62 @@
import { ReactNode } from "react";
import clsx from "clsx";
import Button from "../button/Button";
type CardVariant = "surface" | "panel" | "frosted" | "borderless" | "gradient";
type CardPadding = "none" | "sm" | "md" | "lg";
type CardShadow = "none" | "sm" | "md";
interface CardProps {
children: ReactNode;
className?: string;
onClick?: () => void;
variant?: CardVariant;
padding?: CardPadding;
shadow?: CardShadow;
}
export const Card: React.FC<CardProps> = ({
children,
className = "",
onClick,
variant = "surface",
padding = "md",
shadow = "sm",
}) => {
const variantClasses: Record<CardVariant, string> = {
surface:
"bg-white border border-gray-200 dark:bg-white/[0.03] dark:border-white/10",
panel:
"bg-gray-50 border border-gray-100 dark:bg-white/[0.04] dark:border-white/[0.04]",
frosted:
"bg-white/80 backdrop-blur-xl border border-white/60 dark:bg-white/[0.08] dark:border-white/20",
borderless: "bg-transparent border border-transparent",
gradient:
"bg-[linear-gradient(180deg,var(--color-panel),var(--color-panel-alt))] border border-white/20 dark:border-white/10",
};
const paddingClasses: Record<CardPadding, string> = {
none: "p-0",
sm: "p-4 sm:p-5",
md: "p-5 sm:p-6",
lg: "p-6 sm:p-8",
};
const shadowClasses: Record<CardShadow, string> = {
none: "",
sm: "shadow-theme-sm",
md: "shadow-theme-lg",
};
return (
<div
className={`rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] sm:p-6 ${className}`}
className={clsx(
"rounded-2xl transition-shadow duration-200",
variantClasses[variant],
paddingClasses[padding],
shadowClasses[shadow],
className,
)}
onClick={onClick}
>
{children}
@@ -108,31 +151,34 @@ export const CardAction: React.FC<CardActionProps> = ({
variant = "button",
className = "",
}) => {
const baseClasses =
variant === "button"
? "inline-flex items-center gap-2 px-4 py-3 mt-4 text-sm font-medium text-white rounded-lg bg-brand-500 shadow-theme-xs hover:bg-brand-600"
: "inline-flex items-center gap-1 mt-4 text-sm text-brand-500 hover:text-brand-600";
if (href) {
if (variant === "button") {
return (
<a
href={href}
className={`${baseClasses} ${className}`}
onClick={onClick}
>
{children}
</a>
<div className={clsx("mt-4 inline-flex", className)}>
<Button
size="sm"
variant="solid"
tone="brand"
as={href ? "a" : "button"}
href={href}
onClick={onClick}
>
{children}
</Button>
</div>
);
}
return (
<button
className={`${baseClasses} ${className}`}
<a
href={href}
onClick={onClick}
type="button"
className={clsx(
"mt-4 inline-flex items-center gap-1 text-sm font-semibold text-brand-600 hover:text-brand-700 dark:text-brand-300",
className,
)}
>
{children}
</button>
</a>
);
};

View File

@@ -0,0 +1,102 @@
import { ReactNode } from "react";
import clsx from "clsx";
interface DataViewProps {
children: ReactNode;
className?: string;
toolbar?: ReactNode;
header?: ReactNode;
footer?: ReactNode;
}
export const DataView: React.FC<DataViewProps> = ({
children,
className = "",
toolbar,
header,
footer,
}) => (
<section
className={clsx(
"rounded-2xl border border-gray-200 bg-white shadow-theme-sm dark:border-white/[0.08] dark:bg-white/[0.04]",
className,
)}
>
{header && (
<div className="border-b border-gray-100 px-6 py-4 dark:border-white/[0.08]">
{header}
</div>
)}
{toolbar && (
<div className="border-b border-gray-100 px-6 py-3 dark:border-white/[0.08]">
{toolbar}
</div>
)}
<div className="overflow-x-auto px-4 py-4 sm:px-6">{children}</div>
{footer && (
<div className="border-t border-gray-100 px-6 py-4 dark:border-white/[0.08]">
{footer}
</div>
)}
</section>
);
interface DataViewHeaderProps {
title: string;
description?: string;
actions?: ReactNode;
}
export const DataViewHeader: React.FC<DataViewHeaderProps> = ({
title,
description,
actions,
}) => (
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h2>
{description && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{description}</p>
)}
</div>
{actions && <div className="flex gap-2">{actions}</div>}
</div>
);
interface DataViewToolbarProps {
children: ReactNode;
className?: string;
}
export const DataViewToolbar: React.FC<DataViewToolbarProps> = ({
children,
className = "",
}) => (
<div
className={clsx(
"flex flex-wrap items-center gap-3 text-sm text-gray-700 dark:text-gray-300",
className,
)}
>
{children}
</div>
);
interface DataViewEmptyStateProps {
title: string;
description: string;
action?: ReactNode;
}
export const DataViewEmptyState: React.FC<DataViewEmptyStateProps> = ({
title,
description,
action,
}) => (
<div className="flex flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-gray-200 bg-gray-50/60 px-6 py-12 text-center dark:border-white/10 dark:bg-white/[0.02]">
<h3 className="text-base font-semibold text-gray-900 dark:text-white">{title}</h3>
<p className="max-w-md text-sm text-gray-500 dark:text-gray-400">{description}</p>
{action}
</div>
);

View File

@@ -0,0 +1,2 @@
export * from "./DataView";